diff --git a/.gitignore b/.gitignore index 1e49528df..0267dad17 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ yarn-error.log # Test files coverage + +# vscode +.vscode diff --git a/README.md b/README.md index 91189d10c..69ece6468 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ import logo from './logo.svg'; function App() { useEffect(() => { - addResponseMessage('Welcome to this awesome chat!'); + addResponseMessage('Welcome to this **awesome** chat!'); }, []); const handleNewUserMessage = (newMessage) => { @@ -167,6 +167,7 @@ export default App; |**handleTextInputChange**|(event) => any|NO| |Prop that triggers on input change| |**handleSubmit**|(event) => any|NO| |Prop that triggers when a message is submitted, used for custom validation| |**resizable**|boolean|NO|false|Prop that allows to resize the widget by dragging it's left border| +|**emojis**|boolean|NO|false|enable emoji picker| #### Styles @@ -192,13 +193,13 @@ As of v3.0, messages now have an optional ID that can be added on creation.If yo - **addResponseMessage** - params: - - text: string + - text: string (supports markdown) - id: string (optional) - Method to add a new message written as a response to a user input. - **addUserMessage** - params: - - text: string + - text: string (supports markdown) - id: string (optional) - This method will add a new message written as a user. Keep in mind it will not trigger the prop handleNewUserMessage() diff --git a/assets/icon-smiley.svg b/assets/icon-smiley.svg new file mode 100644 index 000000000..1f584ca2a --- /dev/null +++ b/assets/icon-smiley.svg @@ -0,0 +1 @@ + diff --git a/dev/App.tsx b/dev/App.tsx index 6d6788270..d2b6eeee7 100644 --- a/dev/App.tsx +++ b/dev/App.tsx @@ -46,6 +46,8 @@ export default class App extends Component { handleQuickButtonClicked={this.handleQuickButtonClicked} imagePreview handleSubmit={this.handleSubmit} + emojis + resizable /> ); } diff --git a/package-lock.json b/package-lock.json index 130fcbaec..95b0b8696 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5423,6 +5423,15 @@ "integrity": "sha512-Mi2m55JrX2BFbNZGKYR+2ItcGnR4O5HhrvgoRRyZQlaMGQULqDhoGkLWHzJoshSzi7k1PUofxcDbNhlFrDZNhg==", "dev": true }, + "emoji-mart": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-3.0.1.tgz", + "integrity": "sha512-sxpmMKxqLvcscu6mFn9ITHeZNkGzIvD0BSNFE/LJESPbCA8s1jM6bCDPjWbV31xHq7JXaxgpHxLB54RCbBZSlg==", + "requires": { + "@babel/runtime": "^7.0.0", + "prop-types": "^15.6.0" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", diff --git a/package.json b/package.json index 1f241266a..c024e2d11 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "classnames": "^2.2.6", "date-fns": "^2.11.1", + "emoji-mart": "^3.0.1", "markdown-it": "^8.4.1", "markdown-it-link-attributes": "^2.1.0", "markdown-it-sanitizer": "^0.4.3", diff --git a/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx b/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx index 5021c8039..a28f7031e 100644 --- a/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx +++ b/src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx @@ -15,7 +15,7 @@ type Props = { } function Message({ message, showTimeStamp }: Props) { - const sanitizedHTML = markdownIt() + const sanitizedHTML = markdownIt({ break: true }) .use(markdownItClass, { img: ['rcw-message-img'] }) @@ -26,7 +26,7 @@ function Message({ message, showTimeStamp }: Props) { return (
-
+
{showTimeStamp && {format(message.timestamp, 'hh:mm')}}
); diff --git a/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss b/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss index c286b6289..7c4281e6e 100644 --- a/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss +++ b/src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss @@ -4,6 +4,7 @@ .rcw-message { margin: 10px; display: flex; + white-space: pre-wrap; word-wrap: break-word; } @@ -19,6 +20,9 @@ .rcw-message-text { @include message-bubble($turqois-2); + + white-space: pre-wrap; + word-wrap: break-word; } .rcw-timestamp { diff --git a/src/components/Widget/components/Conversation/components/Sender/index.tsx b/src/components/Widget/components/Conversation/components/Sender/index.tsx index d214d2013..528535115 100644 --- a/src/components/Widget/components/Conversation/components/Sender/index.tsx +++ b/src/components/Widget/components/Conversation/components/Sender/index.tsx @@ -1,10 +1,13 @@ -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react'; import { useSelector } from 'react-redux'; import cn from 'classnames'; import { GlobalState } from 'src/store/types'; +import { getCaretIndex, isFirefox, updateCaret, insertNodeAtCaret, getSelection } from '../../../../../../utils/contentEditable' const send = require('../../../../../../../assets/send_button.svg') as string; +const emoji = require('../../../../../../../assets/icon-smiley.svg') as string; +const brRegex = /
/g; import './style.scss'; @@ -14,41 +17,123 @@ type Props = { autofocus: boolean; sendMessage: (event: any) => void; buttonAlt: string; + onPressEmoji: () => void; + onChangeSize: (event: any) => void; onTextInputChange?: (event: any) => void; } -function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInputChange, buttonAlt }: Props) { +function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInputChange, buttonAlt, onPressEmoji, onChangeSize }: Props, ref) { const showChat = useSelector((state: GlobalState) => state.behavior.showChat); - const inputRef = useRef(null); + const inputRef = useRef(null!); + const refContainer = useRef(null); + const [enter, setEnter]= useState(false) + const [firefox, setFirefox] = useState(false); + const [height, setHeight] = useState(0) // @ts-ignore useEffect(() => { if (showChat && autofocus) inputRef.current?.focus(); }, [showChat]); + useEffect(() => { setFirefox(isFirefox())}, []) + + useImperativeHandle(ref, () => { + return { + onSelectEmoji: handlerOnSelectEmoji, + }; + }); const handlerOnChange = (event) => { onTextInputChange && onTextInputChange(event) } const handlerSendMessage = () => { - const { current } = inputRef - if(current?.innerHTML) { - sendMessage(current.innerText); - current.innerHTML = '' + const el = inputRef.current; + if(el.innerHTML) { + sendMessage(el.innerText); + el.innerHTML = '' } } + const handlerOnSelectEmoji = (emoji) => { + const el = inputRef.current; + const { start, end } = getSelection(el) + if(el.innerHTML) { + const firstPart = el.innerHTML.substring(0, start); + const secondPart = el.innerHTML.substring(end); + el.innerHTML = (`${firstPart}${emoji.native}${secondPart}`) + } else { + el.innerHTML = emoji.native + } + updateCaret(el, start, emoji.native.length) + } + const handlerOnKeyPress = (event) => { + const el = inputRef.current; + if(event.charCode == 13 && !event.shiftKey) { event.preventDefault() handlerSendMessage(); } + if(event.charCode === 13 && event.shiftKey) { + event.preventDefault() + insertNodeAtCaret(el); + setEnter(true) + } + } + + // TODO use a context for checkSize and toggle picker + const checkSize = () => { + const senderEl = refContainer.current + if(senderEl && height !== senderEl.clientHeight) { + const {clientHeight} = senderEl; + setHeight(clientHeight) + onChangeSize(clientHeight ? clientHeight -1 : 0) + } + } + + const handlerOnKeyUp = (event) => { + const el = inputRef.current; + if(!el) return true; + // Conditions need for firefox + if(firefox && event.key === 'Backspace') { + if(el.innerHTML.length === 1 && enter) { + el.innerHTML = ''; + setEnter(false); + } + else if(brRegex.test(el.innerHTML)){ + el.innerHTML = el.innerHTML.replace(brRegex, ''); + } + } + checkSize(); + } + + const handlerOnKeyDown= (event) => { + const el = inputRef.current; + + if( event.key === 'Backspace' && el){ + const caretPosition = getCaretIndex(inputRef.current); + const character = el.innerHTML.charAt(caretPosition - 1); + if(character === "\n") { + event.preventDefault(); + event.stopPropagation(); + el.innerHTML = (el.innerHTML.substring(0, caretPosition - 1) + el.innerHTML.substring(caretPosition)) + updateCaret(el, caretPosition, -1) + } + } + } + + const handlerPressEmoji = () => { + onPressEmoji(); + checkSize(); } return ( -
+
+
- +