Skip to content

Commit

Permalink
Merge branch 'master' into issue-242-resizeable
Browse files Browse the repository at this point in the history
  • Loading branch information
mcallegari10 committed Jul 29, 2021
2 parents edd3271 + aa47838 commit c313fac
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ yarn-error.log

# Test files
coverage

# vscode
.vscode
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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

Expand All @@ -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()

Expand Down
1 change: 1 addition & 0 deletions assets/icon-smiley.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions dev/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export default class App extends Component {
handleQuickButtonClicked={this.handleQuickButtonClicked}
imagePreview
handleSubmit={this.handleSubmit}
emojis
resizable
/>
);
}
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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']
})
Expand All @@ -26,7 +26,7 @@ function Message({ message, showTimeStamp }: Props) {

return (
<div className={`rcw-${message.sender}`}>
<div className="rcw-message-text" dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
<div className="rcw-message-text" dangerouslySetInnerHTML={{ __html: sanitizedHTML.replace(/\n$/,'') }} />
{showTimeStamp && <span className="rcw-timestamp">{format(message.timestamp, 'hh:mm')}</span>}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.rcw-message {
margin: 10px;
display: flex;
white-space: pre-wrap;
word-wrap: break-word;
}

Expand All @@ -19,6 +20,9 @@

.rcw-message-text {
@include message-bubble($turqois-2);

white-space: pre-wrap;
word-wrap: break-word;
}

.rcw-timestamp {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = /<br>/g;

import './style.scss';

Expand All @@ -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<HTMLSpanElement>(null);
const inputRef = useRef<HTMLDivElement>(null!);
const refContainer = useRef<HTMLDivElement>(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 (
<div className="rcw-sender">
<div ref={refContainer} className="rcw-sender">
<button className='rcw-picker-btn' type="submit" onClick={handlerPressEmoji}>
<img src={emoji} className="rcw-picker-icon" alt="" />
</button>
<div className={cn('rcw-new-message', {
'rcw-message-disable': disabledInput,
})
}>
<span
<div
spellCheck
className="rcw-input"
role="textbox"
Expand All @@ -57,7 +142,10 @@ function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInpu
placeholder={placeholder}
onInput={handlerOnChange}
onKeyPress={handlerOnKeyPress}
onKeyUp={handlerOnKeyUp}
onKeyDown={handlerOnKeyDown}
/>

</div>
<button type="submit" className="rcw-send" onClick={handlerSendMessage}>
<img src={send} className="rcw-send-icon" alt={buttonAlt} />
Expand All @@ -66,4 +154,4 @@ function Sender({ sendMessage, placeholder, disabledInput, autofocus, onTextInpu
);
}

export default Sender;
export default forwardRef(Sender);
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
min-height: 45px;
overflow: hidden;
padding: 10px;
position: relative;

&.expand {
height: 55px;
Expand All @@ -36,8 +37,12 @@
.rcw-input {
display: block;
height: 100%;
max-height: 75px;
line-height: 20px;
max-height: 78px;
overflow-y: auto;
user-select: text;
white-space: pre-wrap;
word-wrap: break-word;

&:focus-visible {
outline: none;
Expand All @@ -49,9 +54,10 @@
}
}

.rcw-send {
.rcw-send, .rcw-picker-btn {
background: $grey-2;
border: 0;
cursor: pointer;

.rcw-send-icon {
height: 25px;
Expand Down
35 changes: 33 additions & 2 deletions src/components/Widget/components/Conversation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { useRef, useState, useEffect } from 'react';
import { Picker } from 'emoji-mart';
import cn from 'classnames';

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

import './style.scss';

interface ISenderRef {
onSelectEmoji: (event: any) => void;
}

type Props = {
title: string;
subtitle: string;
Expand All @@ -27,6 +32,7 @@ type Props = {
sendButtonAlt: string;
showTimeStamp: boolean;
resizable?: boolean;
emojis?: boolean;
};

function Conversation({
Expand All @@ -46,6 +52,7 @@ function Conversation({
sendButtonAlt,
showTimeStamp,
resizable,
emojis
}: Props) {
const [containerDiv, setContainerDiv] = useState<HTMLElement | null>();
let startX, startWidth;
Expand Down Expand Up @@ -76,6 +83,23 @@ function Conversation({
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}

const [pickerOffset, setOffset] = useState(0)
const senderRef = useRef<ISenderRef>(null!);
const [pickerStatus, setPicket] = useState(false)

const onSelectEmoji = (emoji) => {
senderRef.current?.onSelectEmoji(emoji)
}

const togglePicker = () => {
setPicket(prevPickerStatus => !prevPickerStatus)
}

const handlerSendMsn = (event) => {
sendMessage(event)
if(pickerStatus) setPicket(false)
}

return (
<div id="rcw-conversation-container" onMouseDown={initResize}
Expand All @@ -90,13 +114,20 @@ function Conversation({
/>
<Messages profileAvatar={profileAvatar} showTimeStamp={showTimeStamp} />
<QuickButtons onQuickButtonClicked={onQuickButtonClicked} />
{emojis && pickerStatus && (<Picker
style={{ position: 'absolute', bottom: pickerOffset, left: '0', width: '100%' }}
onSelect={onSelectEmoji}
/>)}
<Sender
sendMessage={sendMessage}
ref={senderRef}
sendMessage={handlerSendMsn}
placeholder={senderPlaceHolder}
disabledInput={disabledInput}
autofocus={autofocus}
onTextInputChange={onTextInputChange}
buttonAlt={sendButtonAlt}
onPressEmoji={togglePicker}
onChangeSize={setOffset}
/>
</div>
);
Expand Down
Loading

0 comments on commit c313fac

Please sign in to comment.