From c31325e98274c838b23b6fe42970684df03b8872 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 11 Aug 2025 20:01:25 +0200 Subject: [PATCH 1/6] fix: resolve mutex conflicts in Instagram webhook specs (#12154) This PR fixes flaky test failures in the Instagram webhook specs that were caused by Redis mutex lock conflicts when tests ran in parallel. ### The Problem: The InstagramEventsJob uses a Redis mutex with a key based on sender_id and ig_account_id to prevent race conditions. However, all test factories were using the same hardcoded sender_id: 'Sender-id-1', causing multiple test instances to compete for the same mutex lock when running in parallel. ### The Solution: - Updated all Instagram event factories to generate unique sender IDs using SecureRandom.hex(4) - Modified test stubs and expectations to work with dynamic sender IDs instead of hardcoded values - Ensured each test instance gets its own unique mutex key, eliminating lock contention --- .../instagram/message_builder_spec.rb | 88 +++++++----- .../messenger/message_builder_spec.rb | 132 ++++++++++-------- .../instagram_message_create_event.rb | 93 +++++++++--- .../webhooks/instagram_events_job_spec.rb | 65 +++++---- spec/rails_helper.rb | 1 + spec/support/instagram_spec_helpers.rb | 20 +++ 6 files changed, 252 insertions(+), 147 deletions(-) create mode 100644 spec/support/instagram_spec_helpers.rb diff --git a/spec/builders/messages/instagram/message_builder_spec.rb b/spec/builders/messages/instagram/message_builder_spec.rb index 8e863823a..386087fbe 100644 --- a/spec/builders/messages/instagram/message_builder_spec.rb +++ b/spec/builders/messages/instagram/message_builder_spec.rb @@ -17,40 +17,34 @@ describe Messages::Instagram::MessageBuilder do let!(:shared_reel_params) { build(:instagram_shared_reel_event).with_indifferent_access } let!(:instagram_story_reply_event) { build(:instagram_story_reply_event).with_indifferent_access } let!(:instagram_message_reply_event) { build(:instagram_message_reply_event).with_indifferent_access } - let!(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') } - let!(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_inbox.id, source_id: 'Sender-id-1') } - let(:conversation) do - create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id) - end - let(:message) do - create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing', - source_id: 'message-id-1') - end describe '#perform' do before do instagram_channel.update(access_token: 'valid_instagram_token') - stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-1\?.*}) + stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-.*?\?.*}) .to_return( status: 200, - body: { - name: 'Jane', - username: 'some_user_name', - profile_pic: 'https://chatwoot-assets.local/sample.png', - id: 'Sender-id-1', - follower_count: 100, - is_user_follow_business: true, - is_business_follow_user: true, - is_verified_user: false - }.to_json, + body: proc { |request| + sender_id = request.uri.path.split('/').last.split('?').first + { + name: 'Jane', + username: 'some_user_name', + profile_pic: 'https://chatwoot-assets.local/sample.png', + id: sender_id, + follower_count: 100, + is_user_follow_business: true, + is_business_follow_user: true, + is_verified_user: false + }.to_json + }, headers: { 'Content-Type' => 'application/json' } ) end it 'creates contact and message for the instagram direct inbox' do messaging = dm_params[:entry][0]['messaging'][0] - contact_inbox + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform instagram_inbox.reload @@ -63,13 +57,15 @@ describe Messages::Instagram::MessageBuilder do end it 'discard echo message already sent by chatwoot' do - conversation - message + messaging = dm_params[:entry][0]['messaging'][0] + contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) + conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id) + create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing', + source_id: 'message-id-1') expect(instagram_inbox.conversations.count).to be 1 expect(instagram_inbox.messages.count).to be 1 - messaging = dm_params[:entry][0]['messaging'][0] messaging[:message][:mid] = 'message-id-1' # Set same source_id as the existing message described_class.new(messaging, instagram_inbox, outgoing_echo: true).perform @@ -81,6 +77,7 @@ describe Messages::Instagram::MessageBuilder do it 'discards duplicate messages from webhook events with the same message_id' do messaging = dm_params[:entry][0]['messaging'][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform initial_message_count = instagram_inbox.messages.count @@ -93,6 +90,7 @@ describe Messages::Instagram::MessageBuilder do it 'creates message for shared reel' do messaging = shared_reel_params[:entry][0]['messaging'][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform message = instagram_inbox.messages.first @@ -103,7 +101,9 @@ describe Messages::Instagram::MessageBuilder do end it 'creates message with story id' do - story_source_id = instagram_story_reply_event[:entry][0]['messaging'][0]['message']['mid'] + messaging = instagram_story_reply_event[:entry][0]['messaging'][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) + story_source_id = messaging['message']['mid'] stub_request(:get, %r{https://graph\.instagram\.com/.*?/#{story_source_id}\?.*}) .to_return( @@ -121,7 +121,6 @@ describe Messages::Instagram::MessageBuilder do headers: { 'Content-Type' => 'application/json' } ) - messaging = instagram_story_reply_event[:entry][0]['messaging'][0] described_class.new(messaging, instagram_inbox).perform message = instagram_inbox.messages.first @@ -134,10 +133,13 @@ describe Messages::Instagram::MessageBuilder do it 'creates message with reply to mid' do # Create first message to ensure reply to is valid first_messaging = dm_params[:entry][0]['messaging'][0] + sender_id = first_messaging['sender']['id'] + create_instagram_contact_for_sender(sender_id, instagram_inbox) described_class.new(first_messaging, instagram_inbox).perform - # Create second message with reply to mid + # Create second message with reply to mid, using same sender_id messaging = instagram_message_reply_event[:entry][0]['messaging'][0] + messaging['sender']['id'] = sender_id described_class.new(messaging, instagram_inbox).perform first_message = instagram_inbox.messages.first @@ -148,12 +150,13 @@ describe Messages::Instagram::MessageBuilder do end it 'handles deleted story' do - story_source_id = story_mention_params[:entry][0][:messaging][0]['message']['mid'] + messaging = story_mention_params[:entry][0][:messaging][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) + story_source_id = messaging['message']['mid'] stub_request(:get, %r{https://graph\.instagram\.com/.*?/#{story_source_id}\?.*}) .to_return(status: 404, body: { error: { message: 'Story not found', code: 1_609_005 } }.to_json) - messaging = story_mention_params[:entry][0][:messaging][0] described_class.new(messaging, instagram_inbox).perform message = instagram_inbox.messages.first @@ -163,11 +166,12 @@ describe Messages::Instagram::MessageBuilder do end it 'does not create message for unsupported file type' do - conversation + messaging = story_mention_params[:entry][0][:messaging][0] + contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) + create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id) # try to create a message with unsupported file type - story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type' - messaging = story_mention_params[:entry][0][:messaging][0] + messaging['message']['attachments'][0]['type'] = 'unsupported_type' described_class.new(messaging, instagram_inbox, outgoing_echo: false).perform @@ -177,7 +181,11 @@ describe Messages::Instagram::MessageBuilder do end it 'does not create message if the message is already exists' do - message + messaging = dm_params[:entry][0]['messaging'][0] + contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) + conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id) + create(:message, account_id: account.id, inbox_id: instagram_inbox.id, conversation_id: conversation.id, message_type: 'outgoing', + source_id: 'message-id-1') expect(instagram_inbox.conversations.count).to be 1 expect(instagram_inbox.messages.count).to be 1 @@ -194,7 +202,7 @@ describe Messages::Instagram::MessageBuilder do instagram_channel.update(access_token: 'invalid_token') # Stub the request to return authorization error status - stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-1\?.*}) + stub_request(:get, %r{https://graph\.instagram\.com/.*?/Sender-id-.*?\?.*}) .to_return( status: 401, body: { error: { message: 'unauthorized access token', code: 190 } }.to_json, @@ -218,6 +226,7 @@ describe Messages::Instagram::MessageBuilder do it 'creates a new conversation if existing conversation is not present' do initial_count = Conversation.count messaging = dm_params[:entry][0]['messaging'][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform @@ -226,21 +235,23 @@ describe Messages::Instagram::MessageBuilder do end it 'will not create a new conversation if last conversation is not resolved' do + messaging = dm_params[:entry][0]['messaging'][0] + contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id, status: :open) - messaging = dm_params[:entry][0]['messaging'][0] described_class.new(messaging, instagram_inbox).perform expect(instagram_inbox.conversations.last.id).to eq(existing_conversation.id) end it 'creates a new conversation if last conversation is resolved' do + messaging = dm_params[:entry][0]['messaging'][0] + contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id, status: :resolved) initial_count = Conversation.count - messaging = dm_params[:entry][0]['messaging'][0] described_class.new(messaging, instagram_inbox).perform @@ -257,6 +268,7 @@ describe Messages::Instagram::MessageBuilder do it 'creates a new conversation if existing conversation is not present' do initial_count = Conversation.count messaging = dm_params[:entry][0]['messaging'][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform @@ -265,6 +277,8 @@ describe Messages::Instagram::MessageBuilder do end it 'reopens last conversation if last conversation is resolved' do + messaging = dm_params[:entry][0]['messaging'][0] + contact = create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) existing_conversation = create(:conversation, account_id: account.id, inbox_id: instagram_inbox.id, contact_id: contact.id, status: :resolved) @@ -307,6 +321,7 @@ describe Messages::Instagram::MessageBuilder do it 'saves story information when story mention is processed' do messaging = story_mention_params[:entry][0][:messaging][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform message = instagram_inbox.messages.first @@ -328,6 +343,7 @@ describe Messages::Instagram::MessageBuilder do ) messaging = story_mention_params[:entry][0][:messaging][0] + create_instagram_contact_for_sender(messaging['sender']['id'], instagram_inbox) described_class.new(messaging, instagram_inbox).perform message = instagram_inbox.messages.first diff --git a/spec/builders/messages/instagram/messenger/message_builder_spec.rb b/spec/builders/messages/instagram/messenger/message_builder_spec.rb index 03194673a..593dfb946 100644 --- a/spec/builders/messages/instagram/messenger/message_builder_spec.rb +++ b/spec/builders/messages/instagram/messenger/message_builder_spec.rb @@ -17,30 +17,23 @@ describe Messages::Instagram::Messenger::MessageBuilder do let!(:instagram_story_reply_event) { build(:instagram_story_reply_event).with_indifferent_access } let!(:instagram_message_reply_event) { build(:instagram_message_reply_event).with_indifferent_access } let(:fb_object) { double } - let(:contact) { create(:contact, id: 'Sender-id-1', name: 'Jane Dae') } - let(:contact_inbox) { create(:contact_inbox, contact_id: contact.id, inbox_id: instagram_messenger_inbox.id, source_id: 'Sender-id-1') } - let(:conversation) do - create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id, - additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' }) - end - let(:message) do - create(:message, account_id: account.id, inbox_id: instagram_messenger_inbox.id, conversation_id: conversation.id, message_type: 'outgoing', - source_id: 'message-id-1') - end describe '#perform' do it 'creates contact and message for the facebook inbox' do + messaging = dm_params[:entry][0]['messaging'][0] + sender_id = messaging['sender']['id'] + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access ) - messaging = dm_params[:entry][0]['messaging'][0] - contact_inbox + + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(messaging, instagram_messenger_inbox).perform instagram_messenger_inbox.reload @@ -56,7 +49,13 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'discard echo message already sent by chatwoot' do - message + messaging = dm_params[:entry][0]['messaging'][0] + sender_id = messaging['sender']['id'] + contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) + conversation = create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id, + additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' }) + create(:message, account_id: account.id, inbox_id: instagram_messenger_inbox.id, conversation_id: conversation.id, message_type: 'outgoing', + source_id: 'message-id-1') expect(instagram_messenger_inbox.conversations.count).to be 1 expect(instagram_messenger_inbox.messages.count).to be 1 @@ -65,13 +64,11 @@ describe Messages::Instagram::Messenger::MessageBuilder do allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access ) - messaging = dm_params[:entry][0]['messaging'][0] - contact_inbox described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: true).perform instagram_messenger_inbox.reload @@ -81,17 +78,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'creates message for shared reel' do + messaging = shared_reel_params[:entry][0]['messaging'][0] + sender_id = messaging['sender']['id'] + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access ) - messaging = shared_reel_params[:entry][0]['messaging'][0] - contact_inbox + + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(messaging, instagram_messenger_inbox).perform message = instagram_messenger_channel.inbox.messages.first @@ -102,18 +102,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'creates message with for reply with story id' do + messaging = instagram_story_reply_event[:entry][0]['messaging'][0] + sender_id = messaging['sender']['id'] + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access ) - messaging = instagram_story_reply_event[:entry][0]['messaging'][0] - contact_inbox + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(messaging, instagram_messenger_inbox).perform message = instagram_messenger_channel.inbox.messages.first @@ -125,24 +127,26 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'creates message with for reply with mid' do + # create first message to ensure reply to is valid + first_message_data = dm_params[:entry][0]['messaging'][0] + sender_id = first_message_data['sender']['id'] + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access ) - # create first message to ensure reply to is valid - first_message = dm_params[:entry][0]['messaging'][0] - contact_inbox - described_class.new(first_message, instagram_messenger_inbox).perform - # create the second message with the reply to mid set + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) + described_class.new(first_message_data, instagram_messenger_inbox).perform + + # create the second message with the reply to mid set, ensure same sender_id messaging = instagram_message_reply_event[:entry][0]['messaging'][0] - contact_inbox - + messaging['sender']['id'] = sender_id # Use the same sender_id described_class.new(messaging, instagram_messenger_inbox).perform first_message = instagram_messenger_channel.inbox.messages.first message = instagram_messenger_channel.inbox.messages.last @@ -153,14 +157,16 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'raises exception on deleted story' do + messaging = story_mention_params[:entry][0][:messaging][0] + sender_id = messaging['sender']['id'] + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError.new( 190, 'This Message has been deleted by the user or the business.' )) - messaging = story_mention_params[:entry][0][:messaging][0] - contact_inbox + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform instagram_messenger_inbox.reload @@ -180,22 +186,24 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'does not create message for unsupported file type' do + # create a message with unsupported file type + story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type' + messaging = story_mention_params[:entry][0][:messaging][0] + sender_id = messaging['sender']['id'] + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access ) - conversation - - # create a message with unsupported file type - story_mention_params[:entry][0][:messaging][0]['message']['attachments'][0]['type'] = 'unsupported_type' - messaging = story_mention_params[:entry][0][:messaging][0] - + contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) + create(:conversation, account_id: account.id, inbox_id: instagram_messenger_inbox.id, contact_id: contact.id, + additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' }) described_class.new(messaging, instagram_messenger_inbox, outgoing_echo: false).perform instagram_messenger_inbox.reload @@ -218,18 +226,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do it 'creates a new conversation if existing conversation is not present' do inital_count = Conversation.count message = dm_params[:entry][0]['messaging'][0] - contact_inbox + sender_id = message['sender']['id'] + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(message, instagram_messenger_inbox).perform instagram_messenger_inbox.reload - contact_inbox.reload expect(instagram_messenger_inbox.conversations.count).to eq(1) expect(Conversation.count).to eq(inital_count + 1) end it 'will not create a new conversation if last conversation is not resolved' do + message = dm_params[:entry][0]['messaging'][0] + sender_id = message['sender']['id'] + contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) + existing_conversation = create( :conversation, account_id: account.id, @@ -239,18 +251,18 @@ describe Messages::Instagram::Messenger::MessageBuilder do additional_attributes: { type: 'instagram_direct_message', conversation_language: 'en' } ) - message = dm_params[:entry][0]['messaging'][0] - contact_inbox - described_class.new(message, instagram_messenger_inbox).perform instagram_messenger_inbox.reload - contact_inbox.reload expect(instagram_messenger_inbox.conversations.last.id).to eq(existing_conversation.id) end it 'creates a new conversation if last conversation is resolved' do + message = dm_params[:entry][0]['messaging'][0] + sender_id = message['sender']['id'] + contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) + existing_conversation = create( :conversation, account_id: account.id, @@ -261,13 +273,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do ) inital_count = Conversation.count - message = dm_params[:entry][0]['messaging'][0] - contact_inbox - described_class.new(message, instagram_messenger_inbox).perform instagram_messenger_inbox.reload - contact_inbox.reload expect(instagram_messenger_inbox.conversations.last.id).not_to eq(existing_conversation.id) expect(Conversation.count).to eq(inital_count + 1) @@ -283,18 +291,22 @@ describe Messages::Instagram::Messenger::MessageBuilder do it 'creates a new conversation if existing conversation is not present' do inital_count = Conversation.count message = dm_params[:entry][0]['messaging'][0] - contact_inbox + sender_id = message['sender']['id'] + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(message, instagram_messenger_inbox).perform instagram_messenger_inbox.reload - contact_inbox.reload expect(instagram_messenger_inbox.conversations.count).to eq(1) expect(Conversation.count).to eq(inital_count + 1) end it 'reopens last conversation if last conversation is resolved' do + message = dm_params[:entry][0]['messaging'][0] + sender_id = message['sender']['id'] + contact = create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) + existing_conversation = create( :conversation, account_id: account.id, @@ -306,13 +318,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do inital_count = Conversation.count - message = dm_params[:entry][0]['messaging'][0] - contact_inbox - described_class.new(message, instagram_messenger_inbox).perform instagram_messenger_inbox.reload - contact_inbox.reload expect(instagram_messenger_inbox.conversations.last.id).to eq(existing_conversation.id) expect(Conversation.count).to eq(inital_count) @@ -344,7 +352,9 @@ describe Messages::Instagram::Messenger::MessageBuilder do allow(fb_object).to receive(:get_object).and_return(story_data) messaging = story_mention_params[:entry][0][:messaging][0] - contact_inbox + sender_id = messaging['sender']['id'] + + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) builder = described_class.new(messaging, instagram_messenger_inbox) builder.perform @@ -358,18 +368,20 @@ describe Messages::Instagram::Messenger::MessageBuilder do end it 'handles story mentions specifically in the Instagram builder' do + messaging = story_mention_params[:entry][0][:messaging][0] + sender_id = messaging['sender']['id'] + # First allow contact info fetch allow(fb_object).to receive(:get_object).and_return({ name: 'Jane', - id: 'Sender-id-1' + id: sender_id }.with_indifferent_access) # Then allow story data fetch allow(fb_object).to receive(:get_object).with(anything, fields: %w[story from]) .and_return(story_data) - messaging = story_mention_params[:entry][0][:messaging][0] - contact_inbox + create_instagram_contact_for_sender(sender_id, instagram_messenger_inbox) described_class.new(messaging, instagram_messenger_inbox).perform message = instagram_messenger_inbox.messages.first diff --git a/spec/factories/instagram/instagram_message_create_event.rb b/spec/factories/instagram/instagram_message_create_event.rb index 66d5e8a81..a5de8e7d3 100644 --- a/spec/factories/instagram/instagram_message_create_event.rb +++ b/spec/factories/instagram/instagram_message_create_event.rb @@ -1,14 +1,18 @@ FactoryBot.define do factory :instagram_message_create_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-123', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -27,15 +31,19 @@ FactoryBot.define do end factory :instagram_message_standby_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { 'time': '2021-09-08T06:34:04+0000', - 'id': 'instagram-message-id-123', + 'id': ig_entry_id, 'standby': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -54,15 +62,19 @@ FactoryBot.define do end factory :instagram_story_reply_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-123', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -87,15 +99,19 @@ FactoryBot.define do end factory :instagram_message_reply_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-123', + 'id': ig_entry_id, 'time': '2021-09-08T06:35:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -117,11 +133,14 @@ FactoryBot.define do end factory :instagram_test_text_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + end entry do [ { 'time' => 1_661_141_837_537, - 'id' => '0', + 'id' => ig_entry_id, 'messaging' => [ { 'sender' => { @@ -144,15 +163,19 @@ FactoryBot.define do end factory :instagram_message_unsend_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-123', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -171,15 +194,19 @@ FactoryBot.define do end factory :instagram_message_attachment_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-1234', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -205,15 +232,19 @@ FactoryBot.define do end factory :instagram_shared_reel_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-1234', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -241,15 +272,19 @@ FactoryBot.define do end factory :instagram_story_mention_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-1234', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -275,15 +310,19 @@ FactoryBot.define do end factory :instagram_story_mention_event_with_echo, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-1234', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -310,15 +349,19 @@ FactoryBot.define do end factory :instagram_message_unsupported_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-unsupported-id-123', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' @@ -337,15 +380,19 @@ FactoryBot.define do end factory :messaging_seen_event, class: Hash do + transient do + ig_entry_id { SecureRandom.uuid } + sender_id { "Sender-id-#{SecureRandom.hex(4)}" } + end entry do [ { - 'id': 'instagram-message-id-123', + 'id': ig_entry_id, 'time': '2021-09-08T06:34:04+0000', 'messaging': [ { 'sender': { - 'id': 'Sender-id-1' + 'id': sender_id }, 'recipient': { 'id': 'chatwoot-app-user-id-1' diff --git a/spec/jobs/webhooks/instagram_events_job_spec.rb b/spec/jobs/webhooks/instagram_events_job_spec.rb index 1962c78a5..9edd9a34d 100644 --- a/spec/jobs/webhooks/instagram_events_job_spec.rb +++ b/spec/jobs/webhooks/instagram_events_job_spec.rb @@ -10,19 +10,10 @@ describe Webhooks::InstagramEventsJob do end let!(:account) { create(:account) } - let(:return_object) do - { name: 'Jane', - id: 'Sender-id-1', - account_id: instagram_messenger_inbox.account_id, - profile_pic: 'https://chatwoot-assets.local/sample.png', - username: 'some_user_name' } - end let!(:instagram_messenger_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') } let!(:instagram_messenger_inbox) { create(:inbox, channel: instagram_messenger_channel, account: account, greeting_enabled: false) } - let!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') } let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) } - # Combined message events into one helper let(:message_events) do { @@ -37,6 +28,14 @@ describe Webhooks::InstagramEventsJob do } end + def return_object_for(sender_id) + { name: 'Jane', + id: sender_id, + account_id: instagram_messenger_inbox.account_id, + profile_pic: 'https://chatwoot-assets.local/sample.png', + username: 'some_user_name' } + end + describe '#perform' do context 'when handling messaging events for Instagram via Facebook page' do let(:fb_object) { double } @@ -47,8 +46,9 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id] allow(fb_object).to receive(:get_object).and_return( - return_object.with_indifferent_access + return_object_for(sender_id).with_indifferent_access ) instagram_webhook.perform_now(message_events[:dm][:entry]) @@ -63,8 +63,9 @@ describe Webhooks::InstagramEventsJob do it 'creates standby message in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + sender_id = message_events[:standby][:entry][0][:standby][0][:sender][:id] allow(fb_object).to receive(:get_object).and_return( - return_object.with_indifferent_access + return_object_for(sender_id).with_indifferent_access ) instagram_webhook.perform_now(message_events[:standby][:entry]) @@ -82,10 +83,11 @@ describe Webhooks::InstagramEventsJob do it 'handle instagram unsend message event' do message = create(:message, inbox_id: instagram_messenger_inbox.id, source_id: 'message-id-to-delete') allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + sender_id = message_events[:unsend][:entry][0][:messaging][0][:sender][:id] allow(fb_object).to receive(:get_object).and_return( { name: 'Jane', - id: 'Sender-id-1', + id: sender_id, account_id: instagram_messenger_inbox.account_id, profile_pic: 'https://chatwoot-assets.local/sample.png' }.with_indifferent_access @@ -104,8 +106,9 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message with attachments in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + sender_id = message_events[:attachment][:entry][0][:messaging][0][:sender][:id] allow(fb_object).to receive(:get_object).and_return( - return_object.with_indifferent_access + return_object_for(sender_id).with_indifferent_access ) instagram_webhook.perform_now(message_events[:attachment][:entry]) @@ -118,8 +121,9 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message with attachments in the instagram inbox for story mention' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + sender_id = message_events[:story_mention][:entry][0][:messaging][0][:sender][:id] allow(fb_object).to receive(:get_object).and_return( - return_object.with_indifferent_access, + return_object_for(sender_id).with_indifferent_access, { story: { mention: { @@ -165,8 +169,9 @@ describe Webhooks::InstagramEventsJob do it 'handles unsupported message' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + sender_id = message_events[:unsupported][:entry][0][:messaging][0][:sender][:id] allow(fb_object).to receive(:get_object).and_return( - return_object.with_indifferent_access + return_object_for(sender_id).with_indifferent_access ) instagram_webhook.perform_now(message_events[:unsupported][:entry]) @@ -184,19 +189,22 @@ describe Webhooks::InstagramEventsJob do before do instagram_channel.update(access_token: 'valid_instagram_token') - stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-1\?.*}) + stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/Sender-id-.*\?.*}) .to_return( status: 200, - body: { - name: 'Jane', - username: 'some_user_name', - profile_pic: 'https://chatwoot-assets.local/sample.png', - id: 'Sender-id-1', - follower_count: 100, - is_user_follow_business: true, - is_business_follow_user: true, - is_verified_user: false - }.to_json, + body: proc { |request| + sender_id = request.uri.path.split('/').last.split('?').first + { + name: 'Jane', + username: 'some_user_name', + profile_pic: 'https://chatwoot-assets.local/sample.png', + id: sender_id, + follower_count: 100, + is_user_follow_business: true, + is_business_follow_user: true, + is_verified_user: false + }.to_json + }, headers: { 'Content-Type' => 'application/json' } ) end @@ -289,14 +297,15 @@ describe Webhooks::InstagramEventsJob do stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*}) .to_return(status: 401, body: { error: { message: 'No matching Instagram user', code: 9010 } }.to_json) + sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id] instagram_webhook.perform_now(message_events[:dm][:entry]) instagram_inbox.reload expect(instagram_inbox.contacts.count).to be 1 - expect(instagram_inbox.contacts.last.name).to eq 'Unknown (IG: Sender-id-1)' + expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})" expect(instagram_inbox.contacts.last.contact_inboxes.count).to be 1 - expect(instagram_inbox.contacts.last.contact_inboxes.first.source_id).to eq 'Sender-id-1' + expect(instagram_inbox.contacts.last.contact_inboxes.first.source_id).to eq sender_id expect(instagram_inbox.conversations.count).to eq 1 expect(instagram_inbox.messages.count).to eq 1 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 616d62d28..bb8ea89e1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -70,6 +70,7 @@ RSpec.configure do |config| config.include SlackStubs config.include FileUploadHelpers config.include CsvSpecHelpers + config.include InstagramSpecHelpers config.include Devise::Test::IntegrationHelpers, type: :request config.include ActiveSupport::Testing::TimeHelpers config.include ActionCable::TestHelper diff --git a/spec/support/instagram_spec_helpers.rb b/spec/support/instagram_spec_helpers.rb new file mode 100644 index 000000000..651bff585 --- /dev/null +++ b/spec/support/instagram_spec_helpers.rb @@ -0,0 +1,20 @@ +module InstagramSpecHelpers + def create_instagram_contact_for_sender(sender_id, inbox) + contact = Contact.find_by(identifier: sender_id) + if contact.nil? + contact = create(:contact, identifier: sender_id, name: 'Jane Dae') + create(:contact_inbox, contact_id: contact.id, inbox_id: inbox.id, source_id: sender_id) + end + contact + end + + def instagram_user_response_object_for(sender_id, account_id) + { + name: 'Jane', + id: sender_id, + account_id: account_id, + profile_pic: 'https://chatwoot-assets.local/sample.png', + username: 'some_user_name' + } + end +end From 81d8d3862d4d114347071ef8db50e51955c11681 Mon Sep 17 00:00:00 2001 From: Petterson <58094725+hahuma@users.noreply.github.com> Date: Mon, 11 Aug 2025 16:23:05 -0300 Subject: [PATCH 2/6] feat: Add route to list accounts that belongs to a platform_app (#12140) This PR creates a new route to list all accounts that a platform_app has access to. Fixes: #12109 --- .../platform/api/v1/accounts_controller.rb | 7 ++++ .../api/v1/accounts/index.json.jbuilder | 3 ++ config/routes.rb | 2 +- .../api/v1/accounts_controller_spec.rb | 36 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 app/views/platform/api/v1/accounts/index.json.jbuilder diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index e11cf9d4a..4521930a6 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -1,4 +1,11 @@ class Platform::Api::V1::AccountsController < PlatformController + def index + @resources = @platform_app.platform_app_permissibles + .where(permissible_type: 'Account') + .includes(:permissible) + .map(&:permissible) + end + def show; end def create diff --git a/app/views/platform/api/v1/accounts/index.json.jbuilder b/app/views/platform/api/v1/accounts/index.json.jbuilder new file mode 100644 index 000000000..2e8e9f73f --- /dev/null +++ b/app/views/platform/api/v1/accounts/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array! @resources do |account| + json.partial! 'platform/api/v1/models/account', formats: [:json], resource: account +end diff --git a/config/routes.rb b/config/routes.rb index 7fb348084..10749062e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -425,7 +425,7 @@ Rails.application.routes.draw do resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do delete :avatar, on: :member end - resources :accounts, only: [:create, :show, :update, :destroy] do + resources :accounts, only: [:index, :create, :show, :update, :destroy] do resources :account_users, only: [:index, :create] do collection do delete :destroy diff --git a/spec/controllers/platform/api/v1/accounts_controller_spec.rb b/spec/controllers/platform/api/v1/accounts_controller_spec.rb index c7b5b275b..63f53d0d6 100644 --- a/spec/controllers/platform/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/platform/api/v1/accounts_controller_spec.rb @@ -78,6 +78,42 @@ RSpec.describe 'Platform Accounts API', type: :request do end end + describe 'GET /platform/api/v1/accounts' do + context 'when it is an unauthenticated platform app' do + it 'returns unauthorized' do + get '/platform/api/v1/accounts' + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an invalid platform app token' do + it 'returns unauthorized' do + get '/platform/api/v1/accounts', headers: { api_access_token: 'invalid' }, as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated platform app' do + let(:platform_app) { create(:platform_app) } + let!(:account1) { create(:account, name: 'Account A') } + let!(:account2) { create(:account, name: 'Account B') } + + before do + create(:platform_app_permissible, platform_app: platform_app, permissible: account1) + create(:platform_app_permissible, platform_app: platform_app, permissible: account2) + end + + it 'returns all permissible accounts' do + get '/platform/api/v1/accounts', headers: { api_access_token: platform_app.access_token.token }, as: :json + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response.size).to eq(2) + expect(json_response.map { |acc| acc['name'] }).to include('Account A', 'Account B') + end + end + end + describe 'GET /platform/api/v1/accounts/{account_id}' do context 'when it is an unauthenticated platform app' do it 'returns unauthorized' do From ecd9cf0326e240bb648ae9bb8e46c1101bc6d617 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 12 Aug 2025 02:46:48 +0530 Subject: [PATCH 3/6] fix: RTL issues in new conversation form (#12163) # Pull Request Template ## Description This PR fixes RTL alignment issues in the new conversation form, removes the unused [`form-checkbox`](https://github.com/chatwoot/chatwoot/pull/12151#discussion_r2266333315) class name and drops the `app-rtl--wrapper` class, which was previously used for RTL detection in `rtl.scss` (removed earlier) Fixes https://linear.app/chatwoot/issue/CW-5410/rtl-issues ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshots image image image ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- app/javascript/dashboard/App.vue | 1 - .../components/ActionButtons.vue | 2 +- .../components/AttachmentPreviews.vue | 2 +- .../components/InboxSelector.vue | 2 +- .../components/WhatsAppOptions.vue | 10 +++++++--- .../components/WhatsappTemplateParser.vue | 5 +++-- .../components-next/avatar/Avatar.vue | 2 +- .../assistant/EditAssistantForm.vue | 13 ++----------- .../settings/AssistantBasicSettingsForm.vue | 18 +++--------------- .../components-next/sidebar/SidebarGroup.vue | 4 ++-- .../components-next/taginput/TagInput.vue | 2 +- 11 files changed, 22 insertions(+), 39 deletions(-) diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 0fbb20ea9..8da7e7476 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -137,7 +137,6 @@ export default { v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem" id="app" class="flex flex-col w-full h-screen min-h-0" - :class="{ 'app-rtl--wrapper': isRTL }" :dir="isRTL ? 'rtl' : 'ltr'" > diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 90cb67c4f..a686a8d16 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -170,7 +170,7 @@ useKeyboardEvents(keyboardEvents); /> diff --git a/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue b/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue index fe57d19c4..a546fe0df 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/AttachmentPreviews.vue @@ -57,7 +57,7 @@ const removeAttachment = id => { variant="ghost" icon="i-lucide-trash" color="slate" - class="absolute top-1 right-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100" + class="absolute top-1 ltr:right-1 rtl:left-1 !w-5 !h-5 transition-opacity duration-150 ease-in-out opacity-0 group-hover/image:opacity-100" @click="removeAttachment(attachment.resource.id)" /> diff --git a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue index 7794d5bdd..3cafacb21 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/InboxSelector.vue @@ -83,7 +83,7 @@ const targetInboxLabel = computed(() => { diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue index 3980c6b30..124612ad3 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/WhatsAppOptions.vue @@ -2,6 +2,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import WhatsappTemplateParser from './WhatsappTemplateParser.vue'; @@ -84,10 +85,13 @@ const handleSendMessage = template => { />
- + { 'COMPOSE_NEW_CONVERSATION.FORM.WHATSAPP_OPTIONS.SEARCH_PLACEHOLDER' ) " - class="w-full h-8 py-2 pl-10 pr-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12" + class="w-full h-8 py-2 ltr:pl-10 rtl:pr-10 ltr:pr-2 rtl:pl-2 text-sm reset-base outline-none border-none rounded-lg bg-n-alpha-black2 dark:bg-n-solid-1 text-n-slate-12" />
{