bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/chat/message-form/component.jsx

324 lines
8.3 KiB
React
Raw Normal View History

import React, { PureComponent } from 'react';
import { defineMessages, injectIntl, intlShape } from 'react-intl';
2016-06-02 00:33:19 +08:00
import cx from 'classnames';
import TextareaAutosize from 'react-autosize-textarea';
import browser from 'browser-detect';
import PropTypes from 'prop-types';
import { styles } from './styles.scss';
import Button from '../../button/component';
2016-06-02 00:33:19 +08:00
const propTypes = {
intl: intlShape.isRequired,
chatId: PropTypes.string.isRequired,
disabled: PropTypes.bool.isRequired,
minMessageLength: PropTypes.number.isRequired,
maxMessageLength: PropTypes.number.isRequired,
chatTitle: PropTypes.string.isRequired,
chatName: PropTypes.string.isRequired,
className: PropTypes.string,
chatAreaId: PropTypes.string.isRequired,
handleSendMessage: PropTypes.func.isRequired,
UnsentMessagesCollection: PropTypes.object.isRequired,
connected: PropTypes.bool.isRequired,
locked: PropTypes.bool.isRequired,
partnerIsLoggedOut: PropTypes.bool.isRequired,
2016-06-02 00:33:19 +08:00
};
const defaultProps = {
className: '',
2016-06-02 00:33:19 +08:00
};
2016-06-03 04:32:42 +08:00
const messages = defineMessages({
submitLabel: {
id: 'app.chat.submitLabel',
description: 'Chat submit button label',
},
inputLabel: {
id: 'app.chat.inputLabel',
description: 'Chat message input label',
},
2016-12-07 01:07:22 +08:00
inputPlaceholder: {
id: 'app.chat.inputPlaceholder',
description: 'Chat message input placeholder',
},
errorMinMessageLength: {
id: 'app.chat.errorMinMessageLength',
},
errorMaxMessageLength: {
id: 'app.chat.errorMaxMessageLength',
},
2019-06-18 04:49:59 +08:00
errorServerDisconnected: {
id: 'app.chat.disconnected',
},
errorChatLocked: {
id: 'app.chat.locked',
},
2016-06-03 04:32:42 +08:00
});
const CHAT_ENABLED = Meteor.settings.public.chat.enabled;
2019-07-31 10:37:50 +08:00
const IS_TYPING_INTERVAL = 2500;
class MessageForm extends PureComponent {
2016-06-02 00:33:19 +08:00
constructor(props) {
super(props);
this.state = {
message: '',
error: '',
hasErrors: false,
2016-06-02 00:33:19 +08:00
};
this.BROWSER_RESULTS = browser();
2016-06-02 00:33:19 +08:00
this.handleMessageChange = this.handleMessageChange.bind(this);
2016-07-08 02:52:21 +08:00
this.handleMessageKeyDown = this.handleMessageKeyDown.bind(this);
2016-06-02 00:33:19 +08:00
this.handleSubmit = this.handleSubmit.bind(this);
2019-06-18 04:49:59 +08:00
this.setMessageHint = this.setMessageHint.bind(this);
2016-06-02 00:33:19 +08:00
}
componentDidMount() {
const { mobile } = this.BROWSER_RESULTS;
2019-02-09 03:23:35 +08:00
this.setMessageState();
this.setMessageHint();
2019-02-01 20:13:45 +08:00
if (!mobile) {
if (this.textarea) this.textarea.focus();
}
}
componentDidUpdate(prevProps) {
const {
chatId,
connected,
locked,
partnerIsLoggedOut,
} = this.props;
2019-02-01 20:13:45 +08:00
const { message } = this.state;
const { mobile } = this.BROWSER_RESULTS;
2019-02-01 20:13:45 +08:00
if (prevProps.chatId !== chatId && !mobile) {
if (this.textarea) this.textarea.focus();
}
2019-02-01 20:13:45 +08:00
if (prevProps.chatId !== chatId) {
2019-02-09 03:23:35 +08:00
this.updateUnsentMessagesCollection(prevProps.chatId, message);
this.setMessageState();
2019-02-01 20:13:45 +08:00
}
2019-06-18 04:49:59 +08:00
if (
connected !== prevProps.connected
|| locked !== prevProps.locked
|| partnerIsLoggedOut !== prevProps.partnerIsLoggedOut
) {
2019-06-18 04:49:59 +08:00
this.setMessageHint();
}
2019-02-01 20:13:45 +08:00
}
2019-02-09 03:23:35 +08:00
componentWillUnmount() {
const { chatId } = this.props;
const { message } = this.state;
this.updateUnsentMessagesCollection(chatId, message);
this.setMessageState();
}
2019-06-18 04:49:59 +08:00
setMessageHint() {
const {
connected,
disabled,
intl,
locked,
partnerIsLoggedOut,
} = this.props;
let chatDisabledHint = null;
if (disabled && !partnerIsLoggedOut) {
if (connected) {
if (locked) {
chatDisabledHint = messages.errorChatLocked;
}
} else {
chatDisabledHint = messages.errorServerDisconnected;
}
}
2019-06-18 04:49:59 +08:00
this.setState({
hasErrors: disabled,
error: chatDisabledHint ? intl.formatMessage(chatDisabledHint) : null,
2019-06-18 04:49:59 +08:00
});
}
2019-02-09 03:23:35 +08:00
setMessageState() {
const { chatId, UnsentMessagesCollection } = this.props;
const unsentMessageByChat = UnsentMessagesCollection.findOne({ chatId });
this.setState({ message: unsentMessageByChat ? unsentMessageByChat.message : '' });
}
updateUnsentMessagesCollection(chatId, message) {
const { UnsentMessagesCollection } = this.props;
UnsentMessagesCollection.upsert(
{ chatId },
{ $set: { message } },
);
}
2016-07-08 02:52:21 +08:00
handleMessageKeyDown(e) {
2017-06-03 03:25:02 +08:00
// TODO Prevent send message pressing enter on mobile and/or virtual keyboard
2016-06-02 00:33:19 +08:00
if (e.keyCode === 13 && !e.shiftKey) {
2016-07-08 02:52:21 +08:00
e.preventDefault();
2016-06-02 00:33:19 +08:00
2017-06-03 03:25:02 +08:00
const event = new Event('submit', {
2016-07-08 02:52:21 +08:00
bubbles: true,
cancelable: true,
});
this.form.dispatchEvent(event);
2016-06-02 00:33:19 +08:00
}
}
handleMessageChange(e) {
2019-07-31 10:37:50 +08:00
const {
intl,
startUserTyping,
stopUserTyping,
minMessageLength,
maxMessageLength,
chatId,
} = this.props;
const message = e.target.value;
let error = '';
if (message.length < minMessageLength) {
error = intl.formatMessage(
messages.errorMinMessageLength,
{ 0: minMessageLength - message.length },
);
}
if (message.length > maxMessageLength) {
error = intl.formatMessage(
messages.errorMaxMessageLength,
{ 0: message.length - maxMessageLength },
);
}
2019-07-31 10:37:50 +08:00
const handleUserTyping = () => {
startUserTyping(chatId);
setTimeout(() => {
const { message: messageState } = this.state;
const userStoppedTyping = messageState === '' || message.length === messageState.length;
if (userStoppedTyping) stopUserTyping();
}, IS_TYPING_INTERVAL);
};
2019-02-01 20:13:45 +08:00
this.setState({
message,
error,
2019-07-31 10:37:50 +08:00
}, handleUserTyping);
2016-06-02 00:33:19 +08:00
}
handleSubmit(e) {
e.preventDefault();
const {
2019-07-31 10:37:50 +08:00
disabled,
minMessageLength,
maxMessageLength,
handleSendMessage,
stopUserTyping,
} = this.props;
const { message } = this.state;
let msg = message.trim();
2016-06-07 22:19:19 +08:00
2019-01-18 21:03:09 +08:00
if (disabled
|| msg.length === 0
|| msg.length < minMessageLength
|| msg.length > maxMessageLength) {
2017-06-05 21:52:46 +08:00
this.setState({ hasErrors: true });
2016-06-07 22:19:19 +08:00
return false;
}
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
2017-06-03 03:25:02 +08:00
const div = document.createElement('div');
div.appendChild(document.createTextNode(msg));
msg = div.innerHTML;
2016-06-02 00:33:19 +08:00
2019-01-18 21:03:09 +08:00
return (
handleSendMessage(msg),
this.setState({
message: '',
hasErrors: false,
2019-07-31 10:37:50 +08:00
}, stopUserTyping)
2019-01-18 21:03:09 +08:00
);
2016-06-02 00:33:19 +08:00
}
render() {
const {
2019-07-31 10:37:50 +08:00
intl,
chatTitle,
chatName,
disabled,
className,
chatAreaId,
stopUserTyping,
renderIsTypingString,
} = this.props;
const { hasErrors, error, message } = this.state;
2016-06-03 04:32:42 +08:00
return CHAT_ENABLED ? (
2016-06-02 00:33:19 +08:00
<form
ref={(ref) => { this.form = ref; }}
className={cx(className, styles.form)}
2017-06-03 03:25:02 +08:00
onSubmit={this.handleSubmit}
>
<div className={styles.wrapper}>
<TextareaAutosize
className={styles.input}
id="message-input"
innerRef={(ref) => { this.textarea = ref; return this.textarea; }}
2019-08-10 01:37:50 +08:00
placeholder={error === '' ? intl.formatMessage(messages.inputPlaceholder, { 0: chatName }) : error}
aria-controls={chatAreaId}
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
2017-06-05 21:52:46 +08:00
aria-invalid={hasErrors ? 'true' : 'false'}
aria-describedby={hasErrors ? 'message-input-error' : null}
autoCorrect="off"
autoComplete="off"
spellCheck="true"
disabled={disabled}
value={message}
onChange={this.handleMessageChange}
onKeyDown={this.handleMessageKeyDown}
/>
<Button
2017-08-05 01:58:55 +08:00
hideLabel
circle
className={styles.sendButton}
aria-label={intl.formatMessage(messages.submitLabel)}
type="submit"
disabled={disabled}
label={intl.formatMessage(messages.submitLabel)}
2017-08-05 01:58:55 +08:00
color="primary"
icon="send"
2019-07-31 10:37:50 +08:00
onClick={() => {}}
/>
</div>
<div className={styles.info}>
2019-08-10 01:37:50 +08:00
<span>
<span>{renderIsTypingString()}</span>
{renderIsTypingString() ? <span className={styles.connectingAnimation} /> : null}
</span>
</div>
2016-06-02 00:33:19 +08:00
</form>
) : null;
2016-06-02 00:33:19 +08:00
}
}
MessageForm.propTypes = propTypes;
MessageForm.defaultProps = defaultProps;
2016-06-03 04:32:42 +08:00
export default injectIntl(MessageForm);