mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	fix: Rendering on email without html content (#12561)
<img width="983" height="579" alt="image" src="https://github.com/user-attachments/assets/2972e8d9-5145-4958-8f66-9e84bd9c8c4b" />
This commit is contained in:
		| @@ -7,6 +7,7 @@ class Messages::MessageBuilder | |||||||
|     @private = params[:private] || false |     @private = params[:private] || false | ||||||
|     @conversation = conversation |     @conversation = conversation | ||||||
|     @user = user |     @user = user | ||||||
|  |     @account = conversation.account | ||||||
|     @message_type = params[:message_type] || 'outgoing' |     @message_type = params[:message_type] || 'outgoing' | ||||||
|     @attachments = params[:attachments] |     @attachments = params[:attachments] | ||||||
|     @automation_rule = content_attributes&.dig(:automation_rule_id) |     @automation_rule = content_attributes&.dig(:automation_rule_id) | ||||||
| @@ -20,7 +21,9 @@ class Messages::MessageBuilder | |||||||
|     @message = @conversation.messages.build(message_params) |     @message = @conversation.messages.build(message_params) | ||||||
|     process_attachments |     process_attachments | ||||||
|     process_emails |     process_emails | ||||||
|     process_email_content |     # 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.save! | ||||||
|     @message |     @message | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { allowedCssProperties } from 'lettersanitizer'; | |||||||
|  |  | ||||||
| import Icon from 'next/icon/Icon.vue'; | import Icon from 'next/icon/Icon.vue'; | ||||||
| import { EmailQuoteExtractor } from 'dashboard/helper/emailQuoteExtractor.js'; | 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 BaseBubble from 'next/message/bubbles/Base.vue'; | ||||||
| import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; | import AttachmentChips from 'next/message/chips/AttachmentChips.vue'; | ||||||
| import EmailMeta from './EmailMeta.vue'; | import EmailMeta from './EmailMeta.vue'; | ||||||
| @@ -46,6 +47,22 @@ const originalEmailHtml = computed( | |||||||
|     originalEmailText.value |     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) { | ||||||
|  |     return translationContent.value; | ||||||
|  |   } | ||||||
|  |   // Otherwise show original content | ||||||
|  |   return content.value; | ||||||
|  | }); | ||||||
|  |  | ||||||
| const textToShow = computed(() => { | const textToShow = computed(() => { | ||||||
|   // If translations exist and we're showing translations (not original) |   // If translations exist and we're showing translations (not original) | ||||||
|   if (hasTranslations.value && !renderOriginal.value) { |   if (hasTranslations.value && !renderOriginal.value) { | ||||||
| @@ -126,30 +143,37 @@ const handleSeeOriginal = () => { | |||||||
|             {{ $t('EMAIL_HEADER.EXPAND') }} |             {{ $t('EMAIL_HEADER.EXPAND') }} | ||||||
|           </button> |           </button> | ||||||
|         </div> |         </div> | ||||||
|         <Letter |         <FormattedContent | ||||||
|           v-if="showQuotedMessage" |           v-if="isOutgoing && content && !hasEmailContent" | ||||||
|           :key="`letter-quoted-${translationKeySuffix}`" |           class="text-n-slate-12" | ||||||
|           class-name="prose prose-bubble !max-w-none letter-render" |           :content="messageContent" | ||||||
|           :allowed-css-properties="[ |  | ||||||
|             ...allowedCssProperties, |  | ||||||
|             'transform', |  | ||||||
|             'transform-origin', |  | ||||||
|           ]" |  | ||||||
|           :html="fullHTML" |  | ||||||
|           :text="textToShow" |  | ||||||
|         /> |  | ||||||
|         <Letter |  | ||||||
|           v-else |  | ||||||
|           :key="`letter-unquoted-${translationKeySuffix}`" |  | ||||||
|           class-name="prose prose-bubble !max-w-none letter-render" |  | ||||||
|           :html="unquotedHTML" |  | ||||||
|           :allowed-css-properties="[ |  | ||||||
|             ...allowedCssProperties, |  | ||||||
|             'transform', |  | ||||||
|             'transform-origin', |  | ||||||
|           ]" |  | ||||||
|           :text="textToShow" |  | ||||||
|         /> |         /> | ||||||
|  |         <template v-else> | ||||||
|  |           <Letter | ||||||
|  |             v-if="showQuotedMessage" | ||||||
|  |             :key="`letter-quoted-${translationKeySuffix}`" | ||||||
|  |             class-name="prose prose-bubble !max-w-none letter-render" | ||||||
|  |             :allowed-css-properties="[ | ||||||
|  |               ...allowedCssProperties, | ||||||
|  |               'transform', | ||||||
|  |               'transform-origin', | ||||||
|  |             ]" | ||||||
|  |             :html="fullHTML" | ||||||
|  |             :text="textToShow" | ||||||
|  |           /> | ||||||
|  |           <Letter | ||||||
|  |             v-else | ||||||
|  |             :key="`letter-unquoted-${translationKeySuffix}`" | ||||||
|  |             class-name="prose prose-bubble !max-w-none letter-render" | ||||||
|  |             :html="unquotedHTML" | ||||||
|  |             :allowed-css-properties="[ | ||||||
|  |               ...allowedCssProperties, | ||||||
|  |               'transform', | ||||||
|  |               'transform-origin', | ||||||
|  |             ]" | ||||||
|  |             :text="textToShow" | ||||||
|  |           /> | ||||||
|  |         </template> | ||||||
|         <button |         <button | ||||||
|           v-if="hasQuotedMessage" |           v-if="hasQuotedMessage" | ||||||
|           class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2" |           class="text-n-slate-11 px-1 leading-none text-sm bg-n-alpha-black2 text-center flex items-center gap-1 mt-2" | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ const toggleExpand = () => { | |||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         v-dompurify-html="formattedQuotedEmailText" |         v-dompurify-html="formattedQuotedEmailText" | ||||||
|         class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer pr-8" |         class="w-full max-w-none break-words prose prose-sm dark:prose-invert cursor-pointer ltr:pr-8 rtl:pl-8" | ||||||
|         :class="{ |         :class="{ | ||||||
|           'line-clamp-1': !isExpanded, |           'line-clamp-1': !isExpanded, | ||||||
|           'max-h-60 overflow-y-auto': isExpanded, |           'max-h-60 overflow-y-auto': isExpanded, | ||||||
|   | |||||||
| @@ -4,7 +4,10 @@ import { | |||||||
|   getEmailSenderEmail, |   getEmailSenderEmail, | ||||||
|   getEmailDate, |   getEmailDate, | ||||||
|   formatQuotedEmailDate, |   formatQuotedEmailDate, | ||||||
|  |   getInboxEmail, | ||||||
|   buildQuotedEmailHeader, |   buildQuotedEmailHeader, | ||||||
|  |   buildQuotedEmailHeaderFromContact, | ||||||
|  |   buildQuotedEmailHeaderFromInbox, | ||||||
|   formatQuotedTextAsBlockquote, |   formatQuotedTextAsBlockquote, | ||||||
|   extractQuotedEmailText, |   extractQuotedEmailText, | ||||||
|   truncatePreviewText, |   truncatePreviewText, | ||||||
| @@ -131,7 +134,40 @@ describe('quotedEmailHelper', () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('buildQuotedEmailHeader', () => { |   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', () => { |     it('builds complete header with name and email', () => { | ||||||
|       const lastEmail = { |       const lastEmail = { | ||||||
|         sender: { name: 'John Doe', email: 'john@example.com' }, |         sender: { name: 'John Doe', email: 'john@example.com' }, | ||||||
| @@ -139,7 +175,7 @@ describe('quotedEmailHelper', () => { | |||||||
|           email: { date: '2024-01-15T10:30:00Z' }, |           email: { date: '2024-01-15T10:30:00Z' }, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|       const result = buildQuotedEmailHeader(lastEmail, {}); |       const result = buildQuotedEmailHeaderFromContact(lastEmail, {}); | ||||||
|       expect(result).toContain('John Doe'); |       expect(result).toContain('John Doe'); | ||||||
|       expect(result).toContain('john@example.com'); |       expect(result).toContain('john@example.com'); | ||||||
|       expect(result).toContain('wrote:'); |       expect(result).toContain('wrote:'); | ||||||
| @@ -152,14 +188,93 @@ describe('quotedEmailHelper', () => { | |||||||
|           email: { date: '2024-01-15T10:30:00Z' }, |           email: { date: '2024-01-15T10:30:00Z' }, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|       const result = buildQuotedEmailHeader(lastEmail, {}); |       const result = buildQuotedEmailHeaderFromContact(lastEmail, {}); | ||||||
|       expect(result).toContain('<john@example.com>'); |       expect(result).toContain('<john@example.com>'); | ||||||
|       expect(result).not.toContain('undefined'); |       expect(result).not.toContain('undefined'); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('returns empty string if missing required data', () => { |     it('returns empty string if missing required data', () => { | ||||||
|       expect(buildQuotedEmailHeader(null, {})).toBe(''); |       expect(buildQuotedEmailHeaderFromContact(null, {})).toBe(''); | ||||||
|       expect(buildQuotedEmailHeader({}, {})).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(''); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Shivam Mishra
					Shivam Mishra