mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 18:47:51 +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