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

393 lines
10 KiB
React
Raw Normal View History

import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
2022-05-10 20:47:51 +08:00
import { checkText } from 'smile2emoji';
import deviceInfo from '/imports/utils/deviceInfo';
import PropTypes from 'prop-types';
import { throttle } from '/imports/utils/throttle';
2019-08-23 02:53:53 +08:00
import TypingIndicatorContainer from './typing-indicator/container';
2022-05-20 04:41:18 +08:00
import ClickOutside from '/imports/ui/components/click-outside/component';
2021-11-02 20:26:36 +08:00
import Styled from './styles';
import { escapeHtml } from '/imports/utils/string-utils';
2022-03-09 22:19:25 +08:00
import { isChatEnabled } from '/imports/ui/services/features';
2016-06-02 00:33:19 +08:00
const propTypes = {
intl: PropTypes.object.isRequired,
chatId: PropTypes.string.isRequired,
disabled: PropTypes.bool.isRequired,
minMessageLength: PropTypes.number.isRequired,
maxMessageLength: PropTypes.number.isRequired,
chatTitle: PropTypes.string.isRequired,
chatAreaId: PropTypes.string.isRequired,
handleSendMessage: PropTypes.func.isRequired,
UnsentMessagesCollection: PropTypes.objectOf(Object).isRequired,
connected: PropTypes.bool.isRequired,
locked: PropTypes.bool.isRequired,
partnerIsLoggedOut: PropTypes.bool.isRequired,
stopUserTyping: PropTypes.func.isRequired,
startUserTyping: PropTypes.func.isRequired,
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',
},
2022-05-20 04:41:18 +08:00
emojiButtonLabel: {
id: 'app.chat.emojiButtonLabel',
description: 'Chat message emoji picker button label',
},
2016-12-07 01:07:22 +08:00
inputPlaceholder: {
id: 'app.chat.inputPlaceholder',
description: 'Chat message input placeholder',
},
errorMaxMessageLength: {
id: 'app.chat.errorMaxMessageLength',
},
2019-06-18 04:49:59 +08:00
errorServerDisconnected: {
id: 'app.chat.disconnected',
},
errorChatLocked: {
id: 'app.chat.locked',
},
singularTyping: {
id: 'app.chat.singularTyping',
description: 'used to indicate when 1 user is typing',
},
pluralTyping: {
id: 'app.chat.pluralTyping',
description: 'used to indicate when multiple user are typing',
},
severalPeople: {
id: 'app.chat.severalPeople',
description: 'displayed when 4 or more users are typing',
},
2016-06-03 04:32:42 +08:00
});
const CHAT_CONFIG = Meteor.settings.public.chat;
2022-05-10 20:47:51 +08:00
const AUTO_CONVERT_EMOJI = Meteor.settings.public.chat.autoConvertEmoji;
const ENABLE_EMOJI_PICKER = Meteor.settings.public.chat.emojiPicker.enable;
class MessageForm extends PureComponent {
2016-06-02 00:33:19 +08:00
constructor(props) {
super(props);
this.state = {
message: '',
error: null,
hasErrors: false,
2022-05-20 04:41:18 +08:00
showEmojiPicker: false,
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);
this.handleUserTyping = throttle(this.handleUserTyping.bind(this), 2000, { trailing: false });
this.typingIndicator = CHAT_CONFIG.typingIndicator.enabled;
2016-06-02 00:33:19 +08:00
}
componentDidMount() {
const { isMobile } = deviceInfo;
2019-02-09 03:23:35 +08:00
this.setMessageState();
this.setMessageHint();
2019-02-01 20:13:45 +08:00
if (!isMobile) {
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 { isMobile } = deviceInfo;
if (prevProps.chatId !== chatId && !isMobile) {
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.setState(
{
error: null,
hasErrors: false,
}, 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();
}
2022-05-20 04:41:18 +08:00
handleClickOutside() {
const { showEmojiPicker } = this.state;
if (showEmojiPicker) {
this.setState({ showEmojiPicker: false });
}
}
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 },
{ fields: { message: 1 } });
2019-02-09 03:23:35 +08:00
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,
});
2022-05-20 04:41:18 +08:00
this.handleSubmit(event);
2016-06-02 00:33:19 +08:00
}
}
2020-02-03 22:10:02 +08:00
handleUserTyping(error) {
const { startUserTyping, chatId } = this.props;
if (error || !this.typingIndicator) return;
2020-02-03 22:10:02 +08:00
startUserTyping(chatId);
}
2016-06-02 00:33:19 +08:00
handleMessageChange(e) {
2019-07-31 10:37:50 +08:00
const {
intl,
maxMessageLength,
} = this.props;
2022-05-10 20:47:51 +08:00
let message = null;
let error = null;
2022-05-11 00:23:41 +08:00
if (AUTO_CONVERT_EMOJI) {
2022-05-10 20:47:51 +08:00
message = checkText(e.target.value);
} else {
message = e.target.value;
}
if (message.length > maxMessageLength) {
error = intl.formatMessage(
messages.errorMaxMessageLength,
{ 0: maxMessageLength },
);
message = message.substring(0, maxMessageLength);
}
this.setState({
message,
error,
2020-02-03 22:10:02 +08:00
}, this.handleUserTyping(error));
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;
const msg = message.trim();
2016-06-07 22:19:19 +08:00
if (msg.length < minMessageLength) return;
2019-01-18 21:03:09 +08:00
if (disabled
|| msg.length > maxMessageLength) {
2017-06-05 21:52:46 +08:00
this.setState({ hasErrors: true });
2021-05-18 04:25:07 +08:00
return;
2016-06-07 22:19:19 +08:00
}
const callback = this.typingIndicator ? stopUserTyping : null;
handleSendMessage(escapeHtml(msg));
this.setState({ message: '', error: '', hasErrors: false, showEmojiPicker: false }, callback);
2016-06-02 00:33:19 +08:00
}
2022-05-20 04:41:18 +08:00
handleEmojiSelect(emojiObject) {
const { message } = this.state;
const cursor = this.textarea.selectionStart;
this.setState(
{
message: message.slice(0, cursor)
+ emojiObject.native
+ message.slice(cursor),
},
);
const newCursor = cursor + emojiObject.native.length;
setTimeout(() => this.textarea.setSelectionRange(newCursor, newCursor), 10);
2022-05-20 04:41:18 +08:00
}
renderEmojiPicker() {
const { showEmojiPicker } = this.state;
if (showEmojiPicker) {
return (
<Styled.EmojiPickerWrapper>
<Styled.EmojiPicker
onEmojiSelect={(emojiObject) => this.handleEmojiSelect(emojiObject)}
/>
</Styled.EmojiPickerWrapper>
);
}
return null;
}
renderEmojiButton() {
const { intl } = this.props;
return (
<Styled.EmojiButton
onClick={() => this.setState((prevState) => ({
showEmojiPicker: !prevState.showEmojiPicker,
}))}
icon="happy"
2022-08-20 02:19:10 +08:00
color="light"
2022-05-20 04:41:18 +08:00
ghost
type="button"
2022-05-20 04:41:18 +08:00
circle
hideLabel
label={intl.formatMessage(messages.emojiButtonLabel)}
2022-08-30 20:55:57 +08:00
data-test="emojiPickerButton"
2022-05-20 04:41:18 +08:00
/>
);
}
renderForm() {
const {
2019-07-31 10:37:50 +08:00
intl,
chatTitle,
2021-02-09 20:44:01 +08:00
title,
2019-07-31 10:37:50 +08:00
disabled,
2021-05-18 04:25:07 +08:00
idChatOpen,
partnerIsLoggedOut,
} = this.props;
2022-05-20 04:41:18 +08:00
const {
hasErrors, error, message,
} = this.state;
2016-06-03 04:32:42 +08:00
2022-05-20 04:41:18 +08:00
return (
2021-11-02 20:26:36 +08:00
<Styled.Form
ref={(ref) => { this.form = ref; }}
2017-06-03 03:25:02 +08:00
onSubmit={this.handleSubmit}
>
2022-05-20 04:41:18 +08:00
{this.renderEmojiPicker()}
2021-11-02 20:26:36 +08:00
<Styled.Wrapper>
<Styled.Input
id="message-input"
innerRef={(ref) => { this.textarea = ref; return this.textarea; }}
2021-02-09 20:44:01 +08:00
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
2017-06-05 21:52:46 +08:00
aria-invalid={hasErrors ? 'true' : 'false'}
autoCorrect="off"
autoComplete="off"
spellCheck="true"
disabled={disabled || partnerIsLoggedOut}
value={message}
onChange={this.handleMessageChange}
onKeyDown={this.handleMessageKeyDown}
2022-11-04 00:06:25 +08:00
onPaste={(e) => { e.stopPropagation(); }}
2022-11-04 03:24:00 +08:00
onCut={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }}
async
/>
2022-05-20 04:41:18 +08:00
{ENABLE_EMOJI_PICKER && this.renderEmojiButton()}
2021-11-02 20:26:36 +08:00
<Styled.SendButton
2017-08-05 01:58:55 +08:00
hideLabel
circle
aria-label={intl.formatMessage(messages.submitLabel)}
type="submit"
disabled={disabled || partnerIsLoggedOut}
label={intl.formatMessage(messages.submitLabel)}
2017-08-05 01:58:55 +08:00
color="primary"
icon="send"
2021-05-18 04:25:07 +08:00
onClick={() => { }}
data-test="sendMessageButton"
/>
2021-11-02 20:26:36 +08:00
</Styled.Wrapper>
2021-05-18 04:25:07 +08:00
<TypingIndicatorContainer {...{ idChatOpen, error }} />
2021-11-02 20:26:36 +08:00
</Styled.Form>
2022-05-20 04:41:18 +08:00
);
}
render() {
if (!isChatEnabled()) return null;
return ENABLE_EMOJI_PICKER ? (
<ClickOutside
onClick={() => this.handleClickOutside()}
>
{this.renderForm()}
</ClickOutside>
) : this.renderForm();
2016-06-02 00:33:19 +08:00
}
}
MessageForm.propTypes = propTypes;
2016-06-03 04:32:42 +08:00
export default injectIntl(MessageForm);