mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	Merge branch 'develop' into assignment_v2/assignment_service
This commit is contained in:
		
							
								
								
									
										54
									
								
								app/builders/email/base_builder.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/builders/email/base_builder.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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: <agent_name> from <business_name> | ||||
|     # Professional: <business_name> | ||||
|     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 <email@domain.com>" | ||||
|     parse_email(account.support_email) | ||||
|   end | ||||
|  | ||||
|   def parse_email(email_string) | ||||
|     Mail::Address.new(email_string).address | ||||
|   end | ||||
| end | ||||
							
								
								
									
										51
									
								
								app/builders/email/from_builder.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/builders/email/from_builder.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										21
									
								
								app/builders/email/reply_to_builder.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/builders/email/reply_to_builder.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -7,6 +7,7 @@ class Messages::MessageBuilder | ||||
|     @private = params[:private] || false | ||||
|     @conversation = conversation | ||||
|     @user = user | ||||
|     @account = conversation.account | ||||
|     @message_type = params[:message_type] || 'outgoing' | ||||
|     @attachments = params[:attachments] | ||||
|     @automation_rule = content_attributes&.dig(:automation_rule_id) | ||||
| @@ -20,6 +21,9 @@ class Messages::MessageBuilder | ||||
|     @message = @conversation.messages.build(message_params) | ||||
|     process_attachments | ||||
|     process_emails | ||||
|     # When the message has no quoted content, it will just be rendered as a regular message | ||||
|     # The frontend is equipped to handle this case | ||||
|     process_email_content if @account.feature_enabled?(:quoted_email_reply) | ||||
|     @message.save! | ||||
|     @message | ||||
|   end | ||||
| @@ -92,6 +96,14 @@ class Messages::MessageBuilder | ||||
|     @message.content_attributes[:to_emails] = to_emails | ||||
|   end | ||||
|  | ||||
|   def process_email_content | ||||
|     return unless should_process_email_content? | ||||
|  | ||||
|     @message.content_attributes ||= {} | ||||
|     email_attributes = build_email_attributes | ||||
|     @message.content_attributes[:email] = email_attributes | ||||
|   end | ||||
|  | ||||
|   def process_email_string(email_string) | ||||
|     return [] if email_string.blank? | ||||
|  | ||||
| @@ -153,4 +165,52 @@ class Messages::MessageBuilder | ||||
|       source_id: @params[:source_id] | ||||
|     }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params) | ||||
|   end | ||||
|  | ||||
|   def email_inbox? | ||||
|     @conversation.inbox&.inbox_type == 'Email' | ||||
|   end | ||||
|  | ||||
|   def should_process_email_content? | ||||
|     email_inbox? && !@private && @message.content.present? | ||||
|   end | ||||
|  | ||||
|   def build_email_attributes | ||||
|     email_attributes = ensure_indifferent_access(@message.content_attributes[:email] || {}) | ||||
|     normalized_content = normalize_email_body(@message.content) | ||||
|  | ||||
|     email_attributes[:html_content] = build_html_content(normalized_content) | ||||
|     email_attributes[:text_content] = build_text_content(normalized_content) | ||||
|     email_attributes | ||||
|   end | ||||
|  | ||||
|   def build_html_content(normalized_content) | ||||
|     html_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :html_content) || {}) | ||||
|     rendered_html = render_email_html(normalized_content) | ||||
|     html_content[:full] = rendered_html | ||||
|     html_content[:reply] = rendered_html | ||||
|     html_content | ||||
|   end | ||||
|  | ||||
|   def build_text_content(normalized_content) | ||||
|     text_content = ensure_indifferent_access(@message.content_attributes.dig(:email, :text_content) || {}) | ||||
|     text_content[:full] = normalized_content | ||||
|     text_content[:reply] = normalized_content | ||||
|     text_content | ||||
|   end | ||||
|  | ||||
|   def ensure_indifferent_access(hash) | ||||
|     return {} if hash.blank? | ||||
|  | ||||
|     hash.respond_to?(:with_indifferent_access) ? hash.with_indifferent_access : hash | ||||
|   end | ||||
|  | ||||
|   def normalize_email_body(content) | ||||
|     content.to_s.gsub("\r\n", "\n") | ||||
|   end | ||||
|  | ||||
|   def render_email_html(content) | ||||
|     return '' if content.blank? | ||||
|  | ||||
|     ChatwootMarkdownRenderer.new(content).render_message.to_s | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -4,7 +4,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController | ||||
|   before_action :fetch_agent_bot, only: [:set_agent_bot] | ||||
|   before_action :validate_limit, only: [:create] | ||||
|   # we are already handling the authorization in fetch inbox | ||||
|   before_action :check_authorization, except: [:show] | ||||
|   before_action :check_authorization, except: [:show, :health] | ||||
|   before_action :validate_whatsapp_cloud_channel, only: [:health] | ||||
|  | ||||
|   def index | ||||
|     @inboxes = policy_scope(Current.account.inboxes.order_by_name.includes(:channel, { avatar_attachment: [:blob] })) | ||||
| @@ -78,6 +79,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController | ||||
|     render status: :internal_server_error, json: { error: e.message } | ||||
|   end | ||||
|  | ||||
|   def health | ||||
|     health_data = Whatsapp::HealthService.new(@inbox.channel).fetch_health_status | ||||
|     render json: health_data | ||||
|   rescue StandardError => e | ||||
|     Rails.logger.error "[INBOX HEALTH] Error fetching health data: #{e.message}" | ||||
|     render json: { error: e.message }, status: :unprocessable_entity | ||||
|   end | ||||
|  | ||||
|   private | ||||
|  | ||||
|   def fetch_inbox | ||||
| @@ -89,6 +98,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController | ||||
|     @agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot] | ||||
|   end | ||||
|  | ||||
|   def validate_whatsapp_cloud_channel | ||||
|     return if @inbox.channel.is_a?(Channel::Whatsapp) && @inbox.channel.provider == 'whatsapp_cloud' | ||||
|  | ||||
|     render json: { error: 'Health data only available for WhatsApp Cloud API channels' }, status: :bad_request | ||||
|   end | ||||
|  | ||||
|   def create_channel | ||||
|     return unless allowed_channel_types.include?(permitted_params[:channel][:type]) | ||||
|  | ||||
|   | ||||
							
								
								
									
										14
									
								
								app/javascript/dashboard/api/inboxHealth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/javascript/dashboard/api/inboxHealth.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| /* global axios */ | ||||
| import ApiClient from './ApiClient'; | ||||
|  | ||||
| class InboxHealthAPI extends ApiClient { | ||||
|   constructor() { | ||||
|     super('inboxes', { accountScoped: true }); | ||||
|   } | ||||
|  | ||||
|   getHealthStatus(inboxId) { | ||||
|     return axios.get(`${this.url}/${inboxId}/health`); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default new InboxHealthAPI(); | ||||
| @@ -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) => { | ||||
|   | ||||
| @@ -45,7 +45,7 @@ const activeAssistantLabel = computed(() => { | ||||
|         /> | ||||
|       </template> | ||||
|       <DropdownBody class="bottom-9 min-w-64 z-50" strong> | ||||
|         <DropdownSection class="max-h-80 overflow-scroll"> | ||||
|         <DropdownSection class="[&>ul]:max-h-80"> | ||||
|           <DropdownItem | ||||
|             v-for="assistant in assistants" | ||||
|             :key="assistant.id" | ||||
|   | ||||
| @@ -91,7 +91,7 @@ const updateSelected = newValue => { | ||||
|       :class="dropdownPosition" | ||||
|       strong | ||||
|     > | ||||
|       <DropdownSection class="max-h-80 overflow-scroll"> | ||||
|       <DropdownSection class="[&>ul]:max-h-80"> | ||||
|         <DropdownItem | ||||
|           v-for="option in options" | ||||
|           :key="option.value" | ||||
|   | ||||
| @@ -123,7 +123,7 @@ const toggleOption = option => { | ||||
|       </Button> | ||||
|     </template> | ||||
|     <DropdownBody class="top-0 min-w-48 z-50" strong> | ||||
|       <DropdownSection class="max-h-80 overflow-scroll"> | ||||
|       <DropdownSection class="[&>ul]:max-h-80"> | ||||
|         <DropdownItem | ||||
|           v-for="option in options" | ||||
|           :key="option.id" | ||||
|   | ||||
| @@ -124,7 +124,7 @@ const toggleSelected = option => { | ||||
|           :placeholder="searchPlaceholder || t('COMBOBOX.SEARCH_PLACEHOLDER')" | ||||
|         /> | ||||
|       </div> | ||||
|       <DropdownSection class="max-h-80 overflow-scroll"> | ||||
|       <DropdownSection class="[&>ul]:max-h-80"> | ||||
|         <template v-if="searchResults.length"> | ||||
|           <DropdownItem | ||||
|             v-for="option in searchResults" | ||||
|   | ||||
| @@ -5,9 +5,9 @@ import { sanitizeTextForRender } from '@chatwoot/utils'; | ||||
| import { allowedCssProperties } from 'lettersanitizer'; | ||||
|  | ||||
| import Icon from 'next/icon/Icon.vue'; | ||||
| import { EmailQuoteExtractor } from './removeReply.js'; | ||||
| import BaseBubble from 'next/message/bubbles/Base.vue'; | ||||
| import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js'; | ||||
| import FormattedContent from 'next/message/bubbles/Text/FormattedContent.vue'; | ||||
| import BaseBubble from 'next/message/bubbles/Base.vue'; | ||||
| import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; | ||||
| import EmailMeta from './EmailMeta.vue'; | ||||
| import TranslationToggle from 'dashboard/components-next/message/TranslationToggle.vue'; | ||||
| @@ -47,6 +47,13 @@ const originalEmailHtml = computed( | ||||
|     originalEmailText.value | ||||
| ); | ||||
|  | ||||
| const hasEmailContent = computed(() => { | ||||
|   return ( | ||||
|     contentAttributes?.value?.email?.textContent?.full || | ||||
|     contentAttributes?.value?.email?.htmlContent?.full | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| const messageContent = computed(() => { | ||||
|   // If translations exist and we're showing translations (not original) | ||||
|   if (hasTranslations.value && !renderOriginal.value) { | ||||
| @@ -137,7 +144,7 @@ const handleSeeOriginal = () => { | ||||
|           </button> | ||||
|         </div> | ||||
|         <FormattedContent | ||||
|           v-if="isOutgoing && content" | ||||
|           v-if="isOutgoing && content && !hasEmailContent" | ||||
|           class="text-n-slate-12" | ||||
|           :content="messageContent" | ||||
|         /> | ||||
|   | ||||
| @@ -76,7 +76,7 @@ function changeAvailabilityStatus(availability) { | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <DropdownSection> | ||||
|   <DropdownSection class="[&>ul]:overflow-visible"> | ||||
|     <div class="grid gap-0"> | ||||
|       <DropdownItem preserve-open> | ||||
|         <div class="flex-grow flex items-center gap-1"> | ||||
|   | ||||
| @@ -118,6 +118,14 @@ export default { | ||||
|       type: String, | ||||
|       default: '', | ||||
|     }, | ||||
|     showQuotedReplyToggle: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     quotedReplyEnabled: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   emits: [ | ||||
|     'replaceText', | ||||
| @@ -125,6 +133,7 @@ export default { | ||||
|     'toggleEditor', | ||||
|     'selectWhatsappTemplate', | ||||
|     'selectContentTemplate', | ||||
|     'toggleQuotedReply', | ||||
|   ], | ||||
|   setup() { | ||||
|     const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } = | ||||
| @@ -249,6 +258,11 @@ export default { | ||||
|     isFetchingAppIntegrations() { | ||||
|       return this.uiFlags.isFetching; | ||||
|     }, | ||||
|     quotedReplyToggleTooltip() { | ||||
|       return this.quotedReplyEnabled | ||||
|         ? this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.DISABLE_TOOLTIP') | ||||
|         : this.$t('CONVERSATION.REPLYBOX.QUOTED_REPLY.ENABLE_TOOLTIP'); | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     ActiveStorage.start(); | ||||
| @@ -339,6 +353,16 @@ export default { | ||||
|         sm | ||||
|         @click="toggleMessageSignature" | ||||
|       /> | ||||
|       <NextButton | ||||
|         v-if="showQuotedReplyToggle" | ||||
|         v-tooltip.top-end="quotedReplyToggleTooltip" | ||||
|         icon="i-ph-quotes" | ||||
|         :variant="quotedReplyEnabled ? 'solid' : 'faded'" | ||||
|         color="slate" | ||||
|         sm | ||||
|         :aria-pressed="quotedReplyEnabled" | ||||
|         @click="$emit('toggleQuotedReply')" | ||||
|       /> | ||||
|       <NextButton | ||||
|         v-if="enableWhatsAppTemplates" | ||||
|         v-tooltip.top-end="$t('CONVERSATION.FOOTER.WHATSAPP_TEMPLATES')" | ||||
|   | ||||
| @@ -0,0 +1,76 @@ | ||||
| <script setup> | ||||
| import { computed, ref } from 'vue'; | ||||
| import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import NextButton from 'dashboard/components-next/button/Button.vue'; | ||||
|  | ||||
| const props = defineProps({ | ||||
|   quotedEmailText: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
|   previewText: { | ||||
|     type: String, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const emit = defineEmits(['toggle']); | ||||
|  | ||||
| const { t } = useI18n(); | ||||
| const { formatMessage } = useMessageFormatter(); | ||||
|  | ||||
| const isExpanded = ref(false); | ||||
|  | ||||
| const formattedQuotedEmailText = computed(() => { | ||||
|   if (!props.quotedEmailText) { | ||||
|     return ''; | ||||
|   } | ||||
|   return formatMessage(props.quotedEmailText, false, false, true); | ||||
| }); | ||||
|  | ||||
| const toggleExpand = () => { | ||||
|   isExpanded.value = !isExpanded.value; | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="mt-2"> | ||||
|     <div | ||||
|       class="relative rounded-md px-3 py-2 text-xs text-n-slate-12 bg-n-slate-3 dark:bg-n-solid-3" | ||||
|     > | ||||
|       <div class="absolute top-2 right-2 z-10 flex items-center gap-1"> | ||||
|         <NextButton | ||||
|           v-tooltip=" | ||||
|             isExpanded | ||||
|               ? t('CONVERSATION.REPLYBOX.QUOTED_REPLY.COLLAPSE') | ||||
|               : t('CONVERSATION.REPLYBOX.QUOTED_REPLY.EXPAND') | ||||
|           " | ||||
|           ghost | ||||
|           slate | ||||
|           xs | ||||
|           :icon="isExpanded ? 'i-lucide-minimize' : 'i-lucide-maximize'" | ||||
|           @click="toggleExpand" | ||||
|         /> | ||||
|         <NextButton | ||||
|           v-tooltip="t('CONVERSATION.REPLYBOX.QUOTED_REPLY.REMOVE_PREVIEW')" | ||||
|           ghost | ||||
|           slate | ||||
|           xs | ||||
|           icon="i-lucide-x" | ||||
|           @click="emit('toggle')" | ||||
|         /> | ||||
|       </div> | ||||
|       <div | ||||
|         v-dompurify-html="formattedQuotedEmailText" | ||||
|         class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer ltr:pr-8 rtl:pl-8" | ||||
|         :class="{ | ||||
|           'line-clamp-1': !isExpanded, | ||||
|           'max-h-60 overflow-y-auto': isExpanded, | ||||
|         }" | ||||
|         :title="previewText" | ||||
|         @click="toggleExpand" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -5,6 +5,7 @@ import { useAlert } from 'dashboard/composables'; | ||||
| import { useUISettings } from 'dashboard/composables/useUISettings'; | ||||
| import { useTrack } from 'dashboard/composables'; | ||||
| import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; | ||||
| import { FEATURE_FLAGS } from 'dashboard/featureFlags'; | ||||
|  | ||||
| import CannedResponse from './CannedResponse.vue'; | ||||
| import ReplyToMessage from './ReplyToMessage.vue'; | ||||
| @@ -16,6 +17,7 @@ import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBotto | ||||
| import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue'; | ||||
| import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue'; | ||||
| import ReplyBoxBanner from './ReplyBoxBanner.vue'; | ||||
| import QuotedEmailPreview from './QuotedEmailPreview.vue'; | ||||
| import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; | ||||
| import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; | ||||
| import AudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue'; | ||||
| @@ -32,6 +34,12 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper'; | ||||
| import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin'; | ||||
| import { trimContent, debounce, getRecipients } from '@chatwoot/utils'; | ||||
| import wootConstants from 'dashboard/constants/globals'; | ||||
| import { | ||||
|   extractQuotedEmailText, | ||||
|   buildQuotedEmailHeader, | ||||
|   truncatePreviewText, | ||||
|   appendQuotedTextToMessage, | ||||
| } from 'dashboard/helper/quotedEmailHelper'; | ||||
| import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; | ||||
| import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; | ||||
| import { | ||||
| @@ -65,6 +73,7 @@ export default { | ||||
|     ContentTemplates, | ||||
|     WhatsappTemplates, | ||||
|     WootMessageEditor, | ||||
|     QuotedEmailPreview, | ||||
|   }, | ||||
|   mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins], | ||||
|   props: { | ||||
| @@ -80,6 +89,8 @@ export default { | ||||
|       updateUISettings, | ||||
|       isEditorHotKeyEnabled, | ||||
|       fetchSignatureFlagFromUISettings, | ||||
|       setQuotedReplyFlagForInbox, | ||||
|       fetchQuotedReplyFlagFromUISettings, | ||||
|     } = useUISettings(); | ||||
|  | ||||
|     const replyEditor = useTemplateRef('replyEditor'); | ||||
| @@ -89,6 +100,8 @@ export default { | ||||
|       updateUISettings, | ||||
|       isEditorHotKeyEnabled, | ||||
|       fetchSignatureFlagFromUISettings, | ||||
|       setQuotedReplyFlagForInbox, | ||||
|       fetchQuotedReplyFlagFromUISettings, | ||||
|       replyEditor, | ||||
|     }; | ||||
|   }, | ||||
| @@ -130,6 +143,8 @@ export default { | ||||
|       currentUser: 'getCurrentUser', | ||||
|       lastEmail: 'getLastEmailInSelectedChat', | ||||
|       globalConfig: 'globalConfig/get', | ||||
|       accountId: 'getCurrentAccountId', | ||||
|       isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', | ||||
|     }), | ||||
|     currentContact() { | ||||
|       return this.$store.getters['contacts/getContact']( | ||||
| @@ -367,6 +382,51 @@ export default { | ||||
|       const { slug = '' } = portal; | ||||
|       return slug; | ||||
|     }, | ||||
|     isQuotedEmailReplyEnabled() { | ||||
|       return this.isFeatureEnabledonAccount( | ||||
|         this.accountId, | ||||
|         FEATURE_FLAGS.QUOTED_EMAIL_REPLY | ||||
|       ); | ||||
|     }, | ||||
|     quotedReplyPreference() { | ||||
|       if (!this.isAnEmailChannel || !this.isQuotedEmailReplyEnabled) { | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       return !!this.fetchQuotedReplyFlagFromUISettings(this.channelType); | ||||
|     }, | ||||
|     lastEmailWithQuotedContent() { | ||||
|       if (!this.isAnEmailChannel) { | ||||
|         return null; | ||||
|       } | ||||
|  | ||||
|       const lastEmail = this.lastEmail; | ||||
|       if (!lastEmail || lastEmail.private) { | ||||
|         return null; | ||||
|       } | ||||
|  | ||||
|       return lastEmail; | ||||
|     }, | ||||
|     quotedEmailText() { | ||||
|       return extractQuotedEmailText(this.lastEmailWithQuotedContent); | ||||
|     }, | ||||
|     quotedEmailPreviewText() { | ||||
|       return truncatePreviewText(this.quotedEmailText, 80); | ||||
|     }, | ||||
|     shouldShowQuotedReplyToggle() { | ||||
|       return ( | ||||
|         this.isAnEmailChannel && | ||||
|         !this.isOnPrivateNote && | ||||
|         this.isQuotedEmailReplyEnabled | ||||
|       ); | ||||
|     }, | ||||
|     shouldShowQuotedPreview() { | ||||
|       return ( | ||||
|         this.shouldShowQuotedReplyToggle && | ||||
|         this.quotedReplyPreference && | ||||
|         !!this.quotedEmailText | ||||
|       ); | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     currentChat(conversation, oldConversation) { | ||||
| @@ -516,6 +576,36 @@ export default { | ||||
|         ); | ||||
|       } | ||||
|     }, | ||||
|     toggleQuotedReply() { | ||||
|       if (!this.isAnEmailChannel) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const nextValue = !this.quotedReplyPreference; | ||||
|       this.setQuotedReplyFlagForInbox(this.channelType, nextValue); | ||||
|     }, | ||||
|     shouldIncludeQuotedEmail() { | ||||
|       return ( | ||||
|         this.isQuotedEmailReplyEnabled && | ||||
|         this.quotedReplyPreference && | ||||
|         this.shouldShowQuotedReplyToggle && | ||||
|         !!this.quotedEmailText | ||||
|       ); | ||||
|     }, | ||||
|     getMessageWithQuotedEmailText(message) { | ||||
|       if (!this.shouldIncludeQuotedEmail()) { | ||||
|         return message; | ||||
|       } | ||||
|  | ||||
|       const quotedText = this.quotedEmailText || ''; | ||||
|       const header = buildQuotedEmailHeader( | ||||
|         this.lastEmailWithQuotedContent, | ||||
|         this.currentContact, | ||||
|         this.inbox | ||||
|       ); | ||||
|  | ||||
|       return appendQuotedTextToMessage(message, quotedText, header); | ||||
|     }, | ||||
|     resetRecorderAndClearAttachments() { | ||||
|       // Reset audio recorder UI state | ||||
|       this.resetAudioRecorderInput(); | ||||
| @@ -965,9 +1055,11 @@ export default { | ||||
|       return multipleMessagePayload; | ||||
|     }, | ||||
|     getMessagePayload(message) { | ||||
|       const messageWithQuote = this.getMessageWithQuotedEmailText(message); | ||||
|  | ||||
|       let messagePayload = { | ||||
|         conversationId: this.currentChat.id, | ||||
|         message, | ||||
|         message: messageWithQuote, | ||||
|         private: this.isPrivate, | ||||
|         sender: this.sender, | ||||
|       }; | ||||
| @@ -995,7 +1087,6 @@ export default { | ||||
|       if (this.toEmails && !this.isOnPrivateNote) { | ||||
|         messagePayload.toEmails = this.toEmails; | ||||
|       } | ||||
|  | ||||
|       return messagePayload; | ||||
|     }, | ||||
|     setCcEmails(value) { | ||||
| @@ -1160,6 +1251,12 @@ export default { | ||||
|         @toggle-variables-menu="toggleVariablesMenu" | ||||
|         @clear-selection="clearEditorSelection" | ||||
|       /> | ||||
|       <QuotedEmailPreview | ||||
|         v-if="shouldShowQuotedPreview" | ||||
|         :quoted-email-text="quotedEmailText" | ||||
|         :preview-text="quotedEmailPreviewText" | ||||
|         @toggle="toggleQuotedReply" | ||||
|       /> | ||||
|     </div> | ||||
|     <div | ||||
|       v-if="hasAttachments && !showAudioRecorderEditor" | ||||
| @@ -1195,6 +1292,8 @@ export default { | ||||
|       :show-editor-toggle="isAPIInbox && !isOnPrivateNote" | ||||
|       :show-emoji-picker="showEmojiPicker" | ||||
|       :show-file-upload="showFileUpload" | ||||
|       :show-quoted-reply-toggle="shouldShowQuotedReplyToggle" | ||||
|       :quoted-reply-enabled="quotedReplyPreference" | ||||
|       :toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause" | ||||
|       :toggle-audio-recorder="toggleAudioRecorder" | ||||
|       :toggle-emoji-picker="toggleEmojiPicker" | ||||
| @@ -1206,6 +1305,7 @@ export default { | ||||
|       @toggle-editor="toggleRichContentEditor" | ||||
|       @replace-text="replaceText" | ||||
|       @toggle-insert-article="toggleInsertArticle" | ||||
|       @toggle-quoted-reply="toggleQuotedReply" | ||||
|     /> | ||||
|     <WhatsappTemplates | ||||
|       :inbox-id="inbox.id" | ||||
|   | ||||
| @@ -13,6 +13,7 @@ const getUISettingsMock = ref({ | ||||
|   conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER, | ||||
|   contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER, | ||||
|   editor_message_key: 'enter', | ||||
|   channel_email_quoted_reply_enabled: true, | ||||
| }); | ||||
|  | ||||
| vi.mock('dashboard/composables/store', () => ({ | ||||
| @@ -37,6 +38,7 @@ describe('useUISettings', () => { | ||||
|         DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER, | ||||
|       contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER, | ||||
|       editor_message_key: 'enter', | ||||
|       channel_email_quoted_reply_enabled: true, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @@ -51,6 +53,7 @@ describe('useUISettings', () => { | ||||
|           DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER, | ||||
|         contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER, | ||||
|         editor_message_key: 'enter', | ||||
|         channel_email_quoted_reply_enabled: true, | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| @@ -65,6 +68,7 @@ describe('useUISettings', () => { | ||||
|           DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER, | ||||
|         contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER, | ||||
|         editor_message_key: 'enter', | ||||
|         channel_email_quoted_reply_enabled: true, | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| @@ -100,6 +104,7 @@ describe('useUISettings', () => { | ||||
|         contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER, | ||||
|         email_signature_enabled: true, | ||||
|         editor_message_key: 'enter', | ||||
|         channel_email_quoted_reply_enabled: true, | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
| @@ -109,6 +114,26 @@ describe('useUISettings', () => { | ||||
|     expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined); | ||||
|   }); | ||||
|  | ||||
|   it('sets quoted reply flag for inbox correctly', () => { | ||||
|     const { setQuotedReplyFlagForInbox } = useUISettings(); | ||||
|     setQuotedReplyFlagForInbox('Channel::Email', false); | ||||
|     expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', { | ||||
|       uiSettings: { | ||||
|         is_ct_labels_open: true, | ||||
|         conversation_sidebar_items_order: | ||||
|           DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER, | ||||
|         contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER, | ||||
|         editor_message_key: 'enter', | ||||
|         channel_email_quoted_reply_enabled: false, | ||||
|       }, | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   it('fetches quoted reply flag from UI settings correctly', () => { | ||||
|     const { fetchQuotedReplyFlagFromUISettings } = useUISettings(); | ||||
|     expect(fetchQuotedReplyFlagFromUISettings('Channel::Email')).toBe(true); | ||||
|   }); | ||||
|  | ||||
|   it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => { | ||||
|     getUISettingsMock.value.enter_to_send_enabled = false; | ||||
|     const { isEditorHotKeyEnabled } = useUISettings(); | ||||
|   | ||||
| @@ -87,6 +87,13 @@ const setSignatureFlagForInbox = (channelType, value, updateUISettings) => { | ||||
|   updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value }); | ||||
| }; | ||||
|  | ||||
| const setQuotedReplyFlagForInbox = (channelType, value, updateUISettings) => { | ||||
|   if (!channelType) return; | ||||
|  | ||||
|   const slugifiedChannel = slugifyChannel(channelType); | ||||
|   updateUISettings({ [`${slugifiedChannel}_quoted_reply_enabled`]: value }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Fetches the signature flag for a specific channel type from UI settings. | ||||
|  * @param {string} channelType - The type of the channel. | ||||
| @@ -100,6 +107,13 @@ const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => { | ||||
|   return uiSettings.value[`${slugifiedChannel}_signature_enabled`]; | ||||
| }; | ||||
|  | ||||
| const fetchQuotedReplyFlagFromUISettings = (channelType, uiSettings) => { | ||||
|   if (!channelType) return false; | ||||
|  | ||||
|   const slugifiedChannel = slugifyChannel(channelType); | ||||
|   return uiSettings.value[`${slugifiedChannel}_quoted_reply_enabled`]; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Checks if a specific editor hotkey is enabled. | ||||
|  * @param {string} key - The key to check. | ||||
| @@ -147,6 +161,10 @@ export function useUISettings() { | ||||
|       setSignatureFlagForInbox(channelType, value, updateUISettings), | ||||
|     fetchSignatureFlagFromUISettings: channelType => | ||||
|       fetchSignatureFlagFromUISettings(channelType, uiSettings), | ||||
|     setQuotedReplyFlagForInbox: (channelType, value) => | ||||
|       setQuotedReplyFlagForInbox(channelType, value, updateUISettings), | ||||
|     fetchQuotedReplyFlagFromUISettings: channelType => | ||||
|       fetchQuotedReplyFlagFromUISettings(channelType, uiSettings), | ||||
|     isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings), | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,7 @@ export const FEATURE_FLAGS = { | ||||
|   CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team', | ||||
|   CAPTAIN_V2: 'captain_integration_v2', | ||||
|   SAML: 'saml', | ||||
|   QUOTED_EMAIL_REPLY: 'quoted_email_reply', | ||||
| }; | ||||
|  | ||||
| export const PREMIUM_FEATURES = [ | ||||
|   | ||||
| @@ -10,6 +10,8 @@ const QUOTE_INDICATORS = [ | ||||
|   '[class*="Quote"]', | ||||
| ]; | ||||
| 
 | ||||
| const BLOCKQUOTE_FALLBACK_SELECTOR = 'blockquote'; | ||||
| 
 | ||||
| // Regex patterns for quote identification
 | ||||
| const QUOTE_PATTERNS = [ | ||||
|   /On .* wrote:/i, | ||||
| @@ -36,6 +38,8 @@ export class EmailQuoteExtractor { | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     this.removeTrailingBlockquote(tempDiv); | ||||
| 
 | ||||
|     // Remove text-based quotes
 | ||||
|     const textNodeQuotes = this.findTextNodeQuotes(tempDiv); | ||||
|     textNodeQuotes.forEach(el => { | ||||
| @@ -62,6 +66,10 @@ export class EmailQuoteExtractor { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (this.findTrailingBlockquote(tempDiv)) { | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     // Check for text-based quotes
 | ||||
|     const textNodeQuotes = this.findTextNodeQuotes(tempDiv); | ||||
|     return textNodeQuotes.length > 0; | ||||
| @@ -123,4 +131,26 @@ export class EmailQuoteExtractor { | ||||
| 
 | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Remove fallback blockquote if it is the last top-level element. | ||||
|    * @param {Element} rootElement - Root element containing the HTML | ||||
|    */ | ||||
|   static removeTrailingBlockquote(rootElement) { | ||||
|     const trailingBlockquote = this.findTrailingBlockquote(rootElement); | ||||
|     trailingBlockquote?.remove(); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Locate a fallback blockquote that is the last top-level element. | ||||
|    * @param {Element} rootElement - Root element containing the HTML | ||||
|    * @returns {Element|null} The trailing blockquote element if present | ||||
|    */ | ||||
|   static findTrailingBlockquote(rootElement) { | ||||
|     const lastElement = rootElement.lastElementChild; | ||||
|     if (lastElement?.matches?.(BLOCKQUOTE_FALLBACK_SELECTOR)) { | ||||
|       return lastElement; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
| @@ -68,13 +68,17 @@ export const registerSubscription = (onSuccess = () => {}) => { | ||||
|     .then(() => { | ||||
|       onSuccess(); | ||||
|     }) | ||||
|     .catch(() => { | ||||
|     .catch(error => { | ||||
|       // eslint-disable-next-line no-console | ||||
|       console.error('Push subscription registration failed:', error); | ||||
|       useAlert('This browser does not support desktop notification'); | ||||
|     }); | ||||
| }; | ||||
|  | ||||
| export const requestPushPermissions = ({ onSuccess }) => { | ||||
|   if (!('Notification' in window)) { | ||||
|     // eslint-disable-next-line no-console | ||||
|     console.warn('Notification is not supported'); | ||||
|     useAlert('This browser does not support desktop notification'); | ||||
|   } else if (Notification.permission === 'granted') { | ||||
|     registerSubscription(onSuccess); | ||||
|   | ||||
							
								
								
									
										332
									
								
								app/javascript/dashboard/helper/quotedEmailHelper.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										332
									
								
								app/javascript/dashboard/helper/quotedEmailHelper.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,332 @@ | ||||
| import { format, parseISO, isValid as isValidDate } from 'date-fns'; | ||||
|  | ||||
| /** | ||||
|  * Extracts plain text from HTML content | ||||
|  * @param {string} html - HTML content to convert | ||||
|  * @returns {string} Plain text content | ||||
|  */ | ||||
| export const extractPlainTextFromHtml = html => { | ||||
|   if (!html) { | ||||
|     return ''; | ||||
|   } | ||||
|   if (typeof document === 'undefined') { | ||||
|     return html.replace(/<[^>]*>/g, ' '); | ||||
|   } | ||||
|   const tempDiv = document.createElement('div'); | ||||
|   tempDiv.innerHTML = html; | ||||
|   return tempDiv.textContent || tempDiv.innerText || ''; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Extracts sender name from email message | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @param {Object} contact - Contact object | ||||
|  * @returns {string} Sender name | ||||
|  */ | ||||
| export const getEmailSenderName = (lastEmail, contact) => { | ||||
|   const senderName = lastEmail?.sender?.name; | ||||
|   if (senderName && senderName.trim()) { | ||||
|     return senderName.trim(); | ||||
|   } | ||||
|  | ||||
|   const contactName = contact?.name; | ||||
|   return contactName && contactName.trim() ? contactName.trim() : ''; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Extracts sender email from email message | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @param {Object} contact - Contact object | ||||
|  * @returns {string} Sender email address | ||||
|  */ | ||||
| export const getEmailSenderEmail = (lastEmail, contact) => { | ||||
|   const senderEmail = lastEmail?.sender?.email; | ||||
|   if (senderEmail && senderEmail.trim()) { | ||||
|     return senderEmail.trim(); | ||||
|   } | ||||
|  | ||||
|   const contentAttributes = | ||||
|     lastEmail?.contentAttributes || lastEmail?.content_attributes || {}; | ||||
|   const emailMeta = contentAttributes.email || {}; | ||||
|  | ||||
|   if (Array.isArray(emailMeta.from) && emailMeta.from.length > 0) { | ||||
|     const fromAddress = emailMeta.from[0]; | ||||
|     if (fromAddress && fromAddress.trim()) { | ||||
|       return fromAddress.trim(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const contactEmail = contact?.email; | ||||
|   return contactEmail && contactEmail.trim() ? contactEmail.trim() : ''; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Extracts date from email message | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @returns {Date|null} Email date | ||||
|  */ | ||||
| export const getEmailDate = lastEmail => { | ||||
|   const contentAttributes = | ||||
|     lastEmail?.contentAttributes || lastEmail?.content_attributes || {}; | ||||
|   const emailMeta = contentAttributes.email || {}; | ||||
|  | ||||
|   if (emailMeta.date) { | ||||
|     const parsedDate = parseISO(emailMeta.date); | ||||
|     if (isValidDate(parsedDate)) { | ||||
|       return parsedDate; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const createdAt = lastEmail?.created_at; | ||||
|   if (createdAt) { | ||||
|     const timestamp = Number(createdAt); | ||||
|     if (!Number.isNaN(timestamp)) { | ||||
|       const milliseconds = timestamp > 1e12 ? timestamp : timestamp * 1000; | ||||
|       const derivedDate = new Date(milliseconds); | ||||
|       if (!Number.isNaN(derivedDate.getTime())) { | ||||
|         return derivedDate; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return null; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Formats date for quoted email header | ||||
|  * @param {Date} date - Date to format | ||||
|  * @returns {string} Formatted date string | ||||
|  */ | ||||
| export const formatQuotedEmailDate = date => { | ||||
|   try { | ||||
|     return format(date, "EEE, MMM d, yyyy 'at' p"); | ||||
|   } catch (error) { | ||||
|     const fallbackDate = new Date(date); | ||||
|     if (!Number.isNaN(fallbackDate.getTime())) { | ||||
|       return format(fallbackDate, "EEE, MMM d, yyyy 'at' p"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ''; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Extracts inbox email address from last email message | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @param {Object} inbox - Inbox object | ||||
|  * @returns {string} Inbox email address | ||||
|  */ | ||||
| export const getInboxEmail = (lastEmail, inbox) => { | ||||
|   const contentAttributes = | ||||
|     lastEmail?.contentAttributes || lastEmail?.content_attributes || {}; | ||||
|   const emailMeta = contentAttributes.email || {}; | ||||
|  | ||||
|   if (Array.isArray(emailMeta.to) && emailMeta.to.length > 0) { | ||||
|     const toAddress = emailMeta.to[0]; | ||||
|     if (toAddress && toAddress.trim()) { | ||||
|       return toAddress.trim(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const inboxEmail = inbox?.email; | ||||
|   return inboxEmail && inboxEmail.trim() ? inboxEmail.trim() : ''; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Builds quoted email header from contact (for incoming messages) | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @param {Object} contact - Contact object | ||||
|  * @returns {string} Formatted header string | ||||
|  */ | ||||
| export const buildQuotedEmailHeaderFromContact = (lastEmail, contact) => { | ||||
|   if (!lastEmail) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const quotedDate = getEmailDate(lastEmail); | ||||
|   const senderEmail = getEmailSenderEmail(lastEmail, contact); | ||||
|  | ||||
|   if (!quotedDate || !senderEmail) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const formattedDate = formatQuotedEmailDate(quotedDate); | ||||
|   if (!formattedDate) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const senderName = getEmailSenderName(lastEmail, contact); | ||||
|   const hasName = !!senderName; | ||||
|   const contactLabel = hasName | ||||
|     ? `${senderName} <${senderEmail}>` | ||||
|     : `<${senderEmail}>`; | ||||
|  | ||||
|   return `On ${formattedDate} ${contactLabel} wrote:`; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Builds quoted email header from inbox (for outgoing messages) | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @param {Object} inbox - Inbox object | ||||
|  * @returns {string} Formatted header string | ||||
|  */ | ||||
| export const buildQuotedEmailHeaderFromInbox = (lastEmail, inbox) => { | ||||
|   if (!lastEmail) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const quotedDate = getEmailDate(lastEmail); | ||||
|   const inboxEmail = getInboxEmail(lastEmail, inbox); | ||||
|  | ||||
|   if (!quotedDate || !inboxEmail) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const formattedDate = formatQuotedEmailDate(quotedDate); | ||||
|   if (!formattedDate) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const inboxName = inbox?.name; | ||||
|   const hasName = !!inboxName; | ||||
|   const inboxLabel = hasName | ||||
|     ? `${inboxName} <${inboxEmail}>` | ||||
|     : `<${inboxEmail}>`; | ||||
|  | ||||
|   return `On ${formattedDate} ${inboxLabel} wrote:`; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Builds quoted email header based on message type | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @param {Object} contact - Contact object | ||||
|  * @param {Object} inbox - Inbox object | ||||
|  * @returns {string} Formatted header string | ||||
|  */ | ||||
| export const buildQuotedEmailHeader = (lastEmail, contact, inbox) => { | ||||
|   if (!lastEmail) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   // MESSAGE_TYPE.OUTGOING = 1, MESSAGE_TYPE.INCOMING = 0 | ||||
|   const isOutgoing = lastEmail.message_type === 1; | ||||
|  | ||||
|   if (isOutgoing) { | ||||
|     return buildQuotedEmailHeaderFromInbox(lastEmail, inbox); | ||||
|   } | ||||
|  | ||||
|   return buildQuotedEmailHeaderFromContact(lastEmail, contact); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Formats text as markdown blockquote | ||||
|  * @param {string} text - Text to format | ||||
|  * @param {string} header - Optional header to prepend | ||||
|  * @returns {string} Formatted blockquote | ||||
|  */ | ||||
| export const formatQuotedTextAsBlockquote = (text, header = '') => { | ||||
|   const normalizedLines = text | ||||
|     ? String(text).replace(/\r\n/g, '\n').split('\n') | ||||
|     : []; | ||||
|  | ||||
|   if (!header && !normalizedLines.length) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const quotedLines = []; | ||||
|  | ||||
|   if (header) { | ||||
|     quotedLines.push(`> ${header}`); | ||||
|     quotedLines.push('>'); | ||||
|   } | ||||
|  | ||||
|   normalizedLines.forEach(line => { | ||||
|     const trimmedLine = line.trimEnd(); | ||||
|     quotedLines.push(trimmedLine ? `> ${trimmedLine}` : '>'); | ||||
|   }); | ||||
|  | ||||
|   return quotedLines.join('\n'); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Extracts quoted email text from last email message | ||||
|  * @param {Object} lastEmail - Last email message object | ||||
|  * @returns {string} Quoted email text | ||||
|  */ | ||||
| export const extractQuotedEmailText = lastEmail => { | ||||
|   if (!lastEmail) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const contentAttributes = | ||||
|     lastEmail.contentAttributes || lastEmail.content_attributes || {}; | ||||
|   const emailContent = contentAttributes.email || {}; | ||||
|   const textContent = emailContent.textContent || emailContent.text_content; | ||||
|  | ||||
|   if (textContent?.reply) { | ||||
|     return textContent.reply; | ||||
|   } | ||||
|   if (textContent?.full) { | ||||
|     return textContent.full; | ||||
|   } | ||||
|  | ||||
|   const htmlContent = emailContent.htmlContent || emailContent.html_content; | ||||
|   if (htmlContent?.reply) { | ||||
|     return extractPlainTextFromHtml(htmlContent.reply); | ||||
|   } | ||||
|   if (htmlContent?.full) { | ||||
|     return extractPlainTextFromHtml(htmlContent.full); | ||||
|   } | ||||
|  | ||||
|   const fallbackContent = | ||||
|     lastEmail.content || lastEmail.processed_message_content || ''; | ||||
|  | ||||
|   return fallbackContent; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Truncates text for preview display | ||||
|  * @param {string} text - Text to truncate | ||||
|  * @param {number} maxLength - Maximum length (default: 80) | ||||
|  * @returns {string} Truncated text | ||||
|  */ | ||||
| export const truncatePreviewText = (text, maxLength = 80) => { | ||||
|   const preview = text.trim().replace(/\s+/g, ' '); | ||||
|   if (!preview) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   if (preview.length <= maxLength) { | ||||
|     return preview; | ||||
|   } | ||||
|   return `${preview.slice(0, maxLength - 3)}...`; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Appends quoted text to message | ||||
|  * @param {string} message - Original message | ||||
|  * @param {string} quotedText - Text to quote | ||||
|  * @param {string} header - Quote header | ||||
|  * @returns {string} Message with quoted text appended | ||||
|  */ | ||||
| export const appendQuotedTextToMessage = (message, quotedText, header) => { | ||||
|   const baseMessage = message ? String(message) : ''; | ||||
|   const quotedBlock = formatQuotedTextAsBlockquote(quotedText, header); | ||||
|  | ||||
|   if (!quotedBlock) { | ||||
|     return baseMessage; | ||||
|   } | ||||
|  | ||||
|   if (!baseMessage) { | ||||
|     return quotedBlock; | ||||
|   } | ||||
|  | ||||
|   let separator = '\n\n'; | ||||
|   if (baseMessage.endsWith('\n\n')) { | ||||
|     separator = ''; | ||||
|   } else if (baseMessage.endsWith('\n')) { | ||||
|     separator = '\n'; | ||||
|   } | ||||
|  | ||||
|   return `${baseMessage}${separator}${quotedBlock}`; | ||||
| }; | ||||
| @@ -0,0 +1,99 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { EmailQuoteExtractor } from '../emailQuoteExtractor.js'; | ||||
|  | ||||
| const SAMPLE_EMAIL_HTML = ` | ||||
| <p>method</p> | ||||
| <blockquote> | ||||
| <p>On Mon, Sep 29, 2025 at 5:18 PM John <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p> | ||||
| <p>Hi</p> | ||||
| <blockquote> | ||||
| <p>On Mon, Sep 29, 2025 at 5:17 PM Shivam Mishra <a href="mailto:shivam@chatwoot.com">shivam@chatwoot.com</a> wrote:</p> | ||||
| <p>Yes, it is.</p> | ||||
| <p>On Mon, Sep 29, 2025 at 5:16 PM John from Shaneforwoot < shaneforwoot@gmail.com> wrote:</p> | ||||
| <blockquote> | ||||
| <p>Hey</p> | ||||
| <p>On Mon, Sep 29, 2025 at 4:59 PM John shivam@chatwoot.com wrote:</p> | ||||
| <p>This is another quoted quoted text reply</p> | ||||
| <p>This is nice</p> | ||||
| <p>On Mon, Sep 29, 2025 at 4:21 PM John from Shaneforwoot < > shaneforwoot@gmail.com> wrote:</p> | ||||
| <p>Hey there, this is a reply from Chatwoot, notice the quoted text</p> | ||||
| <p>Hey there</p> | ||||
| <p>This is an email text, enjoy reading this</p> | ||||
| <p>-- Shivam Mishra, Chatwoot</p> | ||||
| </blockquote> | ||||
| </blockquote> | ||||
| </blockquote> | ||||
| `; | ||||
|  | ||||
| const EMAIL_WITH_SIGNATURE = ` | ||||
| <p>Latest reply here.</p> | ||||
| <p>Thanks,</p> | ||||
| <p>Jane Doe</p> | ||||
| <blockquote> | ||||
|   <p>On Mon, Sep 22, Someone wrote:</p> | ||||
|   <p>Previous reply content</p> | ||||
| </blockquote> | ||||
| `; | ||||
|  | ||||
| const EMAIL_WITH_FOLLOW_UP_CONTENT = ` | ||||
| <blockquote> | ||||
|   <p>Inline quote that should stay</p> | ||||
| </blockquote> | ||||
| <p>Internal note follows</p> | ||||
| <p>Regards,</p> | ||||
| `; | ||||
|  | ||||
| describe('EmailQuoteExtractor', () => { | ||||
|   it('removes blockquote-based quotes from the email body', () => { | ||||
|     const cleanedHtml = EmailQuoteExtractor.extractQuotes(SAMPLE_EMAIL_HTML); | ||||
|  | ||||
|     const container = document.createElement('div'); | ||||
|     container.innerHTML = cleanedHtml; | ||||
|  | ||||
|     expect(container.querySelectorAll('blockquote').length).toBe(0); | ||||
|     expect(container.textContent?.trim()).toBe('method'); | ||||
|     expect(container.textContent).not.toContain( | ||||
|       'On Mon, Sep 29, 2025 at 5:18 PM' | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('keeps blockquote fallback when it is not the last top-level element', () => { | ||||
|     const cleanedHtml = EmailQuoteExtractor.extractQuotes( | ||||
|       EMAIL_WITH_FOLLOW_UP_CONTENT | ||||
|     ); | ||||
|  | ||||
|     const container = document.createElement('div'); | ||||
|     container.innerHTML = cleanedHtml; | ||||
|  | ||||
|     expect(container.querySelector('blockquote')).not.toBeNull(); | ||||
|     expect(container.lastElementChild?.tagName).toBe('P'); | ||||
|   }); | ||||
|  | ||||
|   it('detects quote indicators in nested blockquotes', () => { | ||||
|     const result = EmailQuoteExtractor.hasQuotes(SAMPLE_EMAIL_HTML); | ||||
|     expect(result).toBe(true); | ||||
|   }); | ||||
|  | ||||
|   it('does not flag blockquotes that are followed by other elements', () => { | ||||
|     expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_FOLLOW_UP_CONTENT)).toBe( | ||||
|       false | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   it('returns false when no quote indicators are present', () => { | ||||
|     const html = '<p>Plain content</p>'; | ||||
|     expect(EmailQuoteExtractor.hasQuotes(html)).toBe(false); | ||||
|   }); | ||||
|  | ||||
|   it('removes trailing blockquotes while preserving trailing signatures', () => { | ||||
|     const cleanedHtml = EmailQuoteExtractor.extractQuotes(EMAIL_WITH_SIGNATURE); | ||||
|  | ||||
|     expect(cleanedHtml).toContain('<p>Thanks,</p>'); | ||||
|     expect(cleanedHtml).toContain('<p>Jane Doe</p>'); | ||||
|     expect(cleanedHtml).not.toContain('<blockquote'); | ||||
|   }); | ||||
|  | ||||
|   it('detects quotes for trailing blockquotes even when signatures follow text', () => { | ||||
|     expect(EmailQuoteExtractor.hasQuotes(EMAIL_WITH_SIGNATURE)).toBe(true); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										441
									
								
								app/javascript/dashboard/helper/specs/quotedEmailHelper.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										441
									
								
								app/javascript/dashboard/helper/specs/quotedEmailHelper.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,441 @@ | ||||
| import { | ||||
|   extractPlainTextFromHtml, | ||||
|   getEmailSenderName, | ||||
|   getEmailSenderEmail, | ||||
|   getEmailDate, | ||||
|   formatQuotedEmailDate, | ||||
|   getInboxEmail, | ||||
|   buildQuotedEmailHeader, | ||||
|   buildQuotedEmailHeaderFromContact, | ||||
|   buildQuotedEmailHeaderFromInbox, | ||||
|   formatQuotedTextAsBlockquote, | ||||
|   extractQuotedEmailText, | ||||
|   truncatePreviewText, | ||||
|   appendQuotedTextToMessage, | ||||
| } from '../quotedEmailHelper'; | ||||
|  | ||||
| describe('quotedEmailHelper', () => { | ||||
|   describe('extractPlainTextFromHtml', () => { | ||||
|     it('returns empty string for null or undefined', () => { | ||||
|       expect(extractPlainTextFromHtml(null)).toBe(''); | ||||
|       expect(extractPlainTextFromHtml(undefined)).toBe(''); | ||||
|     }); | ||||
|  | ||||
|     it('strips HTML tags and returns plain text', () => { | ||||
|       const html = '<p>Hello <strong>world</strong></p>'; | ||||
|       const result = extractPlainTextFromHtml(html); | ||||
|       expect(result).toBe('Hello world'); | ||||
|     }); | ||||
|  | ||||
|     it('handles complex HTML structure', () => { | ||||
|       const html = '<div><p>Line 1</p><p>Line 2</p></div>'; | ||||
|       const result = extractPlainTextFromHtml(html); | ||||
|       expect(result).toContain('Line 1'); | ||||
|       expect(result).toContain('Line 2'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getEmailSenderName', () => { | ||||
|     it('returns sender name from lastEmail', () => { | ||||
|       const lastEmail = { sender: { name: 'John Doe' } }; | ||||
|       const result = getEmailSenderName(lastEmail, {}); | ||||
|       expect(result).toBe('John Doe'); | ||||
|     }); | ||||
|  | ||||
|     it('returns contact name if sender name not available', () => { | ||||
|       const lastEmail = { sender: {} }; | ||||
|       const contact = { name: 'Jane Smith' }; | ||||
|       const result = getEmailSenderName(lastEmail, contact); | ||||
|       expect(result).toBe('Jane Smith'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string if neither available', () => { | ||||
|       const result = getEmailSenderName({}, {}); | ||||
|       expect(result).toBe(''); | ||||
|     }); | ||||
|  | ||||
|     it('trims whitespace from names', () => { | ||||
|       const lastEmail = { sender: { name: '  John Doe  ' } }; | ||||
|       const result = getEmailSenderName(lastEmail, {}); | ||||
|       expect(result).toBe('John Doe'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getEmailSenderEmail', () => { | ||||
|     it('returns sender email from lastEmail', () => { | ||||
|       const lastEmail = { sender: { email: 'john@example.com' } }; | ||||
|       const result = getEmailSenderEmail(lastEmail, {}); | ||||
|       expect(result).toBe('john@example.com'); | ||||
|     }); | ||||
|  | ||||
|     it('returns email from contentAttributes if sender email not available', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { from: ['jane@example.com'] }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = getEmailSenderEmail(lastEmail, {}); | ||||
|       expect(result).toBe('jane@example.com'); | ||||
|     }); | ||||
|  | ||||
|     it('returns contact email as fallback', () => { | ||||
|       const lastEmail = {}; | ||||
|       const contact = { email: 'contact@example.com' }; | ||||
|       const result = getEmailSenderEmail(lastEmail, contact); | ||||
|       expect(result).toBe('contact@example.com'); | ||||
|     }); | ||||
|  | ||||
|     it('trims whitespace from emails', () => { | ||||
|       const lastEmail = { sender: { email: '  john@example.com  ' } }; | ||||
|       const result = getEmailSenderEmail(lastEmail, {}); | ||||
|       expect(result).toBe('john@example.com'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getEmailDate', () => { | ||||
|     it('returns parsed date from email metadata', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { date: '2024-01-15T10:30:00Z' }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = getEmailDate(lastEmail); | ||||
|       expect(result).toBeInstanceOf(Date); | ||||
|     }); | ||||
|  | ||||
|     it('returns date from created_at timestamp', () => { | ||||
|       const lastEmail = { created_at: 1705318200 }; | ||||
|       const result = getEmailDate(lastEmail); | ||||
|       expect(result).toBeInstanceOf(Date); | ||||
|     }); | ||||
|  | ||||
|     it('handles millisecond timestamps', () => { | ||||
|       const lastEmail = { created_at: 1705318200000 }; | ||||
|       const result = getEmailDate(lastEmail); | ||||
|       expect(result).toBeInstanceOf(Date); | ||||
|     }); | ||||
|  | ||||
|     it('returns null if no valid date found', () => { | ||||
|       const result = getEmailDate({}); | ||||
|       expect(result).toBeNull(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('formatQuotedEmailDate', () => { | ||||
|     it('formats date correctly', () => { | ||||
|       const date = new Date('2024-01-15T10:30:00Z'); | ||||
|       const result = formatQuotedEmailDate(date); | ||||
|       expect(result).toMatch(/Mon, Jan 15, 2024 at/); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string for invalid date', () => { | ||||
|       const result = formatQuotedEmailDate('invalid'); | ||||
|       expect(result).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('getInboxEmail', () => { | ||||
|     it('returns email from contentAttributes.email.to', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { to: ['inbox@example.com'] }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = getInboxEmail(lastEmail, {}); | ||||
|       expect(result).toBe('inbox@example.com'); | ||||
|     }); | ||||
|  | ||||
|     it('returns inbox email as fallback', () => { | ||||
|       const lastEmail = {}; | ||||
|       const inbox = { email: 'support@example.com' }; | ||||
|       const result = getInboxEmail(lastEmail, inbox); | ||||
|       expect(result).toBe('support@example.com'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string if no email found', () => { | ||||
|       expect(getInboxEmail({}, {})).toBe(''); | ||||
|     }); | ||||
|  | ||||
|     it('trims whitespace from emails', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { to: ['  inbox@example.com  '] }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = getInboxEmail(lastEmail, {}); | ||||
|       expect(result).toBe('inbox@example.com'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('buildQuotedEmailHeaderFromContact', () => { | ||||
|     it('builds complete header with name and email', () => { | ||||
|       const lastEmail = { | ||||
|         sender: { name: 'John Doe', email: 'john@example.com' }, | ||||
|         contentAttributes: { | ||||
|           email: { date: '2024-01-15T10:30:00Z' }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = buildQuotedEmailHeaderFromContact(lastEmail, {}); | ||||
|       expect(result).toContain('John Doe'); | ||||
|       expect(result).toContain('john@example.com'); | ||||
|       expect(result).toContain('wrote:'); | ||||
|     }); | ||||
|  | ||||
|     it('builds header without name if not available', () => { | ||||
|       const lastEmail = { | ||||
|         sender: { email: 'john@example.com' }, | ||||
|         contentAttributes: { | ||||
|           email: { date: '2024-01-15T10:30:00Z' }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = buildQuotedEmailHeaderFromContact(lastEmail, {}); | ||||
|       expect(result).toContain('<john@example.com>'); | ||||
|       expect(result).not.toContain('undefined'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string if missing required data', () => { | ||||
|       expect(buildQuotedEmailHeaderFromContact(null, {})).toBe(''); | ||||
|       expect(buildQuotedEmailHeaderFromContact({}, {})).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('buildQuotedEmailHeaderFromInbox', () => { | ||||
|     it('builds complete header with inbox name and email', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { | ||||
|             date: '2024-01-15T10:30:00Z', | ||||
|             to: ['support@example.com'], | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
|       const inbox = { name: 'Support Team', email: 'support@example.com' }; | ||||
|       const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox); | ||||
|       expect(result).toContain('Support Team'); | ||||
|       expect(result).toContain('support@example.com'); | ||||
|       expect(result).toContain('wrote:'); | ||||
|     }); | ||||
|  | ||||
|     it('builds header without name if not available', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { | ||||
|             date: '2024-01-15T10:30:00Z', | ||||
|             to: ['inbox@example.com'], | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
|       const inbox = { email: 'inbox@example.com' }; | ||||
|       const result = buildQuotedEmailHeaderFromInbox(lastEmail, inbox); | ||||
|       expect(result).toContain('<inbox@example.com>'); | ||||
|       expect(result).not.toContain('undefined'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string if missing required data', () => { | ||||
|       expect(buildQuotedEmailHeaderFromInbox(null, {})).toBe(''); | ||||
|       expect(buildQuotedEmailHeaderFromInbox({}, {})).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('buildQuotedEmailHeader', () => { | ||||
|     it('uses inbox email for outgoing messages (message_type: 1)', () => { | ||||
|       const lastEmail = { | ||||
|         message_type: 1, | ||||
|         contentAttributes: { | ||||
|           email: { | ||||
|             date: '2024-01-15T10:30:00Z', | ||||
|             to: ['support@example.com'], | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
|       const inbox = { name: 'Support', email: 'support@example.com' }; | ||||
|       const contact = { name: 'John Doe', email: 'john@example.com' }; | ||||
|       const result = buildQuotedEmailHeader(lastEmail, contact, inbox); | ||||
|       expect(result).toContain('Support'); | ||||
|       expect(result).toContain('support@example.com'); | ||||
|       expect(result).not.toContain('John Doe'); | ||||
|     }); | ||||
|  | ||||
|     it('uses contact email for incoming messages (message_type: 0)', () => { | ||||
|       const lastEmail = { | ||||
|         message_type: 0, | ||||
|         sender: { name: 'Jane Smith', email: 'jane@example.com' }, | ||||
|         contentAttributes: { | ||||
|           email: { date: '2024-01-15T10:30:00Z' }, | ||||
|         }, | ||||
|       }; | ||||
|       const inbox = { name: 'Support', email: 'support@example.com' }; | ||||
|       const contact = { name: 'Jane Smith', email: 'jane@example.com' }; | ||||
|       const result = buildQuotedEmailHeader(lastEmail, contact, inbox); | ||||
|       expect(result).toContain('Jane Smith'); | ||||
|       expect(result).toContain('jane@example.com'); | ||||
|       expect(result).not.toContain('Support'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string if missing required data', () => { | ||||
|       expect(buildQuotedEmailHeader(null, {}, {})).toBe(''); | ||||
|       expect(buildQuotedEmailHeader({}, {}, {})).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('formatQuotedTextAsBlockquote', () => { | ||||
|     it('formats single line text', () => { | ||||
|       const result = formatQuotedTextAsBlockquote('Hello world'); | ||||
|       expect(result).toBe('> Hello world'); | ||||
|     }); | ||||
|  | ||||
|     it('formats multi-line text', () => { | ||||
|       const text = 'Line 1\nLine 2\nLine 3'; | ||||
|       const result = formatQuotedTextAsBlockquote(text); | ||||
|       expect(result).toBe('> Line 1\n> Line 2\n> Line 3'); | ||||
|     }); | ||||
|  | ||||
|     it('includes header if provided', () => { | ||||
|       const result = formatQuotedTextAsBlockquote('Hello', 'Header text'); | ||||
|       expect(result).toContain('> Header text'); | ||||
|       expect(result).toContain('>\n> Hello'); | ||||
|     }); | ||||
|  | ||||
|     it('handles empty lines correctly', () => { | ||||
|       const text = 'Line 1\n\nLine 3'; | ||||
|       const result = formatQuotedTextAsBlockquote(text); | ||||
|       expect(result).toBe('> Line 1\n>\n> Line 3'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string for empty input', () => { | ||||
|       expect(formatQuotedTextAsBlockquote('')).toBe(''); | ||||
|       expect(formatQuotedTextAsBlockquote('', '')).toBe(''); | ||||
|     }); | ||||
|  | ||||
|     it('handles Windows line endings', () => { | ||||
|       const text = 'Line 1\r\nLine 2'; | ||||
|       const result = formatQuotedTextAsBlockquote(text); | ||||
|       expect(result).toBe('> Line 1\n> Line 2'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('extractQuotedEmailText', () => { | ||||
|     it('extracts text from textContent.reply', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { textContent: { reply: 'Reply text' } }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = extractQuotedEmailText(lastEmail); | ||||
|       expect(result).toBe('Reply text'); | ||||
|     }); | ||||
|  | ||||
|     it('falls back to textContent.full', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { textContent: { full: 'Full text' } }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = extractQuotedEmailText(lastEmail); | ||||
|       expect(result).toBe('Full text'); | ||||
|     }); | ||||
|  | ||||
|     it('extracts from htmlContent and converts to plain text', () => { | ||||
|       const lastEmail = { | ||||
|         contentAttributes: { | ||||
|           email: { htmlContent: { reply: '<p>HTML reply</p>' } }, | ||||
|         }, | ||||
|       }; | ||||
|       const result = extractQuotedEmailText(lastEmail); | ||||
|       expect(result).toBe('HTML reply'); | ||||
|     }); | ||||
|  | ||||
|     it('uses fallback content if structured content not available', () => { | ||||
|       const lastEmail = { content: 'Fallback content' }; | ||||
|       const result = extractQuotedEmailText(lastEmail); | ||||
|       expect(result).toBe('Fallback content'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string for null or missing email', () => { | ||||
|       expect(extractQuotedEmailText(null)).toBe(''); | ||||
|       expect(extractQuotedEmailText({})).toBe(''); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('truncatePreviewText', () => { | ||||
|     it('returns full text if under max length', () => { | ||||
|       const text = 'Short text'; | ||||
|       const result = truncatePreviewText(text, 80); | ||||
|       expect(result).toBe('Short text'); | ||||
|     }); | ||||
|  | ||||
|     it('truncates text exceeding max length', () => { | ||||
|       const text = 'A'.repeat(100); | ||||
|       const result = truncatePreviewText(text, 80); | ||||
|       expect(result).toHaveLength(80); | ||||
|       expect(result).toContain('...'); | ||||
|     }); | ||||
|  | ||||
|     it('collapses multiple spaces', () => { | ||||
|       const text = 'Text   with    spaces'; | ||||
|       const result = truncatePreviewText(text); | ||||
|       expect(result).toBe('Text with spaces'); | ||||
|     }); | ||||
|  | ||||
|     it('trims whitespace', () => { | ||||
|       const text = '  Text with spaces  '; | ||||
|       const result = truncatePreviewText(text); | ||||
|       expect(result).toBe('Text with spaces'); | ||||
|     }); | ||||
|  | ||||
|     it('returns empty string for empty input', () => { | ||||
|       expect(truncatePreviewText('')).toBe(''); | ||||
|       expect(truncatePreviewText('   ')).toBe(''); | ||||
|     }); | ||||
|  | ||||
|     it('uses default max length of 80', () => { | ||||
|       const text = 'A'.repeat(100); | ||||
|       const result = truncatePreviewText(text); | ||||
|       expect(result).toHaveLength(80); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('appendQuotedTextToMessage', () => { | ||||
|     it('appends quoted text to message', () => { | ||||
|       const message = 'My reply'; | ||||
|       const quotedText = 'Original message'; | ||||
|       const header = 'On date sender wrote:'; | ||||
|       const result = appendQuotedTextToMessage(message, quotedText, header); | ||||
|  | ||||
|       expect(result).toContain('My reply'); | ||||
|       expect(result).toContain('> On date sender wrote:'); | ||||
|       expect(result).toContain('> Original message'); | ||||
|     }); | ||||
|  | ||||
|     it('returns only quoted text if message is empty', () => { | ||||
|       const result = appendQuotedTextToMessage('', 'Quoted', 'Header'); | ||||
|       expect(result).toContain('> Header'); | ||||
|       expect(result).toContain('> Quoted'); | ||||
|       expect(result).not.toContain('\n\n\n'); | ||||
|     }); | ||||
|  | ||||
|     it('returns message if no quoted text', () => { | ||||
|       const result = appendQuotedTextToMessage('Message', '', ''); | ||||
|       expect(result).toBe('Message'); | ||||
|     }); | ||||
|  | ||||
|     it('handles proper spacing with double newline', () => { | ||||
|       const result = appendQuotedTextToMessage('Message', 'Quoted', 'Header'); | ||||
|       expect(result).toContain('Message\n\n>'); | ||||
|     }); | ||||
|  | ||||
|     it('does not add extra newlines if message already ends with newlines', () => { | ||||
|       const result = appendQuotedTextToMessage( | ||||
|         'Message\n\n', | ||||
|         'Quoted', | ||||
|         'Header' | ||||
|       ); | ||||
|       expect(result).not.toContain('\n\n\n'); | ||||
|     }); | ||||
|  | ||||
|     it('adds single newline if message ends with one newline', () => { | ||||
|       const result = appendQuotedTextToMessage('Message\n', 'Quoted', 'Header'); | ||||
|       expect(result).toContain('Message\n\n>'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Cancel" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Search", | ||||
|       "EMPTY_STATE": "No results found" | ||||
|     }, | ||||
|     "CLOSE": "Close" | ||||
|     "CLOSE": "Close", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger Script", | ||||
|       "MESSENGER_SUB_HEAD": "Place this button inside your body tag", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agents", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", | ||||
|       "AGENT_ASSIGNMENT": "Conversation Assignment", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Password", | ||||
|       "PLACEHOLDER": "Password", | ||||
|       "ERROR": "Password is too short.", | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character." | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirm password", | ||||
|       "PLACEHOLDER": "Confirm password", | ||||
|       "ERROR": "Password doesnot match." | ||||
|       "ERROR": "Passwords do not match." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registration Successfull", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Could not connect to Woot server. Please try again." | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "إرسال", | ||||
|           "CANCEL": "إلغاء" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لك ولأعضاء فريقك", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "بحث", | ||||
|       "EMPTY_STATE": "لم يتم العثور على النتائج" | ||||
|     }, | ||||
|     "CLOSE": "أغلق" | ||||
|     "CLOSE": "أغلق", | ||||
|     "BETA": "تجريبي", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "كود \"الماسنجر\"", | ||||
|       "MESSENGER_SUB_HEAD": "ضع هذا الكود داخل وسم الـ body في موقعك", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "وكيل الدعم", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "إضافة أو إزالة وكلاء من صندوق الوارد هذا", | ||||
|       "AGENT_ASSIGNMENT": "تعيين المحادثة", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "كلمة المرور", | ||||
|       "PLACEHOLDER": "كلمة المرور", | ||||
|       "ERROR": "كلمة المرور قصيرة جداً", | ||||
|       "IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد" | ||||
|       "IS_INVALID_PASSWORD": "يجب أن تحتوي كلمة المرور على الأقل على حرف كبير واحد وحرف صغير واحد ورقم واحد وحرف خاص واحد", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "تأكيد كلمة المرور", | ||||
|       "PLACEHOLDER": "تأكيد كلمة المرور", | ||||
|       "ERROR": "كلمة المرور غير متطابقة" | ||||
|       "ERROR": "كلمة المرور غير متطابقة." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "تم التسجيل بنجاح", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً" | ||||
|     }, | ||||
|     "SUBMIT": "إرسال", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Cancel" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Search", | ||||
|       "EMPTY_STATE": "No results found" | ||||
|     }, | ||||
|     "CLOSE": "Close" | ||||
|     "CLOSE": "Close", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger Script", | ||||
|       "MESSENGER_SUB_HEAD": "Place this button inside your body tag", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agents", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", | ||||
|       "AGENT_ASSIGNMENT": "Conversation Assignment", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Password", | ||||
|       "PLACEHOLDER": "Password", | ||||
|       "ERROR": "Password is too short.", | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character." | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirm password", | ||||
|       "PLACEHOLDER": "Confirm password", | ||||
|       "ERROR": "Password doesnot match." | ||||
|       "ERROR": "Passwords do not match." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registration Successfull", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Could not connect to Woot server. Please try again." | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Отмени" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Търсене", | ||||
|       "EMPTY_STATE": "Няма намерени резултати" | ||||
|     }, | ||||
|     "CLOSE": "Close" | ||||
|     "CLOSE": "Close", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger Script", | ||||
|       "MESSENGER_SUB_HEAD": "Place this button inside your body tag", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Агенти", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Add or remove agents from this inbox", | ||||
|       "AGENT_ASSIGNMENT": "Conversation Assignment", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Password", | ||||
|       "PLACEHOLDER": "Password", | ||||
|       "ERROR": "Password is too short", | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character" | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirm Password", | ||||
|       "PLACEHOLDER": "Confirm Password", | ||||
|       "ERROR": "Password doesnot match" | ||||
|       "ERROR": "Passwords do not match." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registration Successfull", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно" | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Envia", | ||||
|           "CANCEL": "Cancel·la" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Cercar", | ||||
|       "EMPTY_STATE": "No s'ha trobat agents" | ||||
|     }, | ||||
|     "CLOSE": "Tanca" | ||||
|     "CLOSE": "Tanca", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Script del missatger", | ||||
|       "MESSENGER_SUB_HEAD": "Col·loca aquest botó dins de l'etiqueta body", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agents", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Afegir o eliminar agents d'aquesta safata d'entrada", | ||||
|       "AGENT_ASSIGNMENT": "Conversació Assignada", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Contrasenya", | ||||
|       "PLACEHOLDER": "Contrasenya", | ||||
|       "ERROR": "La contrasenya és massa curta", | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character" | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirma la contrasenya", | ||||
|       "PLACEHOLDER": "Confirma la contrasenya", | ||||
|       "ERROR": "La contrasenya no coincideix." | ||||
|       "ERROR": "La contrasenya no coindeix." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registrat correctament", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant" | ||||
|     }, | ||||
|     "SUBMIT": "Crear un compte", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Poslat", | ||||
|           "CANCEL": "Zrušit" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Hledat", | ||||
|       "EMPTY_STATE": "Žádné výsledky" | ||||
|     }, | ||||
|     "CLOSE": "Zavřít" | ||||
|     "CLOSE": "Zavřít", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger skript", | ||||
|       "MESSENGER_SUB_HEAD": "Umístěte toto tlačítko dovnitř vašeho tělesného štítku", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agenti", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Přidat nebo odebrat agenty z této složky doručené pošty", | ||||
|       "AGENT_ASSIGNMENT": "Conversation Assignment", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Heslo", | ||||
|       "PLACEHOLDER": "Heslo", | ||||
|       "ERROR": "Heslo je příliš krátké", | ||||
|       "IS_INVALID_PASSWORD": "Heslo by mělo obsahovat alespoň jedno velké písmeno, jedno malé písmeno, jedno číslo a jeden speciální znak" | ||||
|       "IS_INVALID_PASSWORD": "Heslo by mělo obsahovat alespoň jedno velké písmeno, jedno malé písmeno, jedno číslo a jeden speciální znak", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Potvrzení hesla", | ||||
|       "PLACEHOLDER": "Potvrzení hesla", | ||||
|       "ERROR": "Heslo se neshoduje" | ||||
|       "ERROR": "Hesla se neshodují." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registrace byla úspěšná", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později" | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Annuller" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Søg", | ||||
|       "EMPTY_STATE": "Ingen resultater fundet" | ||||
|     }, | ||||
|     "CLOSE": "Luk" | ||||
|     "CLOSE": "Luk", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger- Script", | ||||
|       "MESSENGER_SUB_HEAD": "Placer denne knap inde i din body tag", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agenter", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Tilføj eller fjern agenter fra denne indbakke", | ||||
|       "AGENT_ASSIGNMENT": "Samtale Tildeling", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Adgangskode", | ||||
|       "PLACEHOLDER": "Adgangskode", | ||||
|       "ERROR": "Adgangskoden er for kort", | ||||
|       "IS_INVALID_PASSWORD": "Adgangskoden skal indeholde mindst 1 stort bogstav, 1 lille bogstav, 1 nummer og 1 specialtegn" | ||||
|       "IS_INVALID_PASSWORD": "Adgangskoden skal indeholde mindst 1 stort bogstav, 1 lille bogstav, 1 nummer og 1 specialtegn", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Bekræft Adgangskode", | ||||
|       "PLACEHOLDER": "Bekræft Adgangskode", | ||||
|       "ERROR": "Adgangskode stemmer ikke overens" | ||||
|       "ERROR": "Adgangskoder stemmer ikke overens." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registrering Succesfuld", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere" | ||||
|     }, | ||||
|     "SUBMIT": "Opret en konto", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Senden", | ||||
|           "CANCEL": "Abbrechen" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Suchen", | ||||
|       "EMPTY_STATE": "Keine Ergebnisse gefunden" | ||||
|     }, | ||||
|     "CLOSE": "Schließen" | ||||
|     "CLOSE": "Schließen", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger-Skript", | ||||
|       "MESSENGER_SUB_HEAD": "Platzieren Sie diese Schaltfläche in Ihrem Body-Tag", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agenten", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang", | ||||
|       "AGENT_ASSIGNMENT": "Konversationssauftrag", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Passwort", | ||||
|       "PLACEHOLDER": "Passwort", | ||||
|       "ERROR": "Das Passwort ist zu kurz", | ||||
|       "IS_INVALID_PASSWORD": "Das Passwort sollte mindestens 1 Großbuchstaben, 1 Kleinbuchstaben, 1 Ziffer und 1 Sonderzeichen enthalten" | ||||
|       "IS_INVALID_PASSWORD": "Das Passwort sollte mindestens 1 Großbuchstaben, 1 Kleinbuchstaben, 1 Ziffer und 1 Sonderzeichen enthalten", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Bestätige das Passwort", | ||||
|       "PLACEHOLDER": "Bestätige das Passwort", | ||||
|       "ERROR": "Passwort stimmt nicht überein" | ||||
|       "ERROR": "Passwörter stimmen nicht überein." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registrierung erfolgreich", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Es konnte keine Verbindung zum Woot Server hergestellt werden. Bitte versuchen Sie es später erneut" | ||||
|     }, | ||||
|     "SUBMIT": "Konto erstellen", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Αποστολή", | ||||
|           "CANCEL": "Άκυρο" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Ιδιωτική Σημείωση: Ορατή μόνο σε σας και την ομάδα σας", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Αναζήτηση", | ||||
|       "EMPTY_STATE": "Δεν βρέθηκαν αποτελέσματα" | ||||
|     }, | ||||
|     "CLOSE": "Κλείσιμο" | ||||
|     "CLOSE": "Κλείσιμο", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Κώδικας (Script)", | ||||
|       "MESSENGER_SUB_HEAD": "Τοποθετήσετε αυτόν τον κώδικα μέσα στο body tag της ιστοσελίδας σας", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Πράκτορες", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Προσθέστε ή αφαιρέστε πράκτορες σε αυτό το κιβώτιο", | ||||
|       "AGENT_ASSIGNMENT": "Ανάθεση Συνομιλίας", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Κωδικός", | ||||
|       "PLACEHOLDER": "Κωδικός", | ||||
|       "ERROR": "Ο κωδικός είναι πολύ σύντομος", | ||||
|       "IS_INVALID_PASSWORD": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 1 κεφαλαίο γράμμα, 1 πεζό γράμμα, 1 αριθμό και 1 ειδικό χαρακτήρα" | ||||
|       "IS_INVALID_PASSWORD": "Ο κωδικός πρόσβασης πρέπει να περιέχει τουλάχιστον 1 κεφαλαίο γράμμα, 1 πεζό γράμμα, 1 αριθμό και 1 ειδικό χαρακτήρα", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Επιβεβαίωση κωδικού", | ||||
|       "PLACEHOLDER": "Επιβεβαίωση κωδικού", | ||||
|       "ERROR": "Οι κωδικοί δεν συμφωνούν" | ||||
|       "ERROR": "Οι κωδικοί δεν ταιριάζουν." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Επιτυχής καταχώρηση", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Αδυναμία σύνδεσης με τον Woot Server, Παρακαλώ προσπαθήστε αργότερα" | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Cancel" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|     "LEARN_MORE": "Learn more about inboxes", | ||||
|     "RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.", | ||||
|     "CLICK_TO_RECONNECT": "Click here to reconnect.", | ||||
|     "WHATSAPP_REGISTRATION_INCOMPLETE": "Your WhatsApp Business registration isn’t complete. Please check your display name status in Meta Business Manager before reconnecting.", | ||||
|     "COMPLETE_REGISTRATION": "Complete Registration", | ||||
|     "LIST": { | ||||
|       "404": "There are no inboxes attached to this account." | ||||
|     }, | ||||
| @@ -605,8 +607,62 @@ | ||||
|       "BUSINESS_HOURS": "Business Hours", | ||||
|       "WIDGET_BUILDER": "Widget Builder", | ||||
|       "BOT_CONFIGURATION": "Bot Configuration", | ||||
|       "ACCOUNT_HEALTH": "Account Health", | ||||
|       "CSAT": "CSAT" | ||||
|     }, | ||||
|     "ACCOUNT_HEALTH": { | ||||
|       "TITLE": "Manage your WhatsApp account", | ||||
|       "DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed", | ||||
|       "GO_TO_SETTINGS": "Go to Meta Business Manager", | ||||
|       "NO_DATA": "Health data is not available", | ||||
|       "FIELDS": { | ||||
|         "DISPLAY_PHONE_NUMBER": { | ||||
|           "LABEL": "Display phone number", | ||||
|           "TOOLTIP": "Phone number displayed to customers" | ||||
|         }, | ||||
|         "VERIFIED_NAME": { | ||||
|           "LABEL": "Business name", | ||||
|           "TOOLTIP": "Business name verified by WhatsApp" | ||||
|         }, | ||||
|         "DISPLAY_NAME_STATUS": { | ||||
|           "LABEL": "Display name status", | ||||
|           "TOOLTIP": "Status of your business name verification" | ||||
|         }, | ||||
|         "QUALITY_RATING": { | ||||
|           "LABEL": "Quality rating", | ||||
|           "TOOLTIP": "WhatsApp quality rating for your account" | ||||
|         }, | ||||
|         "MESSAGING_LIMIT_TIER": { | ||||
|           "LABEL": "Messaging limit tier", | ||||
|           "TOOLTIP": "Daily messaging limit for your account" | ||||
|         }, | ||||
|         "ACCOUNT_MODE": { | ||||
|           "LABEL": "Account mode", | ||||
|           "TOOLTIP": "Current operating mode of your WhatsApp account" | ||||
|         } | ||||
|       }, | ||||
|       "VALUES": { | ||||
|         "TIERS": { | ||||
|           "TIER_250": "250 customers per 24h", | ||||
|           "TIER_1000": "1K customers per 24h", | ||||
|           "TIER_1K": "1K customers per 24h", | ||||
|           "TIER_10K": "10K customers per 24h", | ||||
|           "TIER_100K": "100K customers per 24h", | ||||
|           "TIER_UNLIMITED": "Unlimited customers per 24h" | ||||
|         }, | ||||
|         "STATUSES": { | ||||
|           "APPROVED": "Approved", | ||||
|           "PENDING_REVIEW": "Pending Review", | ||||
|           "AVAILABLE_WITHOUT_REVIEW": "Available Without Review", | ||||
|           "REJECTED": "Rejected", | ||||
|           "DECLINED": "Declined" | ||||
|         }, | ||||
|         "MODES": { | ||||
|           "SANDBOX": "Sandbox", | ||||
|           "LIVE": "Live" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "SETTINGS": "Settings", | ||||
|     "FEATURES": { | ||||
|       "LABEL": "Features", | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Password", | ||||
|       "PLACEHOLDER": "Password", | ||||
|       "ERROR": "Password is too short.", | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character." | ||||
|       "IS_INVALID_PASSWORD": "Password should contain atleast 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character.", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirm password", | ||||
|       "PLACEHOLDER": "Confirm password", | ||||
|       "ERROR": "Password doesnot match." | ||||
|       "ERROR": "Passwords do not match." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registration Successfull", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Could not connect to Woot server. Please try again." | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Enviar", | ||||
|           "CANCEL": "Cancelar" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Nota privada: solo visible para ti y tu equipo", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Buscar", | ||||
|       "EMPTY_STATE": "No se encontraron resultados" | ||||
|     }, | ||||
|     "CLOSE": "Cerrar" | ||||
|     "CLOSE": "Cerrar", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Script de Messenger", | ||||
|       "MESSENGER_SUB_HEAD": "Coloca este botón dentro de tu etiqueta cuerpo", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agentes", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Añadir o quitar agentes de esta bandeja de entrada", | ||||
|       "AGENT_ASSIGNMENT": "Asignación de conversación", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Contraseña", | ||||
|       "PLACEHOLDER": "Contraseña", | ||||
|       "ERROR": "La contraseña es demasiado corta", | ||||
|       "IS_INVALID_PASSWORD": "La contraseña debe contener al menos 1 letra mayúscula, 1 letra minúscula, 1 número y 1 carácter especial" | ||||
|       "IS_INVALID_PASSWORD": "La contraseña debe contener al menos 1 letra mayúscula, 1 letra minúscula, 1 número y 1 carácter especial", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirmar contraseña", | ||||
|       "PLACEHOLDER": "Confirmar contraseña", | ||||
|       "ERROR": "La contraseña no coincide" | ||||
|       "ERROR": "Las contraseñas no coinciden." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Registro Exitoso", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "No se pudo conectar al servidor Woot, por favor inténtalo de nuevo más tarde" | ||||
|     }, | ||||
|     "SUBMIT": "Crear una cuenta", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "ارسال", | ||||
|           "CANCEL": "انصراف" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "یادداشت خصوصی: فقط برای شما و تیم شما قابل مشاهده است", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "جستجو", | ||||
|       "EMPTY_STATE": "نتیجهای یافت نشد" | ||||
|     }, | ||||
|     "CLOSE": "بستن" | ||||
|     "CLOSE": "بستن", | ||||
|     "BETA": "آزمایشی", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "اسکریپت ویجت", | ||||
|       "MESSENGER_SUB_HEAD": "این دکمه را در تگ body قرار دهید", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "ایجنت ها", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "اضافه کردن یا حذف کردن دسترسی ایجنت به صندوق ورودی", | ||||
|       "AGENT_ASSIGNMENT": "اختصاص گفتگو", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "رمز عبور", | ||||
|       "PLACEHOLDER": "رمز عبور", | ||||
|       "ERROR": "رمز عبور خیلی کوتاه است", | ||||
|       "IS_INVALID_PASSWORD": "رمز عبور باید شامل حداقل ۱ حرف بزرگ، ۱ حرف کوچک، ۱ عدد و ۱ کاراکتر خاص باشد" | ||||
|       "IS_INVALID_PASSWORD": "رمز عبور باید شامل حداقل ۱ حرف بزرگ، ۱ حرف کوچک، ۱ عدد و ۱ کاراکتر خاص باشد", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "تکرار رمز عبور", | ||||
|       "PLACEHOLDER": "تکرار رمز عبور", | ||||
|       "ERROR": "رمز عبور و تکرار رمز عبور یکسان نیستند" | ||||
|       "ERROR": "تکرار رمز عبور میبایست با رمز عبور یکسان باشد." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "ثبت نام با موفقیت انجام شد", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا بعدا امتحان کنید" | ||||
|     }, | ||||
|     "SUBMIT": "ایجاد حساب کاربری", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Lähetä", | ||||
|           "CANCEL": "Peruuta" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Yksityinen huomautus: Näkyy vain sinulle ja tiimillesi", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Etsi", | ||||
|       "EMPTY_STATE": "Tuloksia ei löytynyt" | ||||
|     }, | ||||
|     "CLOSE": "Sulje" | ||||
|     "CLOSE": "Sulje", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Messenger-skripti", | ||||
|       "MESSENGER_SUB_HEAD": "Aseta tämä painike body-tagiisi", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Edustajat", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Lisää tai poista edustajia tästä saapuneet-kansiosta", | ||||
|       "AGENT_ASSIGNMENT": "Conversation Assignment", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Salasana", | ||||
|       "PLACEHOLDER": "Salasana", | ||||
|       "ERROR": "Salasana on liian lyhyt", | ||||
|       "IS_INVALID_PASSWORD": "Salasanan tulee sisältää vähintään 1 iso kirjain, 1 pieni kirjain, 1 numero ja 1 erikoismerkki." | ||||
|       "IS_INVALID_PASSWORD": "Salasanan tulee sisältää vähintään 1 iso kirjain, 1 pieni kirjain, 1 numero ja 1 erikoismerkki.", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Vahvista salasana", | ||||
|       "PLACEHOLDER": "Vahvista salasana", | ||||
|       "ERROR": "Salasanat eivät täsmää" | ||||
|       "ERROR": "Salasanat eivät täsmää." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Rekisteröinti onnistui", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" | ||||
|     }, | ||||
|     "SUBMIT": "Create account", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Envoyer", | ||||
|           "CANCEL": "Annuler" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Note privée : uniquement visible par vous et votre équipe", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Rechercher", | ||||
|       "EMPTY_STATE": "Aucun résultat trouvé" | ||||
|     }, | ||||
|     "CLOSE": "Fermer" | ||||
|     "CLOSE": "Fermer", | ||||
|     "BETA": "Bêta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "Script du Widget Web", | ||||
|       "MESSENGER_SUB_HEAD": "Placez ce code avant la fermeture de votre balise body", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "Agents", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "Ajouter ou supprimer des agents de cette boîte de réception", | ||||
|       "AGENT_ASSIGNMENT": "Konversationsauftrag", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "Mot de passe", | ||||
|       "PLACEHOLDER": "Mot de passe", | ||||
|       "ERROR": "Le mot de passe est trop court", | ||||
|       "IS_INVALID_PASSWORD": "Le mot de passe doit contenir au moins 1 lettre majuscule, 1 lettre minuscule, 1 chiffre et 1 caractère spécial" | ||||
|       "IS_INVALID_PASSWORD": "Le mot de passe doit contenir au moins 1 lettre majuscule, 1 lettre minuscule, 1 chiffre et 1 caractère spécial", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "Confirmer le mot de passe", | ||||
|       "PLACEHOLDER": "Confirmer le mot de passe", | ||||
|       "ERROR": "Les mots de passe ne correspondent pas" | ||||
|       "ERROR": "Les mots de passe ne correspondent pas." | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "Inscription réussie", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "Impossible de se connecter au serveur Woot, veuillez réessayer plus tard" | ||||
|     }, | ||||
|     "SUBMIT": "Créer un compte", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "שלח", | ||||
|           "CANCEL": "ביטול" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "פתקים פרטיים: רק אתה והצוות שלך יכולים לראות", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "חפש", | ||||
|       "EMPTY_STATE": "לא נמצאו תוצאות" | ||||
|     }, | ||||
|     "CLOSE": "סגור" | ||||
|     "CLOSE": "סגור", | ||||
|     "BETA": "בטא", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -618,6 +618,11 @@ | ||||
|     "SETTINGS_POPUP": { | ||||
|       "MESSENGER_HEADING": "סקריפט מסנג'ר", | ||||
|       "MESSENGER_SUB_HEAD": "מקם את הכפתור הזה בתוך תג הגוף שלך", | ||||
|       "ALLOWED_DOMAINS": { | ||||
|         "TITLE": "Allowed Domains", | ||||
|         "SUBTITLE": "Add wildcard or regular domains separated by commas (leave blank to allow all), e.g. *.chatwoot.dev, chatwoot.com.", | ||||
|         "PLACEHOLDER": "Enter domains separated by commas (eg: *.chatwoot.dev, chatwoot.com)" | ||||
|       }, | ||||
|       "INBOX_AGENTS": "סוכנים", | ||||
|       "INBOX_AGENTS_SUB_TEXT": "הוסף או הסר נציגים מתיבת הדואר הנכנס הזו", | ||||
|       "AGENT_ASSIGNMENT": "שיוך שיחה", | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|       }, | ||||
|       "SUBMIT": "Continue with SSO", | ||||
|       "API": { | ||||
|         "ERROR_MESSAGE": "SSO authentication failed" | ||||
|         "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -27,15 +27,20 @@ | ||||
|       "LABEL": "סיסמה", | ||||
|       "PLACEHOLDER": "סיסמה", | ||||
|       "ERROR": "הסיסמה קצרה מדי", | ||||
|       "IS_INVALID_PASSWORD": "הסיסמה צריכה להכיל לפחות אות אחת גדולה, אות קטנה אחת, מספר אחד ותו מיוחד אחד" | ||||
|       "IS_INVALID_PASSWORD": "הסיסמה צריכה להכיל לפחות אות אחת גדולה, אות קטנה אחת, מספר אחד ותו מיוחד אחד", | ||||
|       "REQUIREMENTS_LENGTH": "At least 6 characters long", | ||||
|       "REQUIREMENTS_UPPERCASE": "At least one uppercase letter", | ||||
|       "REQUIREMENTS_LOWERCASE": "At least one lowercase letter", | ||||
|       "REQUIREMENTS_NUMBER": "At least one number", | ||||
|       "REQUIREMENTS_SPECIAL": "At least one special character" | ||||
|     }, | ||||
|     "CONFIRM_PASSWORD": { | ||||
|       "LABEL": "אמת סיסמה", | ||||
|       "PLACEHOLDER": "אמת סיסמה", | ||||
|       "ERROR": "סיסמה לא מתאימה" | ||||
|       "ERROR": "סיסמאות לא תואמות" | ||||
|     }, | ||||
|     "API": { | ||||
|       "SUCCESS_MESSAGE": "ההרשמה הצליחה", | ||||
|       "SUCCESS_MESSAGE": "Registration Successful", | ||||
|       "ERROR_MESSAGE": "לא ניתן להתחבר לשרת Woot, נסה שוב מאוחר יותר" | ||||
|     }, | ||||
|     "SUBMIT": "צור חשבון", | ||||
|   | ||||
| @@ -227,6 +227,13 @@ | ||||
|           "YES": "Send", | ||||
|           "CANCEL": "Cancel" | ||||
|         } | ||||
|       }, | ||||
|       "QUOTED_REPLY": { | ||||
|         "ENABLE_TOOLTIP": "Include quoted email thread", | ||||
|         "DISABLE_TOOLTIP": "Don't include quoted email thread", | ||||
|         "REMOVE_PREVIEW": "Remove quoted email thread", | ||||
|         "COLLAPSE": "Collapse preview", | ||||
|         "EXPAND": "Expand preview" | ||||
|       } | ||||
|     }, | ||||
|     "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
|       "PLACEHOLDER": "Search", | ||||
|       "EMPTY_STATE": "No results found" | ||||
|     }, | ||||
|     "CLOSE": "Close" | ||||
|     "CLOSE": "Close", | ||||
|     "BETA": "Beta", | ||||
|     "BETA_DESCRIPTION": "This feature is in beta and may change as we improve it." | ||||
|   } | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 Tanmay Deep Sharma
					Tanmay Deep Sharma