You've already forked mattermost-webapp
mirror of
https://github.com/zerotier/mattermost-webapp.git
synced 2026-05-22 16:23:25 -07:00
975 lines
32 KiB
React
975 lines
32 KiB
React
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import $ from 'jquery';
|
|
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import {FormattedMessage, intlShape} from 'react-intl';
|
|
|
|
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
|
|
|
|
import * as GlobalActions from 'actions/global_actions.jsx';
|
|
|
|
import Constants from 'utils/constants.jsx';
|
|
import * as UserAgent from 'utils/user_agent.jsx';
|
|
import * as Utils from 'utils/utils.jsx';
|
|
import {containsAtChannel, postMessageOnKeyPress, shouldFocusMainTextbox, isErrorInvalidSlashCommand} from 'utils/post_utils.jsx';
|
|
import {getTable, formatMarkdownTableMessage} from 'utils/paste.jsx';
|
|
|
|
import ConfirmModal from 'components/confirm_modal.jsx';
|
|
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay.jsx';
|
|
import FilePreview from 'components/file_preview/file_preview.jsx';
|
|
import FileUpload from 'components/file_upload';
|
|
import MsgTyping from 'components/msg_typing';
|
|
import PostDeletedModal from 'components/post_deleted_modal.jsx';
|
|
import EmojiIcon from 'components/svg/emoji_icon';
|
|
import Textbox from 'components/textbox';
|
|
import FormattedMarkdownMessage from 'components/formatted_markdown_message.jsx';
|
|
import MessageSubmitError from 'components/message_submit_error';
|
|
|
|
export default class CreateComment extends React.PureComponent {
|
|
static propTypes = {
|
|
|
|
/**
|
|
* The channel for which this comment is a part of
|
|
*/
|
|
channelId: PropTypes.string.isRequired,
|
|
|
|
/**
|
|
* The number of channel members
|
|
*/
|
|
channelMembersCount: PropTypes.number.isRequired,
|
|
|
|
/**
|
|
* The id of the parent post
|
|
*/
|
|
rootId: PropTypes.string.isRequired,
|
|
|
|
/**
|
|
* True if the root message was deleted
|
|
*/
|
|
rootDeleted: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* The current history message selected
|
|
*/
|
|
messageInHistory: PropTypes.string,
|
|
|
|
/**
|
|
* The current draft of the comment
|
|
*/
|
|
draft: PropTypes.shape({
|
|
message: PropTypes.string.isRequired,
|
|
uploadsInProgress: PropTypes.array.isRequired,
|
|
fileInfos: PropTypes.array.isRequired,
|
|
}).isRequired,
|
|
|
|
/**
|
|
* Whether the submit button is enabled
|
|
*/
|
|
enableAddButton: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* Force message submission on CTRL/CMD + ENTER
|
|
*/
|
|
codeBlockOnCtrlEnter: PropTypes.bool,
|
|
|
|
/**
|
|
* Set to force form submission on CTRL/CMD + ENTER instead of ENTER
|
|
*/
|
|
ctrlSend: PropTypes.bool,
|
|
|
|
/**
|
|
* The id of the latest post in this channel
|
|
*/
|
|
latestPostId: PropTypes.string,
|
|
locale: PropTypes.string.isRequired,
|
|
|
|
/**
|
|
* A function returning a ref to the sidebar
|
|
*/
|
|
getSidebarBody: PropTypes.func,
|
|
|
|
/**
|
|
* Create post error id
|
|
*/
|
|
createPostErrorId: PropTypes.string,
|
|
|
|
/**
|
|
* Called to clear file uploads in progress
|
|
*/
|
|
clearCommentDraftUploads: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called when comment draft needs to be updated
|
|
*/
|
|
onUpdateCommentDraft: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called when comment draft needs to be updated for an specific root ID
|
|
*/
|
|
updateCommentDraftWithRootId: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called when submitting the comment
|
|
*/
|
|
onSubmit: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called when resetting comment message history index
|
|
*/
|
|
onResetHistoryIndex: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called when navigating back through comment message history
|
|
*/
|
|
onMoveHistoryIndexBack: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called when navigating forward through comment message history
|
|
*/
|
|
onMoveHistoryIndexForward: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Called to initiate editing the user's latest post
|
|
*/
|
|
onEditLatestPost: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Function to get the users timezones in the channel
|
|
*/
|
|
getChannelTimezones: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Reset state of createPost request
|
|
*/
|
|
resetCreatePostRequest: PropTypes.func.isRequired,
|
|
|
|
/**
|
|
* Set if channel is read only
|
|
*/
|
|
readOnlyChannel: PropTypes.bool,
|
|
|
|
/**
|
|
* Set if @channel should warn in this channel.
|
|
*/
|
|
enableConfirmNotificationsToChannel: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* Set if the emoji picker is enabled.
|
|
*/
|
|
enableEmojiPicker: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* Set if the gif picker is enabled.
|
|
*/
|
|
enableGifPicker: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* Set if the connection may be bad to warn user
|
|
*/
|
|
badConnection: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* The maximum length of a post
|
|
*/
|
|
maxPostSize: PropTypes.number.isRequired,
|
|
rhsExpanded: PropTypes.bool.isRequired,
|
|
|
|
/**
|
|
* To check if the timezones are enable on the server.
|
|
*/
|
|
isTimezoneEnabled: PropTypes.bool.isRequired,
|
|
}
|
|
|
|
static contextTypes = {
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
showPostDeletedModal: false,
|
|
showConfirmModal: false,
|
|
showEmojiPicker: false,
|
|
draft: {
|
|
message: '',
|
|
uploadsInProgress: [],
|
|
fileInfos: [],
|
|
},
|
|
channelTimezoneCount: 0,
|
|
uploadsProgressPercent: {},
|
|
renderScrollbar: false,
|
|
};
|
|
|
|
this.lastBlurAt = 0;
|
|
this.draftsForPost = {};
|
|
this.doInitialScrollToBottom = false;
|
|
}
|
|
|
|
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
|
|
this.props.clearCommentDraftUploads();
|
|
this.props.onResetHistoryIndex();
|
|
this.setState({draft: {...this.props.draft, uploadsInProgress: []}});
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.focusTextbox();
|
|
document.addEventListener('paste', this.pasteHandler);
|
|
document.addEventListener('keydown', this.focusTextboxIfNecessary);
|
|
|
|
// When draft.message is not empty, set doInitialScrollToBottom to true so that
|
|
// on next component update, the actual this.scrollToBottom() will be called.
|
|
// This is made so that the this.scrollToBottom() will be called only once.
|
|
if (this.props.draft.message !== '') {
|
|
this.doInitialScrollToBottom = true;
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.props.resetCreatePostRequest();
|
|
document.removeEventListener('paste', this.pasteHandler);
|
|
document.removeEventListener('keydown', this.focusTextboxIfNecessary);
|
|
}
|
|
|
|
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
|
|
if (newProps.createPostErrorId === 'api.post.create_post.root_id.app_error' && newProps.createPostErrorId !== this.props.createPostErrorId) {
|
|
this.showPostDeletedModal();
|
|
}
|
|
if (newProps.rootId !== this.props.rootId) {
|
|
this.setState({draft: {...newProps.draft, uploadsInProgress: []}});
|
|
}
|
|
|
|
if (this.props.messageInHistory !== newProps.messageInHistory) {
|
|
this.setState({draft: newProps.draft});
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
if (prevState.draft.uploadsInProgress.length < this.state.draft.uploadsInProgress.length) {
|
|
this.scrollToBottom();
|
|
}
|
|
|
|
if (prevProps.rootId !== this.props.rootId) {
|
|
this.focusTextbox();
|
|
}
|
|
|
|
if (this.doInitialScrollToBottom) {
|
|
this.scrollToBottom();
|
|
this.doInitialScrollToBottom = false;
|
|
}
|
|
}
|
|
|
|
focusTextboxIfNecessary = (e) => {
|
|
// Should only focus if RHS is expanded
|
|
if (!this.props.rhsExpanded) {
|
|
return;
|
|
}
|
|
|
|
// Bit of a hack to not steal focus from the channel switch modal if it's open
|
|
// This is a special case as the channel switch modal does not enforce focus like
|
|
// most modals do
|
|
if (document.getElementsByClassName('channel-switch-modal').length) {
|
|
return;
|
|
}
|
|
|
|
if (shouldFocusMainTextbox(e, document.activeElement)) {
|
|
this.focusTextbox();
|
|
}
|
|
}
|
|
|
|
pasteHandler = (e) => {
|
|
if (!e.clipboardData || !e.clipboardData.items || e.target.id !== 'reply_textbox') {
|
|
return;
|
|
}
|
|
|
|
const table = getTable(e.clipboardData);
|
|
if (!table) {
|
|
return;
|
|
}
|
|
|
|
e.preventDefault();
|
|
|
|
const {draft} = this.state;
|
|
const message = formatMarkdownTableMessage(table, draft.message.trim());
|
|
const updatedDraft = {...draft, message};
|
|
|
|
this.props.onUpdateCommentDraft(updatedDraft);
|
|
this.setState({draft: updatedDraft});
|
|
}
|
|
|
|
handleNotifyAllConfirmation = (e) => {
|
|
this.hideNotifyAllModal();
|
|
this.doSubmit(e);
|
|
}
|
|
|
|
hideNotifyAllModal = () => {
|
|
this.setState({showConfirmModal: false});
|
|
}
|
|
|
|
showNotifyAllModal = () => {
|
|
this.setState({showConfirmModal: true});
|
|
}
|
|
|
|
toggleEmojiPicker = () => {
|
|
this.setState({showEmojiPicker: !this.state.showEmojiPicker});
|
|
}
|
|
|
|
hideEmojiPicker = () => {
|
|
this.setState({showEmojiPicker: false});
|
|
}
|
|
|
|
handleEmojiClick = (emoji) => {
|
|
const emojiAlias = emoji.name || emoji.aliases[0];
|
|
|
|
if (!emojiAlias) {
|
|
//Oops.. There went something wrong
|
|
return;
|
|
}
|
|
|
|
const {draft} = this.state;
|
|
|
|
let newMessage = '';
|
|
if (draft.message === '') {
|
|
newMessage = `:${emojiAlias}: `;
|
|
} else if ((/\s+$/).test(draft.message)) {
|
|
// Check whether there is already a blank at the end of the current message
|
|
newMessage = `${draft.message}:${emojiAlias}: `;
|
|
} else {
|
|
newMessage = `${draft.message} :${emojiAlias}: `;
|
|
}
|
|
|
|
const modifiedDraft = {
|
|
...draft,
|
|
message: newMessage,
|
|
};
|
|
|
|
this.props.onUpdateCommentDraft(modifiedDraft);
|
|
this.draftsForPost[this.props.rootId] = modifiedDraft;
|
|
|
|
this.setState({
|
|
showEmojiPicker: false,
|
|
draft: modifiedDraft,
|
|
});
|
|
|
|
this.focusTextbox();
|
|
}
|
|
|
|
handleGifClick = (gif) => {
|
|
const {draft} = this.state;
|
|
|
|
let newMessage = '';
|
|
if (draft.message === '') {
|
|
newMessage = gif;
|
|
} else if ((/\s+$/).test(draft.message)) {
|
|
// Check whether there is already a blank at the end of the current message
|
|
newMessage = `${draft.message}${gif} `;
|
|
} else {
|
|
newMessage = `${draft.message} ${gif} `;
|
|
}
|
|
|
|
const modifiedDraft = {
|
|
...draft,
|
|
message: newMessage,
|
|
};
|
|
|
|
this.props.onUpdateCommentDraft(modifiedDraft);
|
|
this.draftsForPost[this.props.rootId] = modifiedDraft;
|
|
|
|
this.setState({
|
|
showEmojiPicker: false,
|
|
draft: modifiedDraft,
|
|
});
|
|
|
|
this.focusTextbox();
|
|
}
|
|
|
|
handlePostError = (postError) => {
|
|
this.setState({postError});
|
|
}
|
|
|
|
handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
|
|
const membersCount = this.props.channelMembersCount;
|
|
const notificationsToChannel = this.props.enableConfirmNotificationsToChannel;
|
|
if (notificationsToChannel &&
|
|
membersCount > Constants.NOTIFY_ALL_MEMBERS &&
|
|
containsAtChannel(this.state.draft.message)) {
|
|
if (this.props.isTimezoneEnabled) {
|
|
const {data} = await this.props.getChannelTimezones(this.props.channelId);
|
|
if (data) {
|
|
this.setState({channelTimezoneCount: data.length});
|
|
} else {
|
|
this.setState({channelTimezoneCount: 0});
|
|
}
|
|
}
|
|
this.showNotifyAllModal();
|
|
return;
|
|
}
|
|
|
|
await this.doSubmit(e);
|
|
}
|
|
|
|
doSubmit = async (e) => {
|
|
if (e) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
const {draft} = this.state;
|
|
const enableAddButton = this.shouldEnableAddButton();
|
|
|
|
if (!enableAddButton) {
|
|
return;
|
|
}
|
|
|
|
if (draft.uploadsInProgress.length > 0) {
|
|
return;
|
|
}
|
|
|
|
if (this.state.postError) {
|
|
this.setState({errorClass: 'animation--highlight'});
|
|
setTimeout(() => {
|
|
this.setState({errorClass: null});
|
|
}, Constants.ANIMATION_TIMEOUT);
|
|
return;
|
|
}
|
|
|
|
if (this.props.rootDeleted) {
|
|
this.showPostDeletedModal();
|
|
return;
|
|
}
|
|
|
|
const fasterThanHumanWillClick = 150;
|
|
const forceFocus = (Date.now() - this.lastBlurAt < fasterThanHumanWillClick);
|
|
this.focusTextbox(forceFocus);
|
|
|
|
const serverError = this.state.serverError;
|
|
let ignoreSlash = false;
|
|
if (isErrorInvalidSlashCommand(serverError) && draft.message === serverError.submittedMessage) {
|
|
ignoreSlash = true;
|
|
}
|
|
|
|
const options = {ignoreSlash};
|
|
|
|
try {
|
|
await this.props.onSubmit(options);
|
|
|
|
this.setState({
|
|
postError: null,
|
|
serverError: null,
|
|
});
|
|
} catch (err) {
|
|
if (isErrorInvalidSlashCommand(err)) {
|
|
this.props.onUpdateCommentDraft(draft);
|
|
}
|
|
err.submittedMessage = draft.message;
|
|
this.setState({serverError: err});
|
|
return;
|
|
}
|
|
|
|
this.setState({draft: {...this.props.draft, uploadsInProgress: []}});
|
|
}
|
|
|
|
commentMsgKeyPress = (e) => {
|
|
const {
|
|
ctrlSend,
|
|
codeBlockOnCtrlEnter,
|
|
} = this.props;
|
|
|
|
const {allowSending, withClosedCodeBlock, message} = postMessageOnKeyPress(e, this.state.draft.message, ctrlSend, codeBlockOnCtrlEnter);
|
|
|
|
if (allowSending) {
|
|
e.persist();
|
|
if (this.refs.textbox) {
|
|
this.refs.textbox.getWrappedInstance().blur();
|
|
}
|
|
|
|
if (withClosedCodeBlock && message) {
|
|
const {draft} = this.state;
|
|
const updatedDraft = {...draft, message};
|
|
this.props.onUpdateCommentDraft(updatedDraft);
|
|
this.setState({draft: updatedDraft}, () => this.handleSubmit(e));
|
|
this.draftsForPost[this.props.rootId] = updatedDraft;
|
|
} else {
|
|
this.handleSubmit(e);
|
|
}
|
|
}
|
|
|
|
this.emitTypingEvent();
|
|
}
|
|
|
|
emitTypingEvent = () => {
|
|
const {channelId, rootId} = this.props;
|
|
GlobalActions.emitLocalUserTypingEvent(channelId, rootId);
|
|
}
|
|
|
|
scrollToBottom = () => {
|
|
const $el = $('.post-right__scroll');
|
|
if ($el[0]) {
|
|
$el.parent().scrollTop($el[0].scrollHeight);
|
|
}
|
|
}
|
|
|
|
handleChange = (e) => {
|
|
const message = e.target.value;
|
|
|
|
let serverError = this.state.serverError;
|
|
if (isErrorInvalidSlashCommand(serverError)) {
|
|
serverError = null;
|
|
}
|
|
|
|
const {draft} = this.state;
|
|
const updatedDraft = {...draft, message};
|
|
this.props.onUpdateCommentDraft(updatedDraft);
|
|
this.setState({draft: updatedDraft, serverError}, () => {
|
|
this.scrollToBottom();
|
|
});
|
|
this.draftsForPost[this.props.rootId] = updatedDraft;
|
|
}
|
|
|
|
handleKeyDown = (e) => {
|
|
if (
|
|
(this.props.ctrlSend || this.props.codeBlockOnCtrlEnter) &&
|
|
Utils.isKeyPressed(e, Constants.KeyCodes.ENTER) &&
|
|
(e.ctrlKey || e.metaKey)
|
|
) {
|
|
this.commentMsgKeyPress(e);
|
|
return;
|
|
}
|
|
|
|
const {draft} = this.state;
|
|
const {message} = draft;
|
|
|
|
if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && Utils.isKeyPressed(e, Constants.KeyCodes.UP) && message === '') {
|
|
e.preventDefault();
|
|
if (this.refs.textbox) {
|
|
this.refs.textbox.getWrappedInstance().blur();
|
|
}
|
|
|
|
const {data: canEditNow} = this.props.onEditLatestPost();
|
|
if (!canEditNow) {
|
|
this.focusTextbox(true);
|
|
}
|
|
}
|
|
|
|
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey) {
|
|
if (Utils.isKeyPressed(e, Constants.KeyCodes.UP)) {
|
|
e.preventDefault();
|
|
this.props.onMoveHistoryIndexBack();
|
|
} else if (Utils.isKeyPressed(e, Constants.KeyCodes.DOWN)) {
|
|
e.preventDefault();
|
|
this.props.onMoveHistoryIndexForward();
|
|
}
|
|
}
|
|
}
|
|
|
|
handleFileUploadChange = () => {
|
|
this.focusTextbox();
|
|
}
|
|
|
|
handleUploadStart = (clientIds) => {
|
|
const {draft} = this.state;
|
|
const uploadsInProgress = [...draft.uploadsInProgress, ...clientIds];
|
|
|
|
const modifiedDraft = {
|
|
...draft,
|
|
uploadsInProgress,
|
|
};
|
|
this.props.onUpdateCommentDraft(modifiedDraft);
|
|
this.setState({draft: modifiedDraft});
|
|
this.draftsForPost[this.props.rootId] = modifiedDraft;
|
|
|
|
// this is a bit redundant with the code that sets focus when the file input is clicked,
|
|
// but this also resets the focus after a drag and drop
|
|
this.focusTextbox();
|
|
}
|
|
|
|
handleUploadProgress = ({clientId, name, percent, type}) => {
|
|
const uploadsProgressPercent = {...this.state.uploadsProgressPercent, [clientId]: {percent, name, type}};
|
|
this.setState({uploadsProgressPercent});
|
|
}
|
|
|
|
handleFileUploadComplete = (fileInfos, clientIds, channelId, rootId) => {
|
|
const draft = this.draftsForPost[rootId];
|
|
const uploadsInProgress = [...draft.uploadsInProgress];
|
|
const newFileInfos = sortFileInfos([...draft.fileInfos, ...fileInfos], this.props.locale);
|
|
|
|
// remove each finished file from uploads
|
|
for (let i = 0; i < clientIds.length; i++) {
|
|
const index = uploadsInProgress.indexOf(clientIds[i]);
|
|
|
|
if (index !== -1) {
|
|
uploadsInProgress.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
const modifiedDraft = {
|
|
...draft,
|
|
fileInfos: newFileInfos,
|
|
uploadsInProgress,
|
|
};
|
|
this.props.updateCommentDraftWithRootId(rootId, modifiedDraft);
|
|
this.draftsForPost[rootId] = modifiedDraft;
|
|
if (this.props.rootId === rootId) {
|
|
this.setState({draft: modifiedDraft});
|
|
}
|
|
|
|
// Focus on preview if needed/possible - if user has switched teams since starting the file upload,
|
|
// the preview will be undefined and the switch will fail
|
|
if (typeof this.refs.preview != 'undefined' && this.refs.preview) {
|
|
this.refs.preview.refs.container.scrollIntoView();
|
|
}
|
|
}
|
|
|
|
handleUploadError = (err, clientId = -1, rootId = -1) => {
|
|
if (clientId !== -1) {
|
|
const draft = {...this.draftsForPost[rootId]};
|
|
const uploadsInProgress = [...draft.uploadsInProgress];
|
|
|
|
const index = uploadsInProgress.indexOf(clientId);
|
|
if (index !== -1) {
|
|
uploadsInProgress.splice(index, 1);
|
|
}
|
|
|
|
const modifiedDraft = {
|
|
...draft,
|
|
uploadsInProgress,
|
|
};
|
|
this.props.updateCommentDraftWithRootId(rootId, modifiedDraft);
|
|
this.draftsForPost[rootId] = modifiedDraft;
|
|
if (this.props.rootId === rootId) {
|
|
this.setState({draft: modifiedDraft});
|
|
}
|
|
}
|
|
|
|
let serverError = err;
|
|
if (err && typeof err === 'string') {
|
|
serverError = new Error(err);
|
|
}
|
|
|
|
this.setState({serverError});
|
|
}
|
|
|
|
removePreview = (id) => {
|
|
const {draft} = this.state;
|
|
const fileInfos = [...draft.fileInfos];
|
|
const uploadsInProgress = [...draft.uploadsInProgress];
|
|
|
|
// Clear previous errors
|
|
this.handleUploadError(null);
|
|
|
|
// id can either be the id of an uploaded file or the client id of an in progress upload
|
|
let index = fileInfos.findIndex((info) => info.id === id);
|
|
if (index === -1) {
|
|
index = uploadsInProgress.indexOf(id);
|
|
|
|
if (index !== -1) {
|
|
uploadsInProgress.splice(index, 1);
|
|
|
|
if (this.refs.fileUpload && this.refs.fileUpload.getWrappedInstance()) {
|
|
this.refs.fileUpload.getWrappedInstance().cancelUpload(id);
|
|
}
|
|
}
|
|
} else {
|
|
fileInfos.splice(index, 1);
|
|
}
|
|
|
|
const modifiedDraft = {
|
|
...draft,
|
|
fileInfos,
|
|
uploadsInProgress,
|
|
};
|
|
|
|
this.props.onUpdateCommentDraft(modifiedDraft);
|
|
this.setState({draft: modifiedDraft});
|
|
this.draftsForPost[this.props.rootId] = modifiedDraft;
|
|
|
|
this.handleFileUploadChange();
|
|
}
|
|
|
|
getFileCount = () => {
|
|
const {
|
|
draft: {
|
|
fileInfos,
|
|
uploadsInProgress,
|
|
},
|
|
} = this.state;
|
|
return fileInfos.length + uploadsInProgress.length;
|
|
}
|
|
|
|
getFileUploadTarget = () => {
|
|
return this.refs.textbox.getWrappedInstance();
|
|
}
|
|
|
|
getCreateCommentControls = () => {
|
|
return this.refs.createCommentControls;
|
|
}
|
|
|
|
shouldEnableAddButton = () => {
|
|
if (this.props.enableAddButton) {
|
|
return true;
|
|
}
|
|
|
|
return isErrorInvalidSlashCommand(this.state.serverError);
|
|
}
|
|
|
|
focusTextbox = (keepFocus = false) => {
|
|
if (this.refs.textbox && (keepFocus || !UserAgent.isMobile())) {
|
|
this.refs.textbox.getWrappedInstance().focus();
|
|
}
|
|
}
|
|
|
|
showPostDeletedModal = () => {
|
|
this.setState({
|
|
showPostDeletedModal: true,
|
|
});
|
|
}
|
|
|
|
hidePostDeletedModal = () => {
|
|
this.setState({
|
|
showPostDeletedModal: false,
|
|
});
|
|
|
|
this.props.resetCreatePostRequest();
|
|
}
|
|
|
|
handleBlur = () => {
|
|
this.lastBlurAt = Date.now();
|
|
}
|
|
|
|
handleHeightChange = (height, maxHeight) => {
|
|
this.setState({renderScrollbar: height > maxHeight});
|
|
}
|
|
|
|
render() {
|
|
const {draft} = this.state;
|
|
const {readOnlyChannel} = this.props;
|
|
const {formatMessage} = this.context.intl;
|
|
const enableAddButton = this.shouldEnableAddButton();
|
|
const {renderScrollbar} = this.state;
|
|
|
|
const notifyAllTitle = (
|
|
<FormattedMessage
|
|
id='notify_all.title.confirm'
|
|
defaultMessage='Confirm sending notifications to entire channel'
|
|
/>
|
|
);
|
|
|
|
const notifyAllConfirm = (
|
|
<FormattedMessage
|
|
id='notify_all.confirm'
|
|
defaultMessage='Confirm'
|
|
/>
|
|
);
|
|
|
|
let notifyAllMessage = '';
|
|
if (this.state.channelTimezoneCount && this.props.isTimezoneEnabled) {
|
|
notifyAllMessage = (
|
|
<FormattedMarkdownMessage
|
|
id='notify_all.question_timezone'
|
|
defaultMessage='By using @all or @channel you are about to send notifications to **{totalMembers} people** in **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Are you sure you want to do this?'
|
|
values={{
|
|
totalMembers: this.props.channelMembersCount - 1,
|
|
timezones: this.state.channelTimezoneCount,
|
|
}}
|
|
/>
|
|
);
|
|
} else {
|
|
notifyAllMessage = (
|
|
<FormattedMessage
|
|
id='notify_all.question'
|
|
defaultMessage='By using @all or @channel you are about to send notifications to {totalMembers} people. Are you sure you want to do this?'
|
|
values={{
|
|
totalMembers: this.props.channelMembersCount - 1,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
let serverError = null;
|
|
if (this.state.serverError) {
|
|
serverError = (
|
|
<MessageSubmitError
|
|
id='postServerError'
|
|
error={this.state.serverError}
|
|
submittedMessage={this.state.serverError.submittedMessage}
|
|
handleSubmit={this.handleSubmit}
|
|
/>
|
|
);
|
|
}
|
|
|
|
let postError = null;
|
|
if (this.state.postError) {
|
|
const postErrorClass = 'post-error' + (this.state.errorClass ? (' ' + this.state.errorClass) : '');
|
|
postError = <label className={postErrorClass}>{this.state.postError}</label>;
|
|
}
|
|
|
|
let preview = null;
|
|
if (!readOnlyChannel && (draft.fileInfos.length > 0 || draft.uploadsInProgress.length > 0)) {
|
|
preview = (
|
|
<FilePreview
|
|
fileInfos={draft.fileInfos}
|
|
onRemove={this.removePreview}
|
|
uploadsInProgress={draft.uploadsInProgress}
|
|
uploadsProgressPercent={this.state.uploadsProgressPercent}
|
|
ref='preview'
|
|
/>
|
|
);
|
|
}
|
|
|
|
let uploadsInProgressText = null;
|
|
if (draft.uploadsInProgress.length > 0) {
|
|
uploadsInProgressText = (
|
|
<span className='pull-right post-right-comments-upload-in-progress'>
|
|
{draft.uploadsInProgress.length === 1 ? (
|
|
<FormattedMessage
|
|
id='create_comment.file'
|
|
defaultMessage='File uploading'
|
|
/>
|
|
) : (
|
|
<FormattedMessage
|
|
id='create_comment.files'
|
|
defaultMessage='Files uploading'
|
|
/>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
let addButtonClass = 'btn btn-primary comment-btn pull-right';
|
|
if (!enableAddButton) {
|
|
addButtonClass += ' disabled';
|
|
}
|
|
|
|
let fileUpload;
|
|
if (!readOnlyChannel) {
|
|
fileUpload = (
|
|
<FileUpload
|
|
ref='fileUpload'
|
|
fileCount={this.getFileCount()}
|
|
getTarget={this.getFileUploadTarget}
|
|
onFileUploadChange={this.handleFileUploadChange}
|
|
onUploadStart={this.handleUploadStart}
|
|
onFileUpload={this.handleFileUploadComplete}
|
|
onUploadError={this.handleUploadError}
|
|
onUploadProgress={this.handleUploadProgress}
|
|
rootId={this.props.rootId}
|
|
postType='comment'
|
|
/>
|
|
);
|
|
}
|
|
|
|
let emojiPicker = null;
|
|
if (this.props.enableEmojiPicker && !readOnlyChannel) {
|
|
emojiPicker = (
|
|
<span
|
|
role='button'
|
|
tabIndex='0'
|
|
aria-label={formatMessage({id: 'create_post.open_emoji_picker', defaultMessage: 'Open emoji picker'})}
|
|
className='emoji-picker__container'
|
|
>
|
|
<EmojiPickerOverlay
|
|
show={this.state.showEmojiPicker}
|
|
target={this.getCreateCommentControls}
|
|
onHide={this.hideEmojiPicker}
|
|
onEmojiClose={this.hideEmojiPicker}
|
|
onEmojiClick={this.handleEmojiClick}
|
|
onGifClick={this.handleGifClick}
|
|
enableGifPicker={this.props.enableGifPicker}
|
|
topOffset={55}
|
|
/>
|
|
<EmojiIcon
|
|
className={'icon icon--emoji emoji-rhs ' + (this.state.showEmojiPicker ? 'active' : '')}
|
|
onClick={this.toggleEmojiPicker}
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
let createMessage;
|
|
if (readOnlyChannel) {
|
|
createMessage = Utils.localizeMessage('create_post.read_only', 'This channel is read-only. Only members with permission can post here.');
|
|
} else {
|
|
createMessage = Utils.localizeMessage('create_comment.addComment', 'Add a comment...');
|
|
}
|
|
|
|
let scrollbarClass = '';
|
|
if (renderScrollbar) {
|
|
scrollbarClass = ' scroll';
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={this.handleSubmit}>
|
|
<div className={'post-create' + scrollbarClass}>
|
|
<div
|
|
id={this.props.rootId}
|
|
className='post-create-body comment-create-body'
|
|
>
|
|
<div className='post-body__cell'>
|
|
<Textbox
|
|
onChange={this.handleChange}
|
|
onKeyPress={this.commentMsgKeyPress}
|
|
onKeyDown={this.handleKeyDown}
|
|
onComposition={this.emitTypingEvent}
|
|
onHeightChange={this.handleHeightChange}
|
|
handlePostError={this.handlePostError}
|
|
value={readOnlyChannel ? '' : draft.message}
|
|
onBlur={this.handleBlur}
|
|
createMessage={createMessage}
|
|
emojiEnabled={this.props.enableEmojiPicker}
|
|
initialText=''
|
|
channelId={this.props.channelId}
|
|
isRHS={true}
|
|
popoverMentionKeyClick={true}
|
|
id='reply_textbox'
|
|
ref='textbox'
|
|
disabled={readOnlyChannel}
|
|
characterLimit={this.props.maxPostSize}
|
|
badConnection={this.props.badConnection}
|
|
listenForMentionKeyClick={true}
|
|
/>
|
|
<span
|
|
ref='createCommentControls'
|
|
className='post-body__actions'
|
|
>
|
|
{fileUpload}
|
|
{emojiPicker}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<MsgTyping
|
|
channelId={this.props.channelId}
|
|
postId={this.props.rootId}
|
|
/>
|
|
<div className='post-create-footer'>
|
|
<input
|
|
type='button'
|
|
className={addButtonClass}
|
|
value={formatMessage({id: 'create_comment.comment', defaultMessage: 'Add Comment'})}
|
|
onClick={this.handleSubmit}
|
|
/>
|
|
{uploadsInProgressText}
|
|
{postError}
|
|
{preview}
|
|
{serverError}
|
|
</div>
|
|
</div>
|
|
<PostDeletedModal
|
|
show={this.state.showPostDeletedModal}
|
|
onHide={this.hidePostDeletedModal}
|
|
/>
|
|
<ConfirmModal
|
|
title={notifyAllTitle}
|
|
message={notifyAllMessage}
|
|
confirmButtonText={notifyAllConfirm}
|
|
show={this.state.showConfirmModal}
|
|
onConfirm={this.handleNotifyAllConfirmation}
|
|
onCancel={this.hideNotifyAllModal}
|
|
/>
|
|
</form>
|
|
);
|
|
}
|
|
}
|