Skip to content

Commit c313fac

Browse files
author
mcallegari10
committed
Merge branch 'master' into issue-242-resizeable
2 parents edd3271 + aa47838 commit c313fac

File tree

17 files changed

+246
-23
lines changed

17 files changed

+246
-23
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ yarn-error.log
1111

1212
# Test files
1313
coverage
14+
15+
# vscode
16+
.vscode

README.md

+4-3
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ import logo from './logo.svg';
115115

116116
function App() {
117117
useEffect(() => {
118-
addResponseMessage('Welcome to this awesome chat!');
118+
addResponseMessage('Welcome to this **awesome** chat!');
119119
}, []);
120120

121121
const handleNewUserMessage = (newMessage) => {
@@ -167,6 +167,7 @@ export default App;
167167
|**handleTextInputChange**|(event) => any|NO| |Prop that triggers on input change|
168168
|**handleSubmit**|(event) => any|NO| |Prop that triggers when a message is submitted, used for custom validation|
169169
|**resizable**|boolean|NO|false|Prop that allows to resize the widget by dragging it's left border|
170+
|**emojis**|boolean|NO|false|enable emoji picker|
170171

171172
#### Styles
172173

@@ -192,13 +193,13 @@ As of v3.0, messages now have an optional ID that can be added on creation.If yo
192193

193194
- **addResponseMessage**
194195
- params:
195-
- text: string
196+
- text: string (supports markdown)
196197
- id: string (optional)
197198
- Method to add a new message written as a response to a user input.
198199

199200
- **addUserMessage**
200201
- params:
201-
- text: string
202+
- text: string (supports markdown)
202203
- id: string (optional)
203204
- This method will add a new message written as a user. Keep in mind it will not trigger the prop handleNewUserMessage()
204205

assets/icon-smiley.svg

+1
Loading

dev/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export default class App extends Component {
4646
handleQuickButtonClicked={this.handleQuickButtonClicked}
4747
imagePreview
4848
handleSubmit={this.handleSubmit}
49+
emojis
50+
resizable
4951
/>
5052
);
5153
}

package-lock.json

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"dependencies": {
2222
"classnames": "^2.2.6",
2323
"date-fns": "^2.11.1",
24+
"emoji-mart": "^3.0.1",
2425
"markdown-it": "^8.4.1",
2526
"markdown-it-link-attributes": "^2.1.0",
2627
"markdown-it-sanitizer": "^0.4.3",

src/components/Widget/components/Conversation/components/Messages/components/Message/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type Props = {
1515
}
1616

1717
function Message({ message, showTimeStamp }: Props) {
18-
const sanitizedHTML = markdownIt()
18+
const sanitizedHTML = markdownIt({ break: true })
1919
.use(markdownItClass, {
2020
img: ['rcw-message-img']
2121
})
@@ -26,7 +26,7 @@ function Message({ message, showTimeStamp }: Props) {
2626

2727
return (
2828
<div className={`rcw-${message.sender}`}>
29-
<div className="rcw-message-text" dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
29+
<div className="rcw-message-text" dangerouslySetInnerHTML={{ __html: sanitizedHTML.replace(/\n$/,'') }} />
3030
{showTimeStamp && <span className="rcw-timestamp">{format(message.timestamp, 'hh:mm')}</span>}
3131
</div>
3232
);

src/components/Widget/components/Conversation/components/Messages/components/Message/styles.scss

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
.rcw-message {
55
margin: 10px;
66
display: flex;
7+
white-space: pre-wrap;
78
word-wrap: break-word;
89
}
910

@@ -19,6 +20,9 @@
1920

2021
.rcw-message-text {
2122
@include message-bubble($turqois-2);
23+
24+
white-space: pre-wrap;
25+
word-wrap: break-word;
2226
}
2327

2428
.rcw-timestamp {
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { useRef, useEffect } from 'react';
1+
import { useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
22
import { useSelector } from 'react-redux';
33
import cn from 'classnames';
44

55
import { GlobalState } from 'src/store/types';
66

7+
import { getCaretIndex, isFirefox, updateCaret, insertNodeAtCaret, getSelection } from '../../../../../../utils/contentEditable'
78
const send = require('../../../../../../../assets/send_button.svg') as string;
9+
const emoji = require('../../../../../../../assets/icon-smiley.svg') as string;
10+
const brRegex = /<br>/g;
811

912
import './style.scss';
1013

@@ -14,41 +17,123 @@ type Props = {
1417
autofocus: boolean;
1518
sendMessage: (event: any) => void;
1619
buttonAlt: string;
20+
onPressEmoji: () => void;
21+
onChangeSize: (event: any) => void;
1722
onTextInputChange?: (event: any) => void;
1823
}
1924

20-
function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInputChange, buttonAlt }: Props) {
25+
function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInputChange, buttonAlt, onPressEmoji, onChangeSize }: Props, ref) {
2126
const showChat = useSelector((state: GlobalState) => state.behavior.showChat);
22-
const inputRef = useRef<HTMLSpanElement>(null);
27+
const inputRef = useRef<HTMLDivElement>(null!);
28+
const refContainer = useRef<HTMLDivElement>(null);
29+
const [enter, setEnter]= useState(false)
30+
const [firefox, setFirefox] = useState(false);
31+
const [height, setHeight] = useState(0)
2332
// @ts-ignore
2433
useEffect(() => { if (showChat && autofocus) inputRef.current?.focus(); }, [showChat]);
34+
useEffect(() => { setFirefox(isFirefox())}, [])
35+
36+
useImperativeHandle(ref, () => {
37+
return {
38+
onSelectEmoji: handlerOnSelectEmoji,
39+
};
40+
});
2541

2642
const handlerOnChange = (event) => {
2743
onTextInputChange && onTextInputChange(event)
2844
}
2945

3046
const handlerSendMessage = () => {
31-
const { current } = inputRef
32-
if(current?.innerHTML) {
33-
sendMessage(current.innerText);
34-
current.innerHTML = ''
47+
const el = inputRef.current;
48+
if(el.innerHTML) {
49+
sendMessage(el.innerText);
50+
el.innerHTML = ''
3551
}
3652
}
3753

54+
const handlerOnSelectEmoji = (emoji) => {
55+
const el = inputRef.current;
56+
const { start, end } = getSelection(el)
57+
if(el.innerHTML) {
58+
const firstPart = el.innerHTML.substring(0, start);
59+
const secondPart = el.innerHTML.substring(end);
60+
el.innerHTML = (`${firstPart}${emoji.native}${secondPart}`)
61+
} else {
62+
el.innerHTML = emoji.native
63+
}
64+
updateCaret(el, start, emoji.native.length)
65+
}
66+
3867
const handlerOnKeyPress = (event) => {
68+
const el = inputRef.current;
69+
3970
if(event.charCode == 13 && !event.shiftKey) {
4071
event.preventDefault()
4172
handlerSendMessage();
4273
}
74+
if(event.charCode === 13 && event.shiftKey) {
75+
event.preventDefault()
76+
insertNodeAtCaret(el);
77+
setEnter(true)
78+
}
79+
}
80+
81+
// TODO use a context for checkSize and toggle picker
82+
const checkSize = () => {
83+
const senderEl = refContainer.current
84+
if(senderEl && height !== senderEl.clientHeight) {
85+
const {clientHeight} = senderEl;
86+
setHeight(clientHeight)
87+
onChangeSize(clientHeight ? clientHeight -1 : 0)
88+
}
89+
}
90+
91+
const handlerOnKeyUp = (event) => {
92+
const el = inputRef.current;
93+
if(!el) return true;
94+
// Conditions need for firefox
95+
if(firefox && event.key === 'Backspace') {
96+
if(el.innerHTML.length === 1 && enter) {
97+
el.innerHTML = '';
98+
setEnter(false);
99+
}
100+
else if(brRegex.test(el.innerHTML)){
101+
el.innerHTML = el.innerHTML.replace(brRegex, '');
102+
}
103+
}
104+
checkSize();
105+
}
106+
107+
const handlerOnKeyDown= (event) => {
108+
const el = inputRef.current;
109+
110+
if( event.key === 'Backspace' && el){
111+
const caretPosition = getCaretIndex(inputRef.current);
112+
const character = el.innerHTML.charAt(caretPosition - 1);
113+
if(character === "\n") {
114+
event.preventDefault();
115+
event.stopPropagation();
116+
el.innerHTML = (el.innerHTML.substring(0, caretPosition - 1) + el.innerHTML.substring(caretPosition))
117+
updateCaret(el, caretPosition, -1)
118+
}
119+
}
120+
}
121+
122+
const handlerPressEmoji = () => {
123+
onPressEmoji();
124+
checkSize();
43125
}
44126

45127
return (
46-
<div className="rcw-sender">
128+
<div ref={refContainer} className="rcw-sender">
129+
<button className='rcw-picker-btn' type="submit" onClick={handlerPressEmoji}>
130+
<img src={emoji} className="rcw-picker-icon" alt="" />
131+
</button>
47132
<div className={cn('rcw-new-message', {
48133
'rcw-message-disable': disabledInput,
49134
})
50135
}>
51-
<span
136+
<div
52137
spellCheck
53138
className="rcw-input"
54139
role="textbox"
@@ -57,7 +142,10 @@ function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInpu
57142
placeholder={placeholder}
58143
onInput={handlerOnChange}
59144
onKeyPress={handlerOnKeyPress}
145+
onKeyUp={handlerOnKeyUp}
146+
onKeyDown={handlerOnKeyDown}
60147
/>
148+
61149
</div>
62150
<button type="submit" className="rcw-send" onClick={handlerSendMessage}>
63151
<img src={send} className="rcw-send-icon" alt={buttonAlt} />
@@ -66,4 +154,4 @@ function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInpu
66154
);
67155
}
68156

69-
export default Sender;
157+
export default forwardRef(Sender);

src/components/Widget/components/Conversation/components/Sender/style.scss

+8-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
min-height: 45px;
1111
overflow: hidden;
1212
padding: 10px;
13+
position: relative;
1314

1415
&.expand {
1516
height: 55px;
@@ -36,8 +37,12 @@
3637
.rcw-input {
3738
display: block;
3839
height: 100%;
39-
max-height: 75px;
40+
line-height: 20px;
41+
max-height: 78px;
4042
overflow-y: auto;
43+
user-select: text;
44+
white-space: pre-wrap;
45+
word-wrap: break-word;
4146

4247
&:focus-visible {
4348
outline: none;
@@ -49,9 +54,10 @@
4954
}
5055
}
5156

52-
.rcw-send {
57+
.rcw-send, .rcw-picker-btn {
5358
background: $grey-2;
5459
border: 0;
60+
cursor: pointer;
5561

5662
.rcw-send-icon {
5763
height: 25px;

src/components/Widget/components/Conversation/index.tsx

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useEffect, useState } from 'react';
1+
import { useRef, useState, useEffect } from 'react';
2+
import { Picker } from 'emoji-mart';
23
import cn from 'classnames';
34

45
import Header from './components/Header';
@@ -10,6 +11,10 @@ import { AnyFunction } from '../../../../utils/types';
1011

1112
import './style.scss';
1213

14+
interface ISenderRef {
15+
onSelectEmoji: (event: any) => void;
16+
}
17+
1318
type Props = {
1419
title: string;
1520
subtitle: string;
@@ -27,6 +32,7 @@ type Props = {
2732
sendButtonAlt: string;
2833
showTimeStamp: boolean;
2934
resizable?: boolean;
35+
emojis?: boolean;
3036
};
3137

3238
function Conversation({
@@ -46,6 +52,7 @@ function Conversation({
4652
sendButtonAlt,
4753
showTimeStamp,
4854
resizable,
55+
emojis
4956
}: Props) {
5057
const [containerDiv, setContainerDiv] = useState<HTMLElement | null>();
5158
let startX, startWidth;
@@ -76,6 +83,23 @@ function Conversation({
7683
window.removeEventListener('mousemove', resize, false);
7784
window.removeEventListener('mouseup', stopResize, false);
7885
}
86+
87+
const [pickerOffset, setOffset] = useState(0)
88+
const senderRef = useRef<ISenderRef>(null!);
89+
const [pickerStatus, setPicket] = useState(false)
90+
91+
const onSelectEmoji = (emoji) => {
92+
senderRef.current?.onSelectEmoji(emoji)
93+
}
94+
95+
const togglePicker = () => {
96+
setPicket(prevPickerStatus => !prevPickerStatus)
97+
}
98+
99+
const handlerSendMsn = (event) => {
100+
sendMessage(event)
101+
if(pickerStatus) setPicket(false)
102+
}
79103

80104
return (
81105
<div id="rcw-conversation-container" onMouseDown={initResize}
@@ -90,13 +114,20 @@ function Conversation({
90114
/>
91115
<Messages profileAvatar={profileAvatar} showTimeStamp={showTimeStamp} />
92116
<QuickButtons onQuickButtonClicked={onQuickButtonClicked} />
117+
{emojis && pickerStatus && (<Picker
118+
style={{ position: 'absolute', bottom: pickerOffset, left: '0', width: '100%' }}
119+
onSelect={onSelectEmoji}
120+
/>)}
93121
<Sender
94-
sendMessage={sendMessage}
122+
ref={senderRef}
123+
sendMessage={handlerSendMsn}
95124
placeholder={senderPlaceHolder}
96125
disabledInput={disabledInput}
97126
autofocus={autofocus}
98127
onTextInputChange={onTextInputChange}
99128
buttonAlt={sendButtonAlt}
129+
onPressEmoji={togglePicker}
130+
onChangeSize={setOffset}
100131
/>
101132
</div>
102133
);

0 commit comments

Comments
 (0)