import React, { Component, RefObject } from 'react'; import { WithTranslation } from 'react-i18next'; import { IState, IStore } from '../../../app/types'; import { isMobileBrowser } from '../../../base/environment/utils'; import { translate } from '../../../base/i18n/functions'; import { IconPlane, IconSmile } from '../../../base/icons/svg'; import { connect } from '../../../base/redux/functions'; import Button from '../../../base/ui/components/web/Button'; import Input from '../../../base/ui/components/web/Input'; import { areSmileysDisabled } from '../../functions'; // @ts-ignore import SmileysPanel from './SmileysPanel'; /** * The type of the React {@code Component} props of {@link ChatInput}. */ interface Props extends WithTranslation { /** * Whether chat emoticons are disabled. */ _areSmileysDisabled: boolean; /** * Invoked to send chat messages. */ dispatch: IStore['dispatch']; /** * Callback to invoke on message send. */ onSend: Function; } /** * 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. * * @augments Component */ class ChatInput extends Component { _textArea?: RefObject; 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 = React.createRef(); // 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); this._onSubmitMessage = this._onSubmitMessage.bind(this); this._toggleSmileysPanel = this._toggleSmileysPanel.bind(this); } /** * Implements React's {@link Component#componentDidMount()}. * * @inheritdoc */ componentDidMount() { if (isMobileBrowser()) { // Ensure textarea is not focused when opening chat on mobile browser. this._textArea?.current && this._textArea.current.blur(); } } /** * Implements React's {@link Component#render()}. * * @inheritdoc * @returns {ReactElement} */ render() { return (
{!this.props._areSmileysDisabled && this.state.showSmileysPanel && (
)}
); } /** * Place cursor focus on this component's text area. * * @private * @returns {void} */ _focus() { this._textArea?.current && this._textArea.current.focus(); } /** * 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: '' }); // Keep the textarea in focus when sending messages via submit button. this._focus(); } } /** * 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: any) { // 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; } if (event.key === 'Enter' && event.shiftKey === false && event.ctrlKey === false) { event.preventDefault(); event.stopPropagation(); this._onSubmitMessage(); } } /** * Updates the known message the user is drafting. * * @param {string} value - Keyboard event. * @private * @returns {void} */ _onMessageChange(value: string) { this.setState({ message: value }); } /** * 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: string) { if (smileyText) { this.setState({ message: `${this.state.message} ${smileyText}`, showSmileysPanel: false }); } else { this.setState({ showSmileysPanel: false }); } this._focus(); } /** * Callback invoked to hide or show the smileys selector. * * @private * @returns {void} */ _toggleSmileysPanel() { if (this.state.showSmileysPanel) { this._focus(); } this.setState({ showSmileysPanel: !this.state.showSmileysPanel }); } } /** * Function that maps parts of Redux state tree into component props. * * @param {Object} state - Redux state. * @private * @returns {{ * _areSmileysDisabled: boolean * }} */ const mapStateToProps = (state: IState) => { return { _areSmileysDisabled: areSmileysDisabled(state) }; }; export default translate(connect(mapStateToProps)(ChatInput));