mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: Add support for markdown in messages (#1642)
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
		 Nithin David Thomas
					Nithin David Thomas
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							5adbc84e0c
						
					
				
				
					commit
					a5c3c4301c
				
			| @@ -3,7 +3,7 @@ | |||||||
|   @include margin($zero); |   @include margin($zero); | ||||||
|   background: $color-woot; |   background: $color-woot; | ||||||
|   border-radius: $space-one; |   border-radius: $space-one; | ||||||
|   color: $color-white; |   color: var(--white); | ||||||
|   font-size: $font-size-small; |   font-size: $font-size-small; | ||||||
|   font-weight: $font-weight-normal; |   font-weight: $font-weight-normal; | ||||||
|   position: relative; |   position: relative; | ||||||
| @@ -11,9 +11,8 @@ | |||||||
|   .message-text__wrap { |   .message-text__wrap { | ||||||
|     position: relative; |     position: relative; | ||||||
|  |  | ||||||
|  |  | ||||||
|     .link { |     .link { | ||||||
|       color: $color-white; |       color: var(--white); | ||||||
|       text-decoration: underline; |       text-decoration: underline; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -88,8 +87,6 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   .content-box { |   .content-box { | ||||||
|     text-align: center; |     text-align: center; | ||||||
|   } |   } | ||||||
| @@ -138,7 +135,6 @@ | |||||||
|   @include flex-weight(1); |   @include flex-weight(1); | ||||||
|   @include margin($zero); |   @include margin($zero); | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   // Firefox flexbox fix |  | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   overflow-y: auto; |   overflow-y: auto; | ||||||
|   padding-bottom: var(--space-normal); |   padding-bottom: var(--space-normal); | ||||||
| @@ -164,7 +160,7 @@ | |||||||
|       @include elegant-card; |       @include elegant-card; | ||||||
|       @include round-corner; |       @include round-corner; | ||||||
|       background: $color-woot; |       background: $color-woot; | ||||||
|       color: $color-white; |       color: var(--white); | ||||||
|       font-size: $font-size-mini; |       font-size: $font-size-mini; | ||||||
|       font-weight: $font-weight-medium; |       font-weight: $font-weight-medium; | ||||||
|       margin: $space-one auto; |       margin: $space-one auto; | ||||||
| @@ -215,6 +211,7 @@ | |||||||
|           color: $color-primary-dark; |           color: $color-primary-dark; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     +.right { |     +.right { | ||||||
| @@ -303,6 +300,12 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .activity-wrap .message-text__wrap { | ||||||
|  |   .text-content p { | ||||||
|  |     margin-bottom: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .conversation-footer { | .conversation-footer { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
| @@ -320,7 +323,7 @@ | |||||||
|   .typing-indicator { |   .typing-indicator { | ||||||
|     @include elegant-card; |     @include elegant-card; | ||||||
|     @include round-corner; |     @include round-corner; | ||||||
|     background: $color-white; |     background: var(--white); | ||||||
|     color: $color-light-gray; |     color: $color-light-gray; | ||||||
|     font-size: $font-size-mini; |     font-size: $font-size-mini; | ||||||
|     font-weight: $font-weight-bold; |     font-weight: $font-weight-bold; | ||||||
| @@ -333,3 +336,65 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .left .bubble .text-content { | ||||||
|  |  | ||||||
|  |   h1, | ||||||
|  |   h2, | ||||||
|  |   h3, | ||||||
|  |   h4, | ||||||
|  |   h5, | ||||||
|  |   h6 { | ||||||
|  |     color: var(--color-body); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   a { | ||||||
|  |     color: var(--color-woot); | ||||||
|  |     text-decoration: underline; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   blockquote { | ||||||
|  |     border-left-color: var(--s-300); | ||||||
|  |  | ||||||
|  |     p { | ||||||
|  |       color: var(--s-300); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   p:last-child { | ||||||
|  |     margin-bottom: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .right .bubble .text-content { | ||||||
|  |  | ||||||
|  |   h1, | ||||||
|  |   h2, | ||||||
|  |   h3, | ||||||
|  |   h4, | ||||||
|  |   h5, | ||||||
|  |   h6 { | ||||||
|  |     color: var(--white); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   a { | ||||||
|  |     color: var(--white); | ||||||
|  |     text-decoration: underline; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   blockquote { | ||||||
|  |     border-left-color: var(--w-100); | ||||||
|  |  | ||||||
|  |     p { | ||||||
|  |       color: var(--w-100); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pre code { | ||||||
|  |     background: var(--color-background); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   p:last-child { | ||||||
|  |     margin-bottom: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
|       <p v-if="lastMessageInChat" class="conversation--message"> |       <p v-if="lastMessageInChat" class="conversation--message"> | ||||||
|         <i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i> |         <i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i> | ||||||
|         <span v-if="lastMessageInChat.content"> |         <span v-if="lastMessageInChat.content"> | ||||||
|           {{ lastMessageInChat.content }} |           {{ parsedLastMessage }} | ||||||
|         </span> |         </span> | ||||||
|         <span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span> |         <span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span> | ||||||
|         <span v-else> |         <span v-else> | ||||||
| @@ -47,6 +47,7 @@ | |||||||
| <script> | <script> | ||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import { MESSAGE_TYPE } from 'widget/helpers/constants'; | import { MESSAGE_TYPE } from 'widget/helpers/constants'; | ||||||
|  | import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | ||||||
|  |  | ||||||
| import Thumbnail from '../Thumbnail'; | import Thumbnail from '../Thumbnail'; | ||||||
| import conversationMixin from '../../../mixins/conversations'; | import conversationMixin from '../../../mixins/conversations'; | ||||||
| @@ -59,7 +60,7 @@ export default { | |||||||
|     Thumbnail, |     Thumbnail, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   mixins: [timeMixin, conversationMixin], |   mixins: [timeMixin, conversationMixin, messageFormatterMixin], | ||||||
|   props: { |   props: { | ||||||
|     activeLabel: { |     activeLabel: { | ||||||
|       type: String, |       type: String, | ||||||
| @@ -129,6 +130,10 @@ export default { | |||||||
|       const { message_type: messageType } = lastMessage; |       const { message_type: messageType } = lastMessage; | ||||||
|       return messageType === MESSAGE_TYPE.OUTGOING; |       return messageType === MESSAGE_TYPE.OUTGOING; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  |     parsedLastMessage() { | ||||||
|  |       return this.getPlainText(this.lastMessageInChat.content); | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   methods: { |   methods: { | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="message-text__wrap"> |   <div class="message-text__wrap"> | ||||||
|     <span v-html="message"></span> |     <div class="text-content" v-html="message"></div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -57,6 +57,7 @@ | |||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
| import { frontendURL, conversationUrl } from '../../../../helper/URLHelper'; | import { frontendURL, conversationUrl } from '../../../../helper/URLHelper'; | ||||||
| import timeMixin from '../../../../mixins/time'; | import timeMixin from '../../../../mixins/time'; | ||||||
|  | import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   directives: { |   directives: { | ||||||
| @@ -66,7 +67,7 @@ export default { | |||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   mixins: [timeMixin], |   mixins: [timeMixin, messageFormatterMixin], | ||||||
|   props: { |   props: { | ||||||
|     show: { |     show: { | ||||||
|       type: Boolean, |       type: Boolean, | ||||||
| @@ -107,7 +108,8 @@ export default { | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     prepareContent(content = '') { |     prepareContent(content = '') { | ||||||
|       return content.replace( |       const plainTextContent = this.getPlainText(content); | ||||||
|  |       return plainTextContent.replace( | ||||||
|         new RegExp(`(${this.searchTerm})`, 'ig'), |         new RegExp(`(${this.searchTerm})`, 'ig'), | ||||||
|         '<span class="searchkey--highlight">$1</span>' |         '<span class="searchkey--highlight">$1</span>' | ||||||
|       ); |       ); | ||||||
|   | |||||||
| @@ -1,4 +1,7 @@ | |||||||
|  | import marked from 'marked'; | ||||||
|  | import DOMPurify from 'dompurify'; | ||||||
| import { escapeHtml } from './HTMLSanitizer'; | import { escapeHtml } from './HTMLSanitizer'; | ||||||
|  |  | ||||||
| const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g; | const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g; | ||||||
| const TWITTER_USERNAME_REPLACEMENT = | const TWITTER_USERNAME_REPLACEMENT = | ||||||
|   '$1<a href="http://twitter.com/$2" target="_blank" rel="noreferrer nofollow noopener">@$2</a>'; |   '$1<a href="http://twitter.com/$2" target="_blank" rel="noreferrer nofollow noopener">@$2</a>'; | ||||||
| @@ -9,41 +12,49 @@ const TWITTER_HASH_REPLACEMENT = | |||||||
|  |  | ||||||
| class MessageFormatter { | class MessageFormatter { | ||||||
|   constructor(message, isATweet = false) { |   constructor(message, isATweet = false) { | ||||||
|     this.message = escapeHtml(message || '') || ''; |     this.message = DOMPurify.sanitize(escapeHtml(message) || ''); | ||||||
|     this.isATweet = isATweet; |     this.isATweet = isATweet; | ||||||
|  |     this.marked = marked; | ||||||
|  |  | ||||||
|  |     const renderer = { | ||||||
|  |       heading(text) { | ||||||
|  |         return `<strong>${text}</strong>`; | ||||||
|  |       }, | ||||||
|  |       link(url, title, text) { | ||||||
|  |         return `<a rel="noreferrer noopener nofollow" href="${url}" class="link" title="${title || | ||||||
|  |           ''}" target="_blank">${text}</a>`; | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     this.marked.use({ renderer }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   formatMessage() { |   formatMessage() { | ||||||
|     const linkifiedMessage = this.linkify(); |  | ||||||
|     const messageWithNextLines = linkifiedMessage.replace(/\n/g, '<br>'); |  | ||||||
|     if (this.isATweet) { |     if (this.isATweet) { | ||||||
|       const messageWithUserName = messageWithNextLines.replace( |       const withUserName = this.message.replace( | ||||||
|         TWITTER_USERNAME_REGEX, |         TWITTER_USERNAME_REGEX, | ||||||
|         TWITTER_USERNAME_REPLACEMENT |         TWITTER_USERNAME_REPLACEMENT | ||||||
|       ); |       ); | ||||||
|       return messageWithUserName.replace( |       const withHash = withUserName.replace( | ||||||
|         TWITTER_HASH_REGEX, |         TWITTER_HASH_REGEX, | ||||||
|         TWITTER_HASH_REPLACEMENT |         TWITTER_HASH_REPLACEMENT | ||||||
|       ); |       ); | ||||||
|  |       const markedDownOutput = marked(withHash); | ||||||
|  |       return markedDownOutput; | ||||||
|     } |     } | ||||||
|     return messageWithNextLines; |     return marked(this.message); | ||||||
|   } |  | ||||||
|  |  | ||||||
|   linkify() { |  | ||||||
|     if (!this.message) { |  | ||||||
|       return ''; |  | ||||||
|     } |  | ||||||
|     const urlRegex = /(https?:\/\/[^\s]+)/g; |  | ||||||
|     return this.message.replace( |  | ||||||
|       urlRegex, |  | ||||||
|       url => |  | ||||||
|         `<a rel="noreferrer noopener nofollow" href="${url}" class="link" target="_blank">${url}</a>` |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get formattedMessage() { |   get formattedMessage() { | ||||||
|     return this.formatMessage(); |     return this.formatMessage(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get plainText() { | ||||||
|  |     const strippedOutHtml = new DOMParser().parseFromString( | ||||||
|  |       this.formattedMessage, | ||||||
|  |       'text/html' | ||||||
|  |     ); | ||||||
|  |     return strippedOutHtml.body.textContent || ''; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export default MessageFormatter; | export default MessageFormatter; | ||||||
|   | |||||||
| @@ -4,9 +4,25 @@ describe('#MessageFormatter', () => { | |||||||
|   describe('content with links', () => { |   describe('content with links', () => { | ||||||
|     it('should format correctly', () => { |     it('should format correctly', () => { | ||||||
|       const message = |       const message = | ||||||
|         'Chatwoot is an opensource tool\nSee more at https://www.chatwoot.com'; |         'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)'; | ||||||
|       expect(new MessageFormatter(message).formattedMessage).toEqual( |       expect(new MessageFormatter(message).formattedMessage).toMatch( | ||||||
|         'Chatwoot is an opensource tool<br>See more at <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" target="_blank">https://www.chatwoot.com</a>' |         '<p>Chatwoot is an opensource tool. <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" title="" target="_blank">Chatwoot</a></p>' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |     it('should format correctly', () => { | ||||||
|  |       const message = | ||||||
|  |         'Chatwoot is an opensource tool. https://www.chatwoot.com'; | ||||||
|  |       expect(new MessageFormatter(message).formattedMessage).toMatch( | ||||||
|  |         '<p>Chatwoot is an opensource tool. <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" title="" target="_blank">https://www.chatwoot.com</a></p>' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('parses heading to strong', () => { | ||||||
|  |     it('should format correctly', () => { | ||||||
|  |       const message = '### opensource \n ## tool'; | ||||||
|  |       expect(new MessageFormatter(message).formattedMessage).toMatch( | ||||||
|  |         '<strong>opensource</strong><strong>tool</strong>' | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @@ -14,21 +30,31 @@ describe('#MessageFormatter', () => { | |||||||
|   describe('tweets', () => { |   describe('tweets', () => { | ||||||
|     it('should return the same string if not tags or @mentions', () => { |     it('should return the same string if not tags or @mentions', () => { | ||||||
|       const message = 'Chatwoot is an opensource tool'; |       const message = 'Chatwoot is an opensource tool'; | ||||||
|       expect(new MessageFormatter(message).formattedMessage).toEqual(message); |       expect(new MessageFormatter(message).formattedMessage).toMatch(message); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should add links to @mentions', () => { |     it('should add links to @mentions', () => { | ||||||
|       const message = |       const message = | ||||||
|         '@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername'; |         '@chatwootapp is an opensource tool thanks @longnonexistenttwitterusername'; | ||||||
|       expect(new MessageFormatter(message, true).formattedMessage).toEqual( |       expect(new MessageFormatter(message, true).formattedMessage).toMatch( | ||||||
|         '<a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername' |         '<p><a href="http://twitter.com/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">@chatwootapp</a> is an opensource tool thanks @longnonexistenttwitterusername</p>' | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should add links to #tags', () => { |     it('should add links to #tags', () => { | ||||||
|       const message = '#chatwootapp is an opensource tool'; |       const message = '#chatwootapp is an opensource tool'; | ||||||
|       expect(new MessageFormatter(message, true).formattedMessage).toEqual( |       expect(new MessageFormatter(message, true).formattedMessage).toMatch( | ||||||
|         '<a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool' |         '<p><a href="https://twitter.com/hashtag/chatwootapp" target="_blank" rel="noreferrer nofollow noopener">#chatwootapp</a> is an opensource tool</p>' | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('plain text content', () => { | ||||||
|  |     it('returns the plain text without HTML', () => { | ||||||
|  |       const message = | ||||||
|  |         '<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>'; | ||||||
|  |       expect(new MessageFormatter(message).plainText).toMatch( | ||||||
|  |         'Chatwoot is an opensource tool. https://www.chatwoot.com' | ||||||
|       ); |       ); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -6,6 +6,10 @@ export default { | |||||||
|       const messageFormatter = new MessageFormatter(message, isATweet); |       const messageFormatter = new MessageFormatter(message, isATweet); | ||||||
|       return messageFormatter.formattedMessage; |       return messageFormatter.formattedMessage; | ||||||
|     }, |     }, | ||||||
|  |     getPlainText(message, isATweet) { | ||||||
|  |       const messageFormatter = new MessageFormatter(message, isATweet); | ||||||
|  |       return messageFormatter.plainText; | ||||||
|  |     }, | ||||||
|     truncateMessage(description = '') { |     truncateMessage(description = '') { | ||||||
|       if (description.length < 100) { |       if (description.length < 100) { | ||||||
|         return description; |         return description; | ||||||
|   | |||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | 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' | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -4,7 +4,7 @@ | |||||||
|       v-if="!isCards && !isOptions && !isForm && !isArticle" |       v-if="!isCards && !isOptions && !isForm && !isArticle" | ||||||
|       class="chat-bubble agent" |       class="chat-bubble agent" | ||||||
|     > |     > | ||||||
|       <span v-html="formatMessage(message, false)"></span> |       <div class="message-content" v-html="formatMessage(message, false)"></div> | ||||||
|       <email-input |       <email-input | ||||||
|         v-if="isTemplateEmail" |         v-if="isTemplateEmail" | ||||||
|         :message-id="messageId" |         :message-id="messageId" | ||||||
| @@ -133,3 +133,13 @@ export default { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import '~widget/assets/scss/variables.scss'; | ||||||
|  |  | ||||||
|  | .chat-bubble .message-content::v-deep pre { | ||||||
|  |   background: $color-primary-light; | ||||||
|  |   color: $color-body; | ||||||
|  |   overflow: scroll; | ||||||
|  |   padding: $space-smaller; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|   | |||||||
| @@ -94,6 +94,7 @@ export default { | |||||||
|  |  | ||||||
|     .message-wrap { |     .message-wrap { | ||||||
|       margin-right: $space-small; |       margin-right: $space-small; | ||||||
|  |       max-width: 100%; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .in-progress { |     .in-progress { | ||||||
|   | |||||||
| @@ -44,12 +44,17 @@ export default { | |||||||
|   padding: $space-slab $space-normal $space-slab $space-normal; |   padding: $space-slab $space-normal $space-slab $space-normal; | ||||||
|   text-align: left; |   text-align: left; | ||||||
|   word-break: break-word; |   word-break: break-word; | ||||||
|  |   max-width: 100%; | ||||||
|  |  | ||||||
|   > a { |   > a { | ||||||
|     color: $color-primary; |     color: $color-primary; | ||||||
|     word-break: break-all; |     word-break: break-all; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   .link { | ||||||
|  |     text-decoration: underline; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   &.user { |   &.user { | ||||||
|     border-bottom-right-radius: $space-smaller; |     border-bottom-right-radius: $space-smaller; | ||||||
|  |  | ||||||
| @@ -59,3 +64,13 @@ export default { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import '~widget/assets/scss/variables.scss'; | ||||||
|  |  | ||||||
|  | .chat-bubble.user::v-deep pre { | ||||||
|  |   background: $color-primary-light; | ||||||
|  |   color: $color-body; | ||||||
|  |   overflow: scroll; | ||||||
|  |   padding: $space-smaller; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|   | |||||||
| @@ -23,12 +23,14 @@ | |||||||
|     "core-js": "3", |     "core-js": "3", | ||||||
|     "country-code-emoji": "^1.0.0", |     "country-code-emoji": "^1.0.0", | ||||||
|     "date-fns": "^2.16.1", |     "date-fns": "^2.16.1", | ||||||
|  |     "dompurify": "^2.2.6", | ||||||
|     "dotenv": "^8.0.0", |     "dotenv": "^8.0.0", | ||||||
|     "foundation-sites": "~6.5.3", |     "foundation-sites": "~6.5.3", | ||||||
|     "highlight.js": "~10.4.1", |     "highlight.js": "~10.4.1", | ||||||
|     "ionicons": "~2.0.1", |     "ionicons": "~2.0.1", | ||||||
|     "js-cookie": "^2.2.1", |     "js-cookie": "^2.2.1", | ||||||
|     "lodash.groupby": "^4.6.0", |     "lodash.groupby": "^4.6.0", | ||||||
|  |     "marked": "^1.2.7", | ||||||
|     "md5": "^2.3.0", |     "md5": "^2.3.0", | ||||||
|     "query-string": "5", |     "query-string": "5", | ||||||
|     "spinkit": "~1.2.5", |     "spinkit": "~1.2.5", | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -3860,6 +3860,11 @@ domhandler@^2.3.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     domelementtype "1" |     domelementtype "1" | ||||||
|  |  | ||||||
|  | dompurify@^2.2.6: | ||||||
|  |   version "2.2.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.6.tgz#54945dc5c0b45ce5ae228705777e8e59d7b2edc4" | ||||||
|  |   integrity sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ== | ||||||
|  |  | ||||||
| domutils@^1.5.1, domutils@^1.7.0: | domutils@^1.5.1, domutils@^1.7.0: | ||||||
|   version "1.7.0" |   version "1.7.0" | ||||||
|   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" |   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" | ||||||
| @@ -7016,6 +7021,11 @@ map-visit@^1.0.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     object-visit "^1.0.0" |     object-visit "^1.0.0" | ||||||
|  |  | ||||||
|  | marked@^1.2.7: | ||||||
|  |   version "1.2.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.7.tgz#6e14b595581d2319cdcf033a24caaf41455a01fb" | ||||||
|  |   integrity sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA== | ||||||
|  |  | ||||||
| material-colors@^1.0.0: | material-colors@^1.0.0: | ||||||
|   version "1.2.6" |   version "1.2.6" | ||||||
|   resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" |   resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user