diff --git a/app/builders/email/base_builder.rb b/app/builders/email/base_builder.rb new file mode 100644 index 000000000..731b1b0f5 --- /dev/null +++ b/app/builders/email/base_builder.rb @@ -0,0 +1,54 @@ +class Email::BaseBuilder + pattr_initialize [:inbox!] + + private + + def channel + @channel ||= inbox.channel + end + + def account + @account ||= inbox.account + end + + def conversation + @conversation ||= message.conversation + end + + def custom_sender_name + message&.sender&.available_name || I18n.t('conversations.reply.email.header.notifications') + end + + def sender_name(sender_email) + # Friendly: from + # Professional: + if inbox.friendly? + I18n.t( + 'conversations.reply.email.header.friendly_name', + sender_name: custom_sender_name, + business_name: business_name, + from_email: sender_email + ) + else + I18n.t( + 'conversations.reply.email.header.professional_name', + business_name: business_name, + from_email: sender_email + ) + end + end + + def business_name + inbox.business_name || inbox.sanitized_name + end + + def account_support_email + # Parse the email to ensure it's in the correct format, the user + # can save it in the format "Name " + parse_email(account.support_email) + end + + def parse_email(email_string) + Mail::Address.new(email_string).address + end +end diff --git a/app/builders/email/from_builder.rb b/app/builders/email/from_builder.rb new file mode 100644 index 000000000..fff33dc0a --- /dev/null +++ b/app/builders/email/from_builder.rb @@ -0,0 +1,51 @@ +class Email::FromBuilder < Email::BaseBuilder + pattr_initialize [:inbox!, :message!] + + def build + return sender_name(account_support_email) unless inbox.email? + + from_email = case email_channel_type + when :standard_imap_smtp, + :google_oauth, + :microsoft_oauth, + :forwarding_own_smtp + channel.email + when :imap_chatwoot_smtp, + :forwarding_chatwoot_smtp + channel.verified_for_sending ? channel.email : account_support_email + else + account_support_email + end + + sender_name(from_email) + end + + private + + def email_channel_type + return :google_oauth if channel.google? + return :microsoft_oauth if channel.microsoft? + return :standard_imap_smtp if imap_and_smtp_enabled? + return :imap_chatwoot_smtp if imap_enabled_without_smtp? + return :forwarding_own_smtp if forwarding_with_own_smtp? + return :forwarding_chatwoot_smtp if forwarding_without_smtp? + + :unknown + end + + def imap_and_smtp_enabled? + channel.imap_enabled && channel.smtp_enabled + end + + def imap_enabled_without_smtp? + channel.imap_enabled && !channel.smtp_enabled + end + + def forwarding_with_own_smtp? + !channel.imap_enabled && channel.smtp_enabled + end + + def forwarding_without_smtp? + !channel.imap_enabled && !channel.smtp_enabled + end +end diff --git a/app/builders/email/reply_to_builder.rb b/app/builders/email/reply_to_builder.rb new file mode 100644 index 000000000..d330c922a --- /dev/null +++ b/app/builders/email/reply_to_builder.rb @@ -0,0 +1,21 @@ +class Email::ReplyToBuilder < Email::BaseBuilder + pattr_initialize [:inbox!, :message!] + + def build + reply_to = if inbox.email? + channel.email + elsif inbound_email_enabled? + "reply+#{conversation.uuid}@#{account.inbound_email_domain}" + else + account_support_email + end + + sender_name(reply_to) + end + + private + + def inbound_email_enabled? + account.feature_enabled?('inbound_emails') && account.inbound_email_domain.present? + end +end diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index 039786905..e6270c807 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -17,8 +17,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController before_action :set_include_contact_inboxes, only: [:index, :active, :search, :filter, :show, :update] def index - @contacts_count = resolved_contacts.count @contacts = fetch_contacts(resolved_contacts) + @contacts_count = @contacts.total_count end def search @@ -29,8 +29,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController OR contacts.additional_attributes->>\'company_name\' ILIKE :search', search: "%#{params[:q].strip}%" ) - @contacts_count = contacts.count @contacts = fetch_contacts(contacts) + @contacts_count = @contacts.total_count end def import @@ -55,8 +55,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController def active contacts = Current.account.contacts.where(id: ::OnlineStatusTracker .get_available_contact_ids(Current.account.id)) - @contacts_count = contacts.count @contacts = fetch_contacts(contacts) + @contacts_count = @contacts.total_count end def show; end @@ -133,13 +133,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController end def fetch_contacts(contacts) - contacts_with_avatar = filtrate(contacts) - .includes([{ avatar_attachment: [:blob] }]) - .page(@current_page).per(RESULTS_PER_PAGE) + # Build includes hash to avoid separate query when contact_inboxes are needed + includes_hash = { avatar_attachment: [:blob] } + includes_hash[:contact_inboxes] = { inbox: :channel } if @include_contact_inboxes - return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes - - contacts_with_avatar + filtrate(contacts) + .includes(includes_hash) + .page(@current_page) + .per(RESULTS_PER_PAGE) end def build_contact_inbox diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue index 41732ac2b..4d6d41dac 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue @@ -148,10 +148,21 @@ const isAnyDropdownActive = computed(() => { const handleContactSearch = value => { showContactsDropdown.value = true; - emit('searchContacts', { - keys: ['email', 'phone_number', 'name'], - query: value, + const query = typeof value === 'string' ? value.trim() : ''; + const hasAlphabet = Array.from(query).some(char => { + const lower = char.toLowerCase(); + const upper = char.toUpperCase(); + return lower !== upper; }); + const isEmailLike = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(query); + + const keys = ['email', 'phone_number', 'name'].filter(key => { + if (key === 'phone_number' && hasAlphabet) return false; + if (key === 'name' && isEmailLike) return false; + return true; + }); + + emit('searchContacts', { keys, query: value }); }; const handleDropdownUpdate = (type, value) => { diff --git a/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue index ec21cec9f..ccb36da01 100644 --- a/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue +++ b/app/javascript/dashboard/components-next/copilot/ToggleCopilotAssistant.vue @@ -45,7 +45,7 @@ const activeAssistantLabel = computed(() => { /> - + { :class="dropdownPosition" strong > - + { - + { :placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')" /> - +