2018-08-29 17:24:25 +00:00
|
|
|
|
// @flow
|
|
|
|
|
|
|
|
|
|
import React, { Component } from 'react';
|
2019-05-07 22:29:22 +00:00
|
|
|
|
import TextareaAutosize from 'react-textarea-autosize';
|
2019-03-19 15:42:25 +00:00
|
|
|
|
import type { Dispatch } from 'redux';
|
2018-08-29 17:24:25 +00:00
|
|
|
|
|
2021-03-18 07:08:34 +00:00
|
|
|
|
import { isMobileBrowser } from '../../../base/environment/utils';
|
2019-05-02 20:05:32 +00:00
|
|
|
|
import { translate } from '../../../base/i18n';
|
2021-03-18 07:08:34 +00:00
|
|
|
|
import { Icon, IconPlane, IconSmile } from '../../../base/icons';
|
2019-03-21 16:38:29 +00:00
|
|
|
|
import { connect } from '../../../base/redux';
|
2021-09-09 22:46:28 +00:00
|
|
|
|
import { areSmileysDisabled } from '../../functions';
|
2019-03-21 16:38:29 +00:00
|
|
|
|
|
2018-08-29 17:24:25 +00:00
|
|
|
|
import SmileysPanel from './SmileysPanel';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The type of the React {@code Component} props of {@link ChatInput}.
|
|
|
|
|
*/
|
|
|
|
|
type Props = {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Invoked to send chat messages.
|
|
|
|
|
*/
|
2019-03-19 15:42:25 +00:00
|
|
|
|
dispatch: Dispatch<any>,
|
2018-08-29 17:24:25 +00:00
|
|
|
|
|
2019-05-08 17:45:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Optional callback to invoke when the chat textarea has auto-resized to
|
|
|
|
|
* fit overflowing text.
|
|
|
|
|
*/
|
|
|
|
|
onResize: ?Function,
|
|
|
|
|
|
2019-10-07 12:35:04 +00:00
|
|
|
|
/**
|
|
|
|
|
* Callback to invoke on message send.
|
|
|
|
|
*/
|
|
|
|
|
onSend: Function,
|
|
|
|
|
|
2019-05-02 20:05:32 +00:00
|
|
|
|
/**
|
|
|
|
|
* Invoked to obtain translated strings.
|
|
|
|
|
*/
|
2021-09-09 22:46:28 +00:00
|
|
|
|
t: Function,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether chat emoticons are disabled.
|
|
|
|
|
*/
|
|
|
|
|
_areSmileysDisabled: boolean
|
|
|
|
|
|
2018-08-29 17:24:25 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The type of the React {@code Component} state of {@link ChatInput}.
|
|
|
|
|
*/
|
|
|
|
|
type State = {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* User provided nickname when the input text is provided in the view.
|
|
|
|
|
*/
|
|
|
|
|
message: string,
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Whether or not the smiley selector is visible.
|
|
|
|
|
*/
|
|
|
|
|
showSmileysPanel: boolean
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Implements a React Component for drafting and submitting a chat message.
|
|
|
|
|
*
|
2021-11-04 21:10:43 +00:00
|
|
|
|
* @augments Component
|
2018-08-29 17:24:25 +00:00
|
|
|
|
*/
|
|
|
|
|
class ChatInput extends Component<Props, State> {
|
|
|
|
|
_textArea: ?HTMLTextAreaElement;
|
|
|
|
|
|
|
|
|
|
state = {
|
|
|
|
|
message: '',
|
|
|
|
|
showSmileysPanel: false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initializes a new {@code ChatInput} instance.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} props - The read-only properties with which the new
|
|
|
|
|
* instance is to be initialized.
|
|
|
|
|
*/
|
|
|
|
|
constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
|
|
this._textArea = null;
|
|
|
|
|
|
|
|
|
|
// Bind event handlers so they are only bound once for every instance.
|
|
|
|
|
this._onDetectSubmit = this._onDetectSubmit.bind(this);
|
|
|
|
|
this._onMessageChange = this._onMessageChange.bind(this);
|
|
|
|
|
this._onSmileySelect = this._onSmileySelect.bind(this);
|
2021-02-23 07:39:20 +00:00
|
|
|
|
this._onSubmitMessage = this._onSubmitMessage.bind(this);
|
2018-08-29 17:24:25 +00:00
|
|
|
|
this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
|
2021-06-10 12:48:44 +00:00
|
|
|
|
this._onEscHandler = this._onEscHandler.bind(this);
|
|
|
|
|
this._onToggleSmileysPanelKeyPress = this._onToggleSmileysPanelKeyPress.bind(this);
|
|
|
|
|
this._onSubmitMessageKeyPress = this._onSubmitMessageKeyPress.bind(this);
|
2018-08-29 17:24:25 +00:00
|
|
|
|
this._setTextAreaRef = this._setTextAreaRef.bind(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Implements React's {@link Component#componentDidMount()}.
|
|
|
|
|
*
|
|
|
|
|
* @inheritdoc
|
|
|
|
|
*/
|
|
|
|
|
componentDidMount() {
|
2021-03-18 07:08:34 +00:00
|
|
|
|
if (isMobileBrowser()) {
|
|
|
|
|
// Ensure textarea is not focused when opening chat on mobile browser.
|
|
|
|
|
this._textArea && this._textArea.blur();
|
|
|
|
|
}
|
2018-08-29 17:24:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Implements React's {@link Component#render()}.
|
|
|
|
|
*
|
|
|
|
|
* @inheritdoc
|
|
|
|
|
* @returns {ReactElement}
|
|
|
|
|
*/
|
|
|
|
|
render() {
|
|
|
|
|
const smileysPanelClassName = `${this.state.showSmileysPanel
|
|
|
|
|
? 'show-smileys' : 'hide-smileys'} smileys-panel`;
|
|
|
|
|
|
|
|
|
|
return (
|
2021-02-23 07:39:20 +00:00
|
|
|
|
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
|
|
|
|
|
<div id = 'chat-input' >
|
2021-09-09 22:46:28 +00:00
|
|
|
|
{ this.props._areSmileysDisabled ? null : (
|
|
|
|
|
<div className = 'smiley-input'>
|
|
|
|
|
<div id = 'smileysarea'>
|
|
|
|
|
<div id = 'smileys'>
|
|
|
|
|
<div
|
|
|
|
|
aria-expanded = { this.state.showSmileysPanel }
|
|
|
|
|
aria-haspopup = 'smileysContainer'
|
|
|
|
|
aria-label = { this.props.t('chat.smileysPanel') }
|
|
|
|
|
className = 'smiley-button'
|
|
|
|
|
onClick = { this._onToggleSmileysPanel }
|
|
|
|
|
onKeyDown = { this._onEscHandler }
|
|
|
|
|
onKeyPress = { this._onToggleSmileysPanelKeyPress }
|
|
|
|
|
role = 'button'
|
|
|
|
|
tabIndex = { 0 }>
|
|
|
|
|
<Icon src = { IconSmile } />
|
|
|
|
|
</div>
|
2021-03-18 07:08:34 +00:00
|
|
|
|
</div>
|
2021-02-23 07:39:20 +00:00
|
|
|
|
</div>
|
2021-09-09 22:46:28 +00:00
|
|
|
|
<div
|
|
|
|
|
className = { smileysPanelClassName } >
|
|
|
|
|
<SmileysPanel
|
|
|
|
|
onSmileySelect = { this._onSmileySelect } />
|
|
|
|
|
</div>
|
2021-02-23 07:39:20 +00:00
|
|
|
|
</div>
|
2021-09-09 22:46:28 +00:00
|
|
|
|
) }
|
2021-02-23 07:39:20 +00:00
|
|
|
|
<div className = 'usrmsg-form'>
|
|
|
|
|
<TextareaAutosize
|
2021-06-10 12:48:44 +00:00
|
|
|
|
autoComplete = 'off'
|
|
|
|
|
autoFocus = { true }
|
2021-02-23 07:39:20 +00:00
|
|
|
|
id = 'usermsg'
|
|
|
|
|
maxRows = { 5 }
|
|
|
|
|
onChange = { this._onMessageChange }
|
|
|
|
|
onHeightChange = { this.props.onResize }
|
|
|
|
|
onKeyDown = { this._onDetectSubmit }
|
|
|
|
|
placeholder = { this.props.t('chat.messagebox') }
|
2021-06-10 12:48:44 +00:00
|
|
|
|
ref = { this._setTextAreaRef }
|
|
|
|
|
tabIndex = { 0 }
|
2021-02-23 07:39:20 +00:00
|
|
|
|
value = { this.state.message } />
|
|
|
|
|
</div>
|
|
|
|
|
<div className = 'send-button-container'>
|
|
|
|
|
<div
|
2021-06-10 12:48:44 +00:00
|
|
|
|
aria-label = { this.props.t('chat.sendButton') }
|
2021-02-23 07:39:20 +00:00
|
|
|
|
className = 'send-button'
|
2021-06-10 12:48:44 +00:00
|
|
|
|
onClick = { this._onSubmitMessage }
|
|
|
|
|
onKeyPress = { this._onSubmitMessageKeyPress }
|
|
|
|
|
role = 'button'
|
|
|
|
|
tabIndex = { this.state.message.trim() ? 0 : -1 } >
|
2021-02-23 07:39:20 +00:00
|
|
|
|
<Icon src = { IconPlane } />
|
|
|
|
|
</div>
|
2018-08-29 17:24:25 +00:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Place cursor focus on this component's text area.
|
|
|
|
|
*
|
2019-05-07 22:33:12 +00:00
|
|
|
|
* @private
|
2018-08-29 17:24:25 +00:00
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
2019-05-07 22:33:12 +00:00
|
|
|
|
_focus() {
|
2018-08-29 17:24:25 +00:00
|
|
|
|
this._textArea && this._textArea.focus();
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-23 07:39:20 +00:00
|
|
|
|
|
|
|
|
|
_onSubmitMessage: () => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Submits the message to the chat window.
|
|
|
|
|
*
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onSubmitMessage() {
|
|
|
|
|
const trimmed = this.state.message.trim();
|
|
|
|
|
|
|
|
|
|
if (trimmed) {
|
|
|
|
|
this.props.onSend(trimmed);
|
|
|
|
|
|
|
|
|
|
this.setState({ message: '' });
|
2021-03-18 07:08:34 +00:00
|
|
|
|
|
|
|
|
|
// Keep the textarea in focus when sending messages via submit button.
|
|
|
|
|
this._focus();
|
2021-02-23 07:39:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2018-08-29 17:24:25 +00:00
|
|
|
|
_onDetectSubmit: (Object) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Detects if enter has been pressed. If so, submit the message in the chat
|
|
|
|
|
* window.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} event - Keyboard event.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onDetectSubmit(event) {
|
2022-04-07 14:03:07 +00:00
|
|
|
|
// Composition events used to add accents to characters
|
|
|
|
|
// despite their absence from standard US keyboards,
|
|
|
|
|
// to build up logograms of many Asian languages
|
|
|
|
|
// from their base components or categories and so on.
|
|
|
|
|
if (event.isComposing || event.keyCode === 229) {
|
|
|
|
|
// keyCode 229 means that user pressed some button,
|
|
|
|
|
// but input method is still processing that.
|
|
|
|
|
// This is a standard behavior for some input methods
|
|
|
|
|
// like entering japanese or сhinese hieroglyphs.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-10 12:48:44 +00:00
|
|
|
|
if (event.key === 'Enter'
|
|
|
|
|
&& event.shiftKey === false
|
|
|
|
|
&& event.ctrlKey === false) {
|
2018-08-29 17:24:25 +00:00
|
|
|
|
event.preventDefault();
|
2021-06-10 12:48:44 +00:00
|
|
|
|
event.stopPropagation();
|
2018-08-29 17:24:25 +00:00
|
|
|
|
|
2021-02-23 07:39:20 +00:00
|
|
|
|
this._onSubmitMessage();
|
2018-08-29 17:24:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-10 12:48:44 +00:00
|
|
|
|
_onSubmitMessageKeyPress: (Object) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* KeyPress handler for accessibility.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} e - The key event to handle.
|
|
|
|
|
*
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onSubmitMessageKeyPress(e) {
|
|
|
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._onSubmitMessage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-29 17:24:25 +00:00
|
|
|
|
_onMessageChange: (Object) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates the known message the user is drafting.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} event - Keyboard event.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onMessageChange(event) {
|
|
|
|
|
this.setState({ message: event.target.value });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onSmileySelect: (string) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Appends a selected smileys to the chat message draft.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} smileyText - The value of the smiley to append to the
|
|
|
|
|
* chat message.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onSmileySelect(smileyText) {
|
2021-06-10 12:48:44 +00:00
|
|
|
|
if (smileyText) {
|
|
|
|
|
this.setState({
|
|
|
|
|
message: `${this.state.message} ${smileyText}`,
|
|
|
|
|
showSmileysPanel: false
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this.setState({
|
|
|
|
|
showSmileysPanel: false
|
|
|
|
|
});
|
|
|
|
|
}
|
2018-08-29 17:24:25 +00:00
|
|
|
|
|
2019-05-07 22:33:12 +00:00
|
|
|
|
this._focus();
|
2018-08-29 17:24:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onToggleSmileysPanel: () => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Callback invoked to hide or show the smileys selector.
|
|
|
|
|
*
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onToggleSmileysPanel() {
|
2021-06-10 12:48:44 +00:00
|
|
|
|
if (this.state.showSmileysPanel) {
|
|
|
|
|
this._focus();
|
|
|
|
|
}
|
2018-08-29 17:24:25 +00:00
|
|
|
|
this.setState({ showSmileysPanel: !this.state.showSmileysPanel });
|
2021-06-10 12:48:44 +00:00
|
|
|
|
}
|
2018-08-29 17:24:25 +00:00
|
|
|
|
|
2021-06-10 12:48:44 +00:00
|
|
|
|
_onEscHandler: (Object) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* KeyPress handler for accessibility.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} e - The key event to handle.
|
|
|
|
|
*
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onEscHandler(e) {
|
|
|
|
|
// Escape handling does not work in onKeyPress
|
|
|
|
|
if (this.state.showSmileysPanel && e.key === 'Escape') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
this._onToggleSmileysPanel();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_onToggleSmileysPanelKeyPress: (Object) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* KeyPress handler for accessibility.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} e - The key event to handle.
|
|
|
|
|
*
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_onToggleSmileysPanelKeyPress(e) {
|
|
|
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
this._onToggleSmileysPanel();
|
|
|
|
|
}
|
2018-08-29 17:24:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_setTextAreaRef: (?HTMLTextAreaElement) => void;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets the reference to the HTML TextArea.
|
|
|
|
|
*
|
|
|
|
|
* @param {HTMLAudioElement} textAreaElement - The HTML text area element.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {void}
|
|
|
|
|
*/
|
|
|
|
|
_setTextAreaRef(textAreaElement: ?HTMLTextAreaElement) {
|
|
|
|
|
this._textArea = textAreaElement;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-09 22:46:28 +00:00
|
|
|
|
/**
|
|
|
|
|
* Function that maps parts of Redux state tree into component props.
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} state - Redux state.
|
|
|
|
|
* @private
|
|
|
|
|
* @returns {{
|
|
|
|
|
* _areSmileysDisabled: boolean
|
|
|
|
|
* }}
|
|
|
|
|
*/
|
|
|
|
|
const mapStateToProps = state => {
|
|
|
|
|
return {
|
|
|
|
|
_areSmileysDisabled: areSmileysDisabled(state)
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default translate(connect(mapStateToProps)(ChatInput));
|