chore: Repalce message formatter mixin with useMessageFormatter [CW-3470] (#9986)

# Pull Request Template

## Description

Replaced the old messageFormatterMixin with a useMessageFormatter
composable
This commit is contained in:
Fayaz Ahmed
2024-08-27 08:06:51 +05:30
committed by GitHub
parent 32c25047c4
commit f82ec3b885
25 changed files with 287 additions and 114 deletions

View File

@@ -1,13 +1,12 @@
<script>
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { useAI } from 'dashboard/composables/useAI';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import AILoader from './AILoader.vue';
export default {
components: {
AILoader,
},
mixins: [messageFormatterMixin],
props: {
aiOption: {
type: String,
@@ -15,12 +14,9 @@ export default {
},
},
setup() {
const { formatMessage } = useMessageFormatter();
const { draftMessage, processEvent, recordAnalytics } = useAI();
return {
draftMessage,
processEvent,
recordAnalytics,
};
return { draftMessage, processEvent, recordAnalytics, formatMessage };
},
data() {
return {

View File

@@ -1,5 +1,5 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import BubbleActions from './bubble/Actions.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleFile from './bubble/File.vue';
@@ -39,7 +39,6 @@ export default {
InstagramStoryReply,
Spinner,
},
mixins: [messageFormatterMixin],
props: {
data: {
type: Object,
@@ -74,6 +73,12 @@ export default {
default: () => ({}),
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return {
showContextMenu: false,

View File

@@ -1,11 +1,10 @@
<script>
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
export default {
name: 'MessagePreview',
mixins: [messageFormatterMixin],
props: {
message: {
type: Object,
@@ -20,6 +19,12 @@ export default {
default: '',
},
},
setup() {
const { getPlainText } = useMessageFormatter();
return {
getPlainText,
};
},
computed: {
messageByAgent() {
const { message_type: messageType } = this.message;

View File

@@ -17,7 +17,6 @@ import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue';
import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { AUDIO_FORMATS } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
@@ -61,12 +60,7 @@ export default {
MessageSignatureMissingAlert,
ArticleSearchPopover,
},
mixins: [
inboxMixin,
messageFormatterMixin,
fileUploadMixin,
keyboardEventListenerMixins,
],
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
props: {
popoutReplyBox: {
type: Boolean,

View File

@@ -1,7 +1,7 @@
<script>
import { useAlert } from 'dashboard/composables';
import { mapGetters } from 'vuex';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import { conversationUrl, frontendURL } from '../../../helper/URLHelper';
@@ -18,7 +18,6 @@ export default {
TranslateModal,
MenuItem,
},
mixins: [messageFormatterMixin],
props: {
message: {
type: Object,
@@ -37,6 +36,12 @@ export default {
default: () => ({}),
},
},
setup() {
const { getPlainText } = useMessageFormatter();
return {
getPlainText,
};
},
data() {
return {
isCannedResponseModalOpen: false,

View File

@@ -1,15 +1,12 @@
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { dynamicTime } from 'shared/helpers/timeHelper';
export default {
components: {
Thumbnail,
},
mixins: [messageFormatterMixin],
props: {
id: {
type: Number,
@@ -28,6 +25,12 @@ export default {
default: 0,
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return {
showDeleteModal: false,

View File

@@ -1,12 +1,11 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import ReadMore from './ReadMore.vue';
export default {
components: {
ReadMore,
},
mixins: [messageFormatterMixin],
props: {
author: {
type: String,
@@ -21,6 +20,13 @@ export default {
default: '',
},
},
setup() {
const { formatMessage, highlightContent } = useMessageFormatter();
return {
formatMessage,
highlightContent,
};
},
data() {
return {
isOverflowing: false,

View File

@@ -1,6 +1,5 @@
<script>
import { mapGetters } from 'vuex';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import SwitchLayout from './SwitchLayout.vue';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
@@ -14,7 +13,6 @@ export default {
},
},
},
mixins: [messageFormatterMixin],
props: {
isOnExpandedLayout: {
type: Boolean,

View File

@@ -1,5 +1,5 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { mapGetters } from 'vuex';
import { useAccount } from 'dashboard/composables/useAccount';
@@ -7,12 +7,12 @@ import BillingItem from './components/BillingItem.vue';
export default {
components: { BillingItem },
mixins: [messageFormatterMixin],
setup() {
const { accountId } = useAccount();
const { formatMessage } = useMessageFormatter();
return {
accountId,
formatMessage,
};
},
computed: {

View File

@@ -1,7 +1,7 @@
<script>
import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue';
import InboxName from 'dashboard/components/widgets/InboxName.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { messageStamp } from 'shared/helpers/timeHelper';
export default {
@@ -9,7 +9,6 @@ export default {
UserAvatarWithName,
InboxName,
},
mixins: [messageFormatterMixin],
props: {
campaign: {
type: Object,
@@ -20,7 +19,12 @@ export default {
default: true,
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
computed: {
campaignStatus() {
if (this.isOngoingType) {

View File

@@ -2,7 +2,6 @@
import { mapGetters } from 'vuex';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import Integration from './Integration.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import SelectChannelWarning from './Slack/SelectChannelWarning.vue';
import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue';
import Spinner from 'shared/components/Spinner.vue';
@@ -13,7 +12,7 @@ export default {
SelectChannelWarning,
SlackIntegrationHelpText,
},
mixins: [globalConfigMixin, messageFormatterMixin],
mixins: [globalConfigMixin],
props: {
code: { type: String, default: '' },
},

View File

@@ -2,16 +2,22 @@
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
export default {
mixins: [globalConfigMixin, messageFormatterMixin],
mixins: [globalConfigMixin],
props: {
hasConnectedAChannel: {
type: Boolean,
default: true,
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return { selectedChannelId: '', availableChannels: [] };
},

View File

@@ -1,13 +1,18 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
export default {
mixins: [messageFormatterMixin],
props: {
selectedChannelName: {
type: String,
required: true,
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
};
</script>

View File

@@ -1,10 +1,9 @@
<script>
import { ref, computed, nextTick } from 'vue';
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
export default {
mixins: [messageFormatterMixin],
props: {
items: {
type: Array,
@@ -30,7 +29,7 @@ export default {
setup(props) {
const selectedIndex = ref(-1);
const portalSearchSuggestionsRef = ref(null);
const { highlightContent } = useMessageFormatter();
const adjustScroll = () => {
nextTick(() => {
portalSearchSuggestionsRef.value.scrollTop = 102 * selectedIndex.value;
@@ -53,6 +52,7 @@ export default {
selectedIndex,
portalSearchSuggestionsRef,
isSearchItemActive,
highlightContent,
};
},

View File

@@ -1,12 +1,11 @@
<script>
import ChatOption from 'shared/components/ChatOption.vue';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
export default {
components: {
ChatOption,
},
mixins: [messageFormatterMixin],
props: {
title: {
type: String,
@@ -25,6 +24,12 @@ export default {
default: false,
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
methods: {
isSelected(option) {
return this.selected === option.id;

View File

@@ -0,0 +1,79 @@
import { useMessageFormatter } from '../useMessageFormatter';
describe('useMessageFormatter', () => {
let messageFormatter;
beforeEach(() => {
messageFormatter = useMessageFormatter();
});
describe('formatMessage', () => {
it('should format a regular message correctly', () => {
const message = 'This is a [test](https://example.com) message';
const result = messageFormatter.formatMessage(message, false, false);
expect(result).toContain('<a href="https://example.com"');
expect(result).toContain('class="link"');
});
it('should format a tweet correctly', () => {
const message = '@user #hashtag';
const result = messageFormatter.formatMessage(message, true, false);
expect(result).toContain('<a href="http://twitter.com/user"');
expect(result).toContain('<a href="https://twitter.com/hashtag/hashtag"');
});
it('should not format mentions and hashtags for private notes', () => {
const message = '@user #hashtag';
const result = messageFormatter.formatMessage(message, false, true);
expect(result).not.toContain('<a href="http://twitter.com/user"');
expect(result).not.toContain(
'<a href="https://twitter.com/hashtag/hashtag"'
);
});
});
describe('truncateMessage', () => {
it('should not truncate short messages', () => {
const message = 'Short message';
const result = messageFormatter.truncateMessage(message);
expect(result).toBe(message);
});
it('should truncate long messages', () => {
const message = 'A'.repeat(150);
const result = messageFormatter.truncateMessage(message);
expect(result.length).toBe(100);
expect(result.endsWith('...')).toBe(true);
});
});
describe('highlightContent', () => {
it('should highlight search term in content', () => {
const content = 'This is a test message';
const searchTerm = 'test';
const highlightClass = 'highlight';
const result = messageFormatter.highlightContent(
content,
searchTerm,
highlightClass
);
expect(result.trim()).toBe(
'This is a <span class="highlight">test</span> message'
);
});
it('should handle special characters in search term', () => {
const content = 'This (message) contains [special] characters';
const searchTerm = '(message)';
const highlightClass = 'highlight';
const result = messageFormatter.highlightContent(
content,
searchTerm,
highlightClass
);
expect(result.trim()).toBe(
'This <span class="highlight">(message)</span> contains [special] characters'
);
});
});
});

View File

@@ -0,0 +1,82 @@
import MessageFormatter from '../helpers/MessageFormatter';
/**
* A composable providing utility functions for message formatting.
*
* @returns {Object} A set of functions for message formatting.
*/
export const useMessageFormatter = () => {
/**
* Formats a message based on specified conditions.
*
* @param {string} message - The message to be formatted.
* @param {boolean} isATweet - Whether the message is a tweet.
* @param {boolean} isAPrivateNote - Whether the message is a private note.
* @returns {string} - The formatted message.
*/
const formatMessage = (message, isATweet, isAPrivateNote) => {
const messageFormatter = new MessageFormatter(
message,
isATweet,
isAPrivateNote
);
return messageFormatter.formattedMessage;
};
/**
* Converts a message to plain text.
*
* @param {string} message - The message to be converted.
* @param {boolean} isATweet - Whether the message is a tweet.
* @returns {string} - The plain text message.
*/
const getPlainText = (message, isATweet) => {
const messageFormatter = new MessageFormatter(message, isATweet);
return messageFormatter.plainText;
};
/**
* Truncates a description to a maximum length of 100 characters.
*
* @param {string} [description=''] - The description to be truncated.
* @returns {string} - The truncated description.
*/
const truncateMessage = (description = '') => {
if (description.length < 100) {
return description;
}
return `${description.slice(0, 97)}...`;
};
/**
* Highlights occurrences of a search term within given content.
*
* @param {string} [content=''] - The content in which to search.
* @param {string} [searchTerm=''] - The term to search for.
* @param {string} [highlightClass=''] - The CSS class to apply to the highlighted term.
* @returns {string} - The content with highlighted terms.
*/
const highlightContent = (
content = '',
searchTerm = '',
highlightClass = ''
) => {
const plainTextContent = getPlainText(content);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return plainTextContent.replace(
new RegExp(`(${escapedSearchTerm})`, 'ig'),
`<span class="${highlightClass}">$1</span>`
);
};
return {
formatMessage,
getPlainText,
truncateMessage,
highlightContent,
};
};

View File

@@ -1,40 +0,0 @@
import MessageFormatter from '../helpers/MessageFormatter';
export default {
methods: {
formatMessage(message, isATweet, isAPrivateNote) {
const messageFormatter = new MessageFormatter(
message,
isATweet,
isAPrivateNote
);
return messageFormatter.formattedMessage;
},
getPlainText(message, isATweet) {
const messageFormatter = new MessageFormatter(message, isATweet);
return messageFormatter.plainText;
},
truncateMessage(description = '') {
if (description.length < 100) {
return description;
}
return `${description.slice(0, 97)}...`;
},
highlightContent(content = '', searchTerm = '', highlightClass = '') {
const plainTextContent = this.getPlainText(content);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
const escapedSearchTerm = searchTerm.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
return plainTextContent.replace(
new RegExp(`(${escapedSearchTerm})`, 'ig'),
`<span class="${highlightClass}">$1</span>`
);
},
},
};

View File

@@ -1,17 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import messageFormatterMixin from '../messageFormatterMixin';
describe('messageFormatterMixin', () => {
it('returns correct plain text', () => {
const Component = {
render() {},
mixins: [messageFormatterMixin],
};
const wrapper = shallowMount(Component);
const message =
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>';
expect(wrapper.vm.getPlainText(message)).toMatch(
'Chatwoot is an opensource tool. https://www.chatwoot.com'
);
});
});

View File

@@ -1,5 +1,5 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import PlaygroundHeader from '../../components/playground/Header.vue';
import UserMessage from '../../components/playground/UserMessage.vue';
import BotMessage from '../../components/playground/BotMessage.vue';
@@ -12,13 +12,18 @@ export default {
BotMessage,
TypingIndicator,
},
mixins: [messageFormatterMixin],
props: {
componentData: {
type: Object,
default: () => ({}),
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return { messages: [], messageContent: '', isWaiting: false };
},

View File

@@ -1,5 +1,5 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import ChatCard from 'shared/components/ChatCard.vue';
import ChatForm from 'shared/components/ChatForm.vue';
import ChatOptions from 'shared/components/ChatOptions.vue';
@@ -20,7 +20,7 @@ export default {
CustomerSatisfaction,
IntegrationCard,
},
mixins: [messageFormatterMixin, darkModeMixin],
mixins: [darkModeMixin],
props: {
message: { type: String, default: null },
contentType: { type: String, default: null },
@@ -31,6 +31,16 @@ export default {
default: () => {},
},
},
setup() {
const { formatMessage, getPlainText, truncateMessage, highlightContent } =
useMessageFormatter();
return {
formatMessage,
getPlainText,
truncateMessage,
highlightContent,
};
},
computed: {
isTemplate() {
return this.messageType === 3;

View File

@@ -5,7 +5,7 @@ import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import { isEmptyObject } from 'widget/helpers/utils';
import { getRegexp } from 'shared/helpers/Validators';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import routerMixin from 'widget/mixins/routerMixin';
import darkModeMixin from 'widget/mixins/darkModeMixin';
import configMixin from 'widget/mixins/configMixin';
@@ -15,13 +15,19 @@ export default {
CustomButton,
Spinner,
},
mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin],
mixins: [routerMixin, darkModeMixin, configMixin],
props: {
options: {
type: Object,
default: () => {},
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
data() {
return {
locale: this.$root.$i18n.locale,

View File

@@ -1,5 +1,5 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import configMixin from '../mixins/configMixin';
import { isEmptyObject } from 'widget/helpers/utils';
@@ -11,7 +11,7 @@ import darkModeMixin from 'widget/mixins/darkModeMixin';
export default {
name: 'UnreadMessage',
components: { Thumbnail },
mixins: [messageFormatterMixin, configMixin, darkModeMixin],
mixins: [configMixin, darkModeMixin],
props: {
message: {
type: String,
@@ -30,6 +30,12 @@ export default {
default: null,
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
computed: {
companyName() {
return `${this.$t('UNREAD_VIEW.COMPANY_FROM')} ${

View File

@@ -1,10 +1,9 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import { getContrastingTextColor } from '@chatwoot/utils';
export default {
name: 'UserMessageBubble',
mixins: [messageFormatterMixin],
props: {
message: {
type: String,
@@ -15,6 +14,12 @@ export default {
default: '',
},
},
setup() {
const { formatMessage } = useMessageFormatter();
return {
formatMessage,
};
},
computed: {
textColor() {
return getContrastingTextColor(this.widgetColor);

View File

@@ -1,5 +1,5 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import darkModeMixin from 'widget/mixins/darkModeMixin.js';
@@ -7,13 +7,19 @@ export default {
components: {
FluentIcon,
},
mixins: [messageFormatterMixin, darkModeMixin],
mixins: [darkModeMixin],
props: {
items: {
type: Array,
default: () => [],
},
},
setup() {
const { truncateMessage } = useMessageFormatter();
return {
truncateMessage,
};
},
};
</script>