mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	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:
		| @@ -1,13 +1,12 @@ | |||||||
| <script> | <script> | ||||||
|  | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import { useAI } from 'dashboard/composables/useAI'; | import { useAI } from 'dashboard/composables/useAI'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; |  | ||||||
| import AILoader from './AILoader.vue'; | import AILoader from './AILoader.vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     AILoader, |     AILoader, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     aiOption: { |     aiOption: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -15,12 +14,9 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   setup() { |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|     const { draftMessage, processEvent, recordAnalytics } = useAI(); |     const { draftMessage, processEvent, recordAnalytics } = useAI(); | ||||||
|     return { |     return { draftMessage, processEvent, recordAnalytics, formatMessage }; | ||||||
|       draftMessage, |  | ||||||
|       processEvent, |  | ||||||
|       recordAnalytics, |  | ||||||
|     }; |  | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import BubbleActions from './bubble/Actions.vue'; | import BubbleActions from './bubble/Actions.vue'; | ||||||
| import BubbleContact from './bubble/Contact.vue'; | import BubbleContact from './bubble/Contact.vue'; | ||||||
| import BubbleFile from './bubble/File.vue'; | import BubbleFile from './bubble/File.vue'; | ||||||
| @@ -39,7 +39,6 @@ export default { | |||||||
|     InstagramStoryReply, |     InstagramStoryReply, | ||||||
|     Spinner, |     Spinner, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     data: { |     data: { | ||||||
|       type: Object, |       type: Object, | ||||||
| @@ -74,6 +73,12 @@ export default { | |||||||
|       default: () => ({}), |       default: () => ({}), | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       showContextMenu: false, |       showContextMenu: false, | ||||||
|   | |||||||
| @@ -1,11 +1,10 @@ | |||||||
| <script> | <script> | ||||||
| import { MESSAGE_TYPE } from 'widget/helpers/constants'; | 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'; | import { ATTACHMENT_ICONS } from 'shared/constants/messages'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'MessagePreview', |   name: 'MessagePreview', | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     message: { |     message: { | ||||||
|       type: Object, |       type: Object, | ||||||
| @@ -20,6 +19,12 @@ export default { | |||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { getPlainText } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       getPlainText, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     messageByAgent() { |     messageByAgent() { | ||||||
|       const { message_type: messageType } = this.message; |       const { message_type: messageType } = this.message; | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ import Banner from 'dashboard/components/ui/Banner.vue'; | |||||||
| import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; | import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; | ||||||
| import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; | import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor.vue'; | ||||||
| import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue'; | import WootAudioRecorder from 'dashboard/components/widgets/WootWriter/AudioRecorder.vue'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; |  | ||||||
| import { AUDIO_FORMATS } from 'shared/constants/messages'; | import { AUDIO_FORMATS } from 'shared/constants/messages'; | ||||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
| import { | import { | ||||||
| @@ -61,12 +60,7 @@ export default { | |||||||
|     MessageSignatureMissingAlert, |     MessageSignatureMissingAlert, | ||||||
|     ArticleSearchPopover, |     ArticleSearchPopover, | ||||||
|   }, |   }, | ||||||
|   mixins: [ |   mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins], | ||||||
|     inboxMixin, |  | ||||||
|     messageFormatterMixin, |  | ||||||
|     fileUploadMixin, |  | ||||||
|     keyboardEventListenerMixins, |  | ||||||
|   ], |  | ||||||
|   props: { |   props: { | ||||||
|     popoutReplyBox: { |     popoutReplyBox: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <script> | <script> | ||||||
| import { useAlert } from 'dashboard/composables'; | import { useAlert } from 'dashboard/composables'; | ||||||
| import { mapGetters } from 'vuex'; | 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 AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue'; | ||||||
| import { copyTextToClipboard } from 'shared/helpers/clipboard'; | import { copyTextToClipboard } from 'shared/helpers/clipboard'; | ||||||
| import { conversationUrl, frontendURL } from '../../../helper/URLHelper'; | import { conversationUrl, frontendURL } from '../../../helper/URLHelper'; | ||||||
| @@ -18,7 +18,6 @@ export default { | |||||||
|     TranslateModal, |     TranslateModal, | ||||||
|     MenuItem, |     MenuItem, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     message: { |     message: { | ||||||
|       type: Object, |       type: Object, | ||||||
| @@ -37,6 +36,12 @@ export default { | |||||||
|       default: () => ({}), |       default: () => ({}), | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { getPlainText } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       getPlainText, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       isCannedResponseModalOpen: false, |       isCannedResponseModalOpen: false, | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| <script> | <script> | ||||||
| import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; | 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'; | import { dynamicTime } from 'shared/helpers/timeHelper'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     Thumbnail, |     Thumbnail, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|  |  | ||||||
|   props: { |   props: { | ||||||
|     id: { |     id: { | ||||||
|       type: Number, |       type: Number, | ||||||
| @@ -28,6 +25,12 @@ export default { | |||||||
|       default: 0, |       default: 0, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       showDeleteModal: false, |       showDeleteModal: false, | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import ReadMore from './ReadMore.vue'; | import ReadMore from './ReadMore.vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     ReadMore, |     ReadMore, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     author: { |     author: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -21,6 +20,13 @@ export default { | |||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage, highlightContent } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |       highlightContent, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       isOverflowing: false, |       isOverflowing: false, | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; |  | ||||||
| import SwitchLayout from './SwitchLayout.vue'; | import SwitchLayout from './SwitchLayout.vue'; | ||||||
| import { frontendURL } from 'dashboard/helper/URLHelper'; | import { frontendURL } from 'dashboard/helper/URLHelper'; | ||||||
| export default { | export default { | ||||||
| @@ -14,7 +13,6 @@ export default { | |||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     isOnExpandedLayout: { |     isOnExpandedLayout: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
|  |  | ||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import { useAccount } from 'dashboard/composables/useAccount'; | import { useAccount } from 'dashboard/composables/useAccount'; | ||||||
| @@ -7,12 +7,12 @@ import BillingItem from './components/BillingItem.vue'; | |||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { BillingItem }, |   components: { BillingItem }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   setup() { |   setup() { | ||||||
|     const { accountId } = useAccount(); |     const { accountId } = useAccount(); | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|     return { |     return { | ||||||
|       accountId, |       accountId, | ||||||
|  |       formatMessage, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <script> | <script> | ||||||
| import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue'; | import UserAvatarWithName from 'dashboard/components/widgets/UserAvatarWithName.vue'; | ||||||
| import InboxName from 'dashboard/components/widgets/InboxName.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'; | import { messageStamp } from 'shared/helpers/timeHelper'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
| @@ -9,7 +9,6 @@ export default { | |||||||
|     UserAvatarWithName, |     UserAvatarWithName, | ||||||
|     InboxName, |     InboxName, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     campaign: { |     campaign: { | ||||||
|       type: Object, |       type: Object, | ||||||
| @@ -20,7 +19,12 @@ export default { | |||||||
|       default: true, |       default: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     campaignStatus() { |     campaignStatus() { | ||||||
|       if (this.isOngoingType) { |       if (this.isOngoingType) { | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ | |||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | ||||||
| import Integration from './Integration.vue'; | import Integration from './Integration.vue'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; |  | ||||||
| import SelectChannelWarning from './Slack/SelectChannelWarning.vue'; | import SelectChannelWarning from './Slack/SelectChannelWarning.vue'; | ||||||
| import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue'; | import SlackIntegrationHelpText from './Slack/SlackIntegrationHelpText.vue'; | ||||||
| import Spinner from 'shared/components/Spinner.vue'; | import Spinner from 'shared/components/Spinner.vue'; | ||||||
| @@ -13,7 +12,7 @@ export default { | |||||||
|     SelectChannelWarning, |     SelectChannelWarning, | ||||||
|     SlackIntegrationHelpText, |     SlackIntegrationHelpText, | ||||||
|   }, |   }, | ||||||
|   mixins: [globalConfigMixin, messageFormatterMixin], |   mixins: [globalConfigMixin], | ||||||
|   props: { |   props: { | ||||||
|     code: { type: String, default: '' }, |     code: { type: String, default: '' }, | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -2,16 +2,22 @@ | |||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import { useAlert } from 'dashboard/composables'; | import { useAlert } from 'dashboard/composables'; | ||||||
| import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | import globalConfigMixin from 'shared/mixins/globalConfigMixin'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   mixins: [globalConfigMixin, messageFormatterMixin], |   mixins: [globalConfigMixin], | ||||||
|   props: { |   props: { | ||||||
|     hasConnectedAChannel: { |     hasConnectedAChannel: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: true, |       default: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { selectedChannelId: '', availableChannels: [] }; |     return { selectedChannelId: '', availableChannels: [] }; | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -1,13 +1,18 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| export default { | export default { | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     selectedChannelName: { |     selectedChannelName: { | ||||||
|       type: String, |       type: String, | ||||||
|       required: true, |       required: true, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,9 @@ | |||||||
| <script> | <script> | ||||||
| import { ref, computed, nextTick } from 'vue'; | import { ref, computed, nextTick } from 'vue'; | ||||||
| import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList'; | import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     items: { |     items: { | ||||||
|       type: Array, |       type: Array, | ||||||
| @@ -30,7 +29,7 @@ export default { | |||||||
|   setup(props) { |   setup(props) { | ||||||
|     const selectedIndex = ref(-1); |     const selectedIndex = ref(-1); | ||||||
|     const portalSearchSuggestionsRef = ref(null); |     const portalSearchSuggestionsRef = ref(null); | ||||||
|  |     const { highlightContent } = useMessageFormatter(); | ||||||
|     const adjustScroll = () => { |     const adjustScroll = () => { | ||||||
|       nextTick(() => { |       nextTick(() => { | ||||||
|         portalSearchSuggestionsRef.value.scrollTop = 102 * selectedIndex.value; |         portalSearchSuggestionsRef.value.scrollTop = 102 * selectedIndex.value; | ||||||
| @@ -53,6 +52,7 @@ export default { | |||||||
|       selectedIndex, |       selectedIndex, | ||||||
|       portalSearchSuggestionsRef, |       portalSearchSuggestionsRef, | ||||||
|       isSearchItemActive, |       isSearchItemActive, | ||||||
|  |       highlightContent, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,11 @@ | |||||||
| <script> | <script> | ||||||
| import ChatOption from 'shared/components/ChatOption.vue'; | import ChatOption from 'shared/components/ChatOption.vue'; | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     ChatOption, |     ChatOption, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     title: { |     title: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -25,6 +24,12 @@ export default { | |||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     isSelected(option) { |     isSelected(option) { | ||||||
|       return this.selected === option.id; |       return this.selected === option.id; | ||||||
|   | |||||||
| @@ -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' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										82
									
								
								app/javascript/shared/composables/useMessageFormatter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								app/javascript/shared/composables/useMessageFormatter.js
									
									
									
									
									
										Normal 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, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
| @@ -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>` |  | ||||||
|       ); |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| @@ -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' |  | ||||||
|     ); |  | ||||||
|   }); |  | ||||||
| }); |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import PlaygroundHeader from '../../components/playground/Header.vue'; | import PlaygroundHeader from '../../components/playground/Header.vue'; | ||||||
| import UserMessage from '../../components/playground/UserMessage.vue'; | import UserMessage from '../../components/playground/UserMessage.vue'; | ||||||
| import BotMessage from '../../components/playground/BotMessage.vue'; | import BotMessage from '../../components/playground/BotMessage.vue'; | ||||||
| @@ -12,13 +12,18 @@ export default { | |||||||
|     BotMessage, |     BotMessage, | ||||||
|     TypingIndicator, |     TypingIndicator, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     componentData: { |     componentData: { | ||||||
|       type: Object, |       type: Object, | ||||||
|       default: () => ({}), |       default: () => ({}), | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { messages: [], messageContent: '', isWaiting: false }; |     return { messages: [], messageContent: '', isWaiting: false }; | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import ChatCard from 'shared/components/ChatCard.vue'; | import ChatCard from 'shared/components/ChatCard.vue'; | ||||||
| import ChatForm from 'shared/components/ChatForm.vue'; | import ChatForm from 'shared/components/ChatForm.vue'; | ||||||
| import ChatOptions from 'shared/components/ChatOptions.vue'; | import ChatOptions from 'shared/components/ChatOptions.vue'; | ||||||
| @@ -20,7 +20,7 @@ export default { | |||||||
|     CustomerSatisfaction, |     CustomerSatisfaction, | ||||||
|     IntegrationCard, |     IntegrationCard, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin, darkModeMixin], |   mixins: [darkModeMixin], | ||||||
|   props: { |   props: { | ||||||
|     message: { type: String, default: null }, |     message: { type: String, default: null }, | ||||||
|     contentType: { type: String, default: null }, |     contentType: { type: String, default: null }, | ||||||
| @@ -31,6 +31,16 @@ export default { | |||||||
|       default: () => {}, |       default: () => {}, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage, getPlainText, truncateMessage, highlightContent } = | ||||||
|  |       useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |       getPlainText, | ||||||
|  |       truncateMessage, | ||||||
|  |       highlightContent, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     isTemplate() { |     isTemplate() { | ||||||
|       return this.messageType === 3; |       return this.messageType === 3; | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import { mapGetters } from 'vuex'; | |||||||
| import { getContrastingTextColor } from '@chatwoot/utils'; | import { getContrastingTextColor } from '@chatwoot/utils'; | ||||||
| import { isEmptyObject } from 'widget/helpers/utils'; | import { isEmptyObject } from 'widget/helpers/utils'; | ||||||
| import { getRegexp } from 'shared/helpers/Validators'; | 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 routerMixin from 'widget/mixins/routerMixin'; | ||||||
| import darkModeMixin from 'widget/mixins/darkModeMixin'; | import darkModeMixin from 'widget/mixins/darkModeMixin'; | ||||||
| import configMixin from 'widget/mixins/configMixin'; | import configMixin from 'widget/mixins/configMixin'; | ||||||
| @@ -15,13 +15,19 @@ export default { | |||||||
|     CustomButton, |     CustomButton, | ||||||
|     Spinner, |     Spinner, | ||||||
|   }, |   }, | ||||||
|   mixins: [routerMixin, darkModeMixin, messageFormatterMixin, configMixin], |   mixins: [routerMixin, darkModeMixin, configMixin], | ||||||
|   props: { |   props: { | ||||||
|     options: { |     options: { | ||||||
|       type: Object, |       type: Object, | ||||||
|       default: () => {}, |       default: () => {}, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       locale: this.$root.$i18n.locale, |       locale: this.$root.$i18n.locale, | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; | import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; | ||||||
| import configMixin from '../mixins/configMixin'; | import configMixin from '../mixins/configMixin'; | ||||||
| import { isEmptyObject } from 'widget/helpers/utils'; | import { isEmptyObject } from 'widget/helpers/utils'; | ||||||
| @@ -11,7 +11,7 @@ import darkModeMixin from 'widget/mixins/darkModeMixin'; | |||||||
| export default { | export default { | ||||||
|   name: 'UnreadMessage', |   name: 'UnreadMessage', | ||||||
|   components: { Thumbnail }, |   components: { Thumbnail }, | ||||||
|   mixins: [messageFormatterMixin, configMixin, darkModeMixin], |   mixins: [configMixin, darkModeMixin], | ||||||
|   props: { |   props: { | ||||||
|     message: { |     message: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -30,6 +30,12 @@ export default { | |||||||
|       default: null, |       default: null, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     companyName() { |     companyName() { | ||||||
|       return `${this.$t('UNREAD_VIEW.COMPANY_FROM')} ${ |       return `${this.$t('UNREAD_VIEW.COMPANY_FROM')} ${ | ||||||
|   | |||||||
| @@ -1,10 +1,9 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import { getContrastingTextColor } from '@chatwoot/utils'; | import { getContrastingTextColor } from '@chatwoot/utils'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'UserMessageBubble', |   name: 'UserMessageBubble', | ||||||
|   mixins: [messageFormatterMixin], |  | ||||||
|   props: { |   props: { | ||||||
|     message: { |     message: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -15,6 +14,12 @@ export default { | |||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { formatMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       formatMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     textColor() { |     textColor() { | ||||||
|       return getContrastingTextColor(this.widgetColor); |       return getContrastingTextColor(this.widgetColor); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script> | <script> | ||||||
| import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; | ||||||
| import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | import FluentIcon from 'shared/components/FluentIcon/Index.vue'; | ||||||
| import darkModeMixin from 'widget/mixins/darkModeMixin.js'; | import darkModeMixin from 'widget/mixins/darkModeMixin.js'; | ||||||
|  |  | ||||||
| @@ -7,13 +7,19 @@ export default { | |||||||
|   components: { |   components: { | ||||||
|     FluentIcon, |     FluentIcon, | ||||||
|   }, |   }, | ||||||
|   mixins: [messageFormatterMixin, darkModeMixin], |   mixins: [darkModeMixin], | ||||||
|   props: { |   props: { | ||||||
|     items: { |     items: { | ||||||
|       type: Array, |       type: Array, | ||||||
|       default: () => [], |       default: () => [], | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   setup() { | ||||||
|  |     const { truncateMessage } = useMessageFormatter(); | ||||||
|  |     return { | ||||||
|  |       truncateMessage, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Fayaz Ahmed
					Fayaz Ahmed