2018-12-17 19:48:34 +08:00
|
|
|
import React, { PureComponent } from 'react';
|
2020-05-26 04:00:13 +08:00
|
|
|
import { defineMessages, injectIntl } from 'react-intl';
|
2022-05-10 20:47:51 +08:00
|
|
|
import { checkText } from 'smile2emoji';
|
2021-04-01 19:14:24 +08:00
|
|
|
import deviceInfo from '/imports/utils/deviceInfo';
|
2019-07-18 05:00:33 +08:00
|
|
|
import PropTypes from 'prop-types';
|
2020-02-03 22:10:02 +08:00
|
|
|
import _ from 'lodash';
|
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';
|
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 = {
|
2020-05-26 04:00:13 +08:00
|
|
|
intl: PropTypes.object.isRequired,
|
2019-07-18 05:00:33 +08:00
|
|
|
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,
|
2019-08-13 03:25:52 +08:00
|
|
|
UnsentMessagesCollection: PropTypes.objectOf(Object).isRequired,
|
2019-08-02 22:40:43 +08:00
|
|
|
connected: PropTypes.bool.isRequired,
|
|
|
|
locked: PropTypes.bool.isRequired,
|
2019-08-03 00:39:27 +08:00
|
|
|
partnerIsLoggedOut: PropTypes.bool.isRequired,
|
2019-08-13 03:25:52 +08:00
|
|
|
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',
|
|
|
|
},
|
2017-06-01 22:24:29 +08:00
|
|
|
errorMaxMessageLength: {
|
|
|
|
id: 'app.chat.errorMaxMessageLength',
|
|
|
|
},
|
2019-06-18 04:49:59 +08:00
|
|
|
errorServerDisconnected: {
|
|
|
|
id: 'app.chat.disconnected',
|
|
|
|
},
|
2019-08-02 22:40:43 +08:00
|
|
|
errorChatLocked: {
|
|
|
|
id: 'app.chat.locked',
|
|
|
|
},
|
2019-08-22 05:56:32 +08:00
|
|
|
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
|
|
|
});
|
|
|
|
|
2021-02-09 20:06:48 +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;
|
2022-05-20 04:41:18 +08:00
|
|
|
const ENABLE_EMOJI_PICKER = Meteor.settings.public.chat.enableEmojiPicker;
|
2019-07-18 05:00:33 +08:00
|
|
|
|
2018-12-17 19:48:34 +08:00
|
|
|
class MessageForm extends PureComponent {
|
2016-06-02 00:33:19 +08:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
message: '',
|
2019-08-13 03:25:52 +08:00
|
|
|
error: null,
|
2017-06-01 22:24:29 +08:00
|
|
|
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);
|
2020-02-03 22:10:02 +08:00
|
|
|
this.handleUserTyping = _.throttle(this.handleUserTyping.bind(this), 2000, { trailing: false });
|
2021-02-09 20:06:48 +08:00
|
|
|
this.typingIndicator = CHAT_CONFIG.typingIndicator.enabled;
|
2016-06-02 00:33:19 +08:00
|
|
|
}
|
|
|
|
|
2019-01-03 23:41:20 +08:00
|
|
|
componentDidMount() {
|
2021-04-01 19:14:24 +08:00
|
|
|
const { isMobile } = deviceInfo;
|
2019-02-09 03:23:35 +08:00
|
|
|
this.setMessageState();
|
2019-08-02 22:40:43 +08:00
|
|
|
this.setMessageHint();
|
2019-02-01 20:13:45 +08:00
|
|
|
|
2021-04-01 19:14:24 +08:00
|
|
|
if (!isMobile) {
|
2019-07-18 05:00:33 +08:00
|
|
|
if (this.textarea) this.textarea.focus();
|
2019-01-03 23:41:20 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidUpdate(prevProps) {
|
2019-08-03 00:39:27 +08:00
|
|
|
const {
|
|
|
|
chatId,
|
|
|
|
connected,
|
|
|
|
locked,
|
|
|
|
partnerIsLoggedOut,
|
|
|
|
} = this.props;
|
2019-02-01 20:13:45 +08:00
|
|
|
const { message } = this.state;
|
2021-04-01 19:14:24 +08:00
|
|
|
const { isMobile } = deviceInfo;
|
2019-01-03 23:41:20 +08:00
|
|
|
|
2021-04-01 19:14:24 +08:00
|
|
|
if (prevProps.chatId !== chatId && !isMobile) {
|
2019-07-18 05:00:33 +08:00
|
|
|
if (this.textarea) this.textarea.focus();
|
2019-01-03 23:41:20 +08:00
|
|
|
}
|
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);
|
2019-08-14 23:34:20 +08:00
|
|
|
this.setState(
|
|
|
|
{
|
|
|
|
error: null,
|
|
|
|
hasErrors: false,
|
|
|
|
}, this.setMessageState(),
|
|
|
|
);
|
2019-02-01 20:13:45 +08:00
|
|
|
}
|
2019-06-18 04:49:59 +08:00
|
|
|
|
2019-08-03 00:39:27 +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() {
|
2019-08-03 00:39:27 +08:00
|
|
|
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-08-02 22:40:43 +08:00
|
|
|
|
2019-06-18 04:49:59 +08:00
|
|
|
this.setState({
|
|
|
|
hasErrors: disabled,
|
2019-08-03 00:39:27 +08:00
|
|
|
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;
|
2019-08-22 20:05:06 +08:00
|
|
|
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 } },
|
|
|
|
);
|
2019-01-03 23:41:20 +08:00
|
|
|
}
|
|
|
|
|
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;
|
2021-02-09 20:06:48 +08:00
|
|
|
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;
|
2017-06-01 22:24:29 +08:00
|
|
|
|
2022-05-10 20:47:51 +08:00
|
|
|
let message = null;
|
2019-08-13 03:25:52 +08:00
|
|
|
let error = null;
|
2017-06-01 22:24:29 +08:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-06-01 22:24:29 +08:00
|
|
|
if (message.length > maxMessageLength) {
|
2018-06-29 01:12:50 +08:00
|
|
|
error = intl.formatMessage(
|
|
|
|
messages.errorMaxMessageLength,
|
|
|
|
{ 0: message.length - maxMessageLength },
|
|
|
|
);
|
2017-06-01 22:24:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
2018-12-29 05:48:59 +08:00
|
|
|
const {
|
2019-07-31 10:37:50 +08:00
|
|
|
disabled,
|
|
|
|
minMessageLength,
|
|
|
|
maxMessageLength,
|
|
|
|
handleSendMessage,
|
|
|
|
stopUserTyping,
|
2018-12-29 05:48:59 +08:00
|
|
|
} = this.props;
|
|
|
|
const { message } = this.state;
|
|
|
|
let msg = message.trim();
|
2016-06-07 22:19:19 +08:00
|
|
|
|
2020-03-07 01:12:35 +08:00
|
|
|
if (msg.length < minMessageLength) return;
|
2019-01-18 21:03:09 +08:00
|
|
|
|
2017-06-01 22:24:29 +08:00
|
|
|
if (disabled
|
2018-12-29 05:48:59 +08:00
|
|
|
|| 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
|
|
|
}
|
|
|
|
|
2016-06-03 03:08:17 +08:00
|
|
|
// Sanitize. See: http://shebang.brandonmintern.com/foolproof-html-escaping-in-javascript/
|
|
|
|
|
2017-06-03 03:25:02 +08:00
|
|
|
const div = document.createElement('div');
|
2018-12-29 05:48:59 +08:00
|
|
|
div.appendChild(document.createTextNode(msg));
|
|
|
|
msg = div.innerHTML;
|
2016-06-02 00:33:19 +08:00
|
|
|
|
2021-02-09 20:06:48 +08:00
|
|
|
const callback = this.typingIndicator ? stopUserTyping : null;
|
|
|
|
|
2021-05-18 04:25:07 +08:00
|
|
|
handleSendMessage(msg);
|
2022-05-20 04:41:18 +08:00
|
|
|
this.setState({ message: '', 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;
|
|
|
|
this.setState({ message: message + emojiObject.native });
|
|
|
|
}
|
|
|
|
|
|
|
|
renderEmojiPicker() {
|
|
|
|
const { showEmojiPicker } = this.state;
|
|
|
|
|
|
|
|
if (showEmojiPicker) {
|
|
|
|
return (
|
|
|
|
<Styled.EmojiPickerWrapper>
|
|
|
|
<Styled.EmojiPicker
|
|
|
|
onEmojiSelect={(emojiObject) => this.handleEmojiSelect(emojiObject)}
|
|
|
|
showPreview={false}
|
|
|
|
showSkinTones={false}
|
|
|
|
/>
|
|
|
|
</Styled.EmojiPickerWrapper>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
renderEmojiButton() {
|
|
|
|
const { intl } = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Styled.EmojiButton
|
|
|
|
onClick={() => this.setState((prevState) => ({
|
|
|
|
showEmojiPicker: !prevState.showEmojiPicker,
|
|
|
|
}))}
|
|
|
|
icon="happy"
|
|
|
|
color="dark"
|
|
|
|
ghost
|
|
|
|
circle
|
|
|
|
hideLabel
|
|
|
|
label={intl.formatMessage(messages.emojiButtonLabel)}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
renderForm() {
|
2018-06-29 01:12:50 +08:00
|
|
|
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,
|
2021-04-09 22:16:49 +08:00
|
|
|
partnerIsLoggedOut,
|
2018-06-29 01:12:50 +08:00
|
|
|
} = this.props;
|
2017-06-01 22:24:29 +08:00
|
|
|
|
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
|
2017-06-04 07:58:27 +08:00
|
|
|
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
|
2017-06-01 22:24:29 +08:00
|
|
|
id="message-input"
|
2019-01-03 23:41:20 +08:00
|
|
|
innerRef={(ref) => { this.textarea = ref; return this.textarea; }}
|
2021-02-09 20:44:01 +08:00
|
|
|
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
|
2017-06-01 22:24:29 +08:00
|
|
|
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
|
2017-06-05 21:52:46 +08:00
|
|
|
aria-invalid={hasErrors ? 'true' : 'false'}
|
2017-06-01 22:24:29 +08:00
|
|
|
autoCorrect="off"
|
|
|
|
autoComplete="off"
|
|
|
|
spellCheck="true"
|
2021-04-09 22:16:49 +08:00
|
|
|
disabled={disabled || partnerIsLoggedOut}
|
2018-12-29 05:48:59 +08:00
|
|
|
value={message}
|
2017-06-01 22:24:29 +08:00
|
|
|
onChange={this.handleMessageChange}
|
|
|
|
onKeyDown={this.handleMessageKeyDown}
|
2021-03-29 10:12:58 +08:00
|
|
|
async
|
2017-06-01 22:24:29 +08:00
|
|
|
/>
|
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
|
2017-06-01 22:24:29 +08:00
|
|
|
aria-label={intl.formatMessage(messages.submitLabel)}
|
|
|
|
type="submit"
|
2021-04-09 22:16:49 +08:00
|
|
|
disabled={disabled || partnerIsLoggedOut}
|
2017-06-01 22:24:29 +08:00
|
|
|
label={intl.formatMessage(messages.submitLabel)}
|
2017-08-05 01:58:55 +08:00
|
|
|
color="primary"
|
2017-06-01 22:24:29 +08:00
|
|
|
icon="send"
|
2021-05-18 04:25:07 +08:00
|
|
|
onClick={() => { }}
|
2019-08-24 03:44:47 +08:00
|
|
|
data-test="sendMessageButton"
|
2017-04-05 21:21:30 +08:00
|
|
|
/>
|
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);
|