diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue index 8fe929354..2180a870d 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue @@ -17,6 +17,8 @@ import { BUS_EVENTS } from 'shared/constants/busEvents'; import TagAgents from '../conversation/TagAgents.vue'; import CannedResponse from '../conversation/CannedResponse.vue'; import VariableList from '../conversation/VariableList.vue'; +import KeyboardEmojiSelector from './keyboardEmojiSelector.vue'; + import { appendSignature, removeSignature, @@ -24,6 +26,7 @@ import { scrollCursorIntoView, findNodeToInsertImage, setURLWithQueryAndSize, + getContentNode, } from 'dashboard/helper/editorHelper'; const TYPING_INDICATOR_IDLE_TIME = 4000; @@ -35,10 +38,8 @@ import { } from 'shared/helpers/KeyboardHelpers'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import { useUISettings } from 'dashboard/composables/useUISettings'; -import { - replaceVariablesInMessage, - createTypingIndicator, -} from '@chatwoot/utils'; + +import { createTypingIndicator } from '@chatwoot/utils'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { uploadFile } from 'dashboard/helper/uploadHelper'; @@ -71,7 +72,12 @@ const createState = ( export default { name: 'WootMessageEditor', - components: { TagAgents, CannedResponse, VariableList }, + components: { + TagAgents, + CannedResponse, + VariableList, + KeyboardEmojiSelector, + }, mixins: [keyboardEventListenerMixins], props: { value: { type: String, default: '' }, @@ -119,9 +125,11 @@ export default { showUserMentions: false, showCannedMenu: false, showVariables: false, + showEmojiMenu: false, mentionSearchKey: '', cannedSearchTerm: '', variableSearchTerm: '', + emojiSearchTerm: '', editorView: null, range: null, state: undefined, @@ -169,7 +177,7 @@ export default { this.editorView = args.view; this.range = args.range; - this.mentionSearchKey = args.text.replace('@', ''); + this.mentionSearchKey = args.text; return false; }, @@ -198,7 +206,7 @@ export default { this.editorView = args.view; this.range = args.range; - this.cannedSearchTerm = args.text.replace('/', ''); + this.cannedSearchTerm = args.text; return false; }, onExit: () => { @@ -226,7 +234,7 @@ export default { this.editorView = args.view; this.range = args.range; - this.variableSearchTerm = args.text.replace('{{', ''); + this.variableSearchTerm = args.text; return false; }, onExit: () => { @@ -238,6 +246,31 @@ export default { return event.keyCode === 13 && this.showVariables; }, }), + suggestionsPlugin({ + matcher: triggerCharacters(':', 1), // Trigger after ':' and at least 1 characters + suggestionClass: '', + onEnter: args => { + this.showEmojiMenu = true; + this.emojiSearchTerm = args.text || ''; + this.range = args.range; + this.editorView = args.view; + return false; + }, + onChange: args => { + this.editorView = args.view; + this.range = args.range; + this.emojiSearchTerm = args.text; + return false; + }, + onExit: () => { + this.emojiSearchTerm = ''; + this.showEmojiMenu = false; + return false; + }, + onKeyDown: ({ event }) => { + return event.keyCode === 13 && this.showEmojiMenu; + }, + }), ]; }, sendWithSignature() { @@ -267,6 +300,8 @@ export default { }, editorId() { this.showCannedMenu = false; + this.showEmojiMenu = false; + this.showVariables = false; this.cannedSearchTerm = ''; this.reloadState(this.value); }, @@ -517,57 +552,36 @@ export default { this.editorView.dispatch(tr.setSelection(selection)); this.editorView.focus(); }, - insertMentionNode(mentionItem) { + /** + * Inserts special content (mention, canned response, variable, emoji) into the editor. + * @param {string} type - The type of special content to insert. Possible values: 'mention', 'canned_response', 'variable', 'emoji'. + * @param {Object|string} content - The content to insert, depending on the type. + */ + insertSpecialContent(type, content) { if (!this.editorView) { - return null; - } - const node = this.editorView.state.schema.nodes.mention.create({ - userId: mentionItem.id, - userFullName: mentionItem.name, - }); - - this.insertNodeIntoEditor(node, this.range.from, this.range.to); - this.$track(CONVERSATION_EVENTS.USED_MENTIONS); - - return false; - }, - insertCannedResponse(cannedItem) { - const updatedMessage = replaceVariablesInMessage({ - message: cannedItem, - variables: this.variables, - }); - - if (!this.editorView) { - return null; + return; } - let node = new MessageMarkdownTransformer(messageSchema).parse( - updatedMessage + let { node, from, to } = getContentNode( + this.editorView, + type, + content, + this.range, + this.variables ); - const from = - node.textContent === updatedMessage - ? this.range.from - : this.range.from - 1; - - this.insertNodeIntoEditor(node, from, this.range.to); - - this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); - return false; - }, - insertVariable(variable) { - if (!this.editorView) { - return null; - } - - const content = `{{${variable}}}`; - let node = this.editorView.state.schema.text(content); - const { from, to } = this.range; + if (!node) return; this.insertNodeIntoEditor(node, from, to); - this.showVariables = false; - this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE); - return false; + + const event_map = { + mention: CONVERSATION_EVENTS.USED_MENTIONS, + cannedResponse: CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE, + variable: CONVERSATION_EVENTS.INSERTED_A_VARIABLE, + emoji: CONVERSATION_EVENTS.INSERTED_AN_EMOJI, + }; + + this.$track(event_map[type]); }, openFileBrowser() { this.$refs.imageUpload.click(); @@ -687,17 +701,22 @@ export default { + +import { shallowRef, computed, onMounted } from 'vue'; +import emojis from 'shared/components/emoji/emojisGroup.json'; +import MentionBox from '../mentions/MentionBox.vue'; + +const props = defineProps({ + searchKey: { + type: String, + default: '', + }, +}); + +const emit = defineEmits(['click']); + +const allEmojis = shallowRef([]); + +const items = computed(() => { + if (!props.searchKey) return []; + const searchTerm = props.searchKey.toLowerCase(); + return allEmojis.value.filter(emoji => + emoji.searchString.includes(searchTerm) + ); +}); + +function loadEmojis() { + allEmojis.value = emojis.flatMap(group => + group.emojis.map(emoji => ({ + ...emoji, + searchString: `${emoji.slug} ${emoji.name}`.toLowerCase(), + })) + ); +} + +function handleMentionClick(item = {}) { + emit('click', item.emoji); +} + +onMounted(() => { + loadEmojis(); +}); + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue b/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue index 1600a9cff..1cdc57a0f 100644 --- a/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue +++ b/app/javascript/dashboard/components/widgets/conversation/CannedResponse.vue @@ -41,14 +41,11 @@ export default { }; + diff --git a/app/javascript/dashboard/components/widgets/conversation/VariableList.vue b/app/javascript/dashboard/components/widgets/conversation/VariableList.vue index 66ad8965d..ab9dc57d8 100644 --- a/app/javascript/dashboard/components/widgets/conversation/VariableList.vue +++ b/app/javascript/dashboard/components/widgets/conversation/VariableList.vue @@ -56,20 +56,14 @@ export default { }; +