mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
feat: allow quoted email thread in reply (#12545)
This PR adds the ability to include the thread history as a quoted text ## Preview https://github.com/user-attachments/assets/c96a85e5-8ac8-4021-86ca-57509b4eea9f
This commit is contained in:
@@ -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 pr-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"
|
||||
|
||||
Reference in New Issue
Block a user