Files
mattermost/webapp/components/create_post.jsx
Asaad Mahmood f91b9d4a65 PLT-3418, PLT-2950, PLT-3375, PLT-3382, PLT-3446, PLT-3465: Multiple UI Improvements (#3449)
Improving help text margins

Updating changes for get link and create post files

Fixing icon for select team screen

Fixing styles for select team button

Adding improvements to posts UI

Adding improvement to post layout

Updating changes for post controls

Updating z-index for sidebar--right

Updating help text position

Fixing code for posts

Fixing css for post view

Pushing improvements for posts view

Updating changes for post view

Updating post layout

Fixing system time css

Updating header for system posts

Updating post css

Removing opacity and changing color for system messages

Simplifying root post and system post behaviour

Removing images from compact view

Updating help text for display

Updating embed preview text for advanced option

PLT-3490 - Fixing RHS issue on Edge
2016-07-05 15:46:36 -04:00

575 lines
20 KiB
JavaScript

// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import ReactDOM from 'react-dom';
import MsgTyping from './msg_typing.jsx';
import Textbox from './textbox.jsx';
import FileUpload from './file_upload.jsx';
import FilePreview from './file_preview.jsx';
import PostDeletedModal from './post_deleted_modal.jsx';
import TutorialTip from './tutorial/tutorial_tip.jsx';
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import Client from 'utils/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import PostStore from 'stores/post_store.jsx';
import MessageHistoryStore from 'stores/message_history_store.jsx';
import UserStore from 'stores/user_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import Constants from 'utils/constants.jsx';
import {intlShape, injectIntl, defineMessages, FormattedHTMLMessage} from 'react-intl';
import {browserHistory} from 'react-router/es6';
const Preferences = Constants.Preferences;
const TutorialSteps = Constants.TutorialSteps;
const ActionTypes = Constants.ActionTypes;
const KeyCodes = Constants.KeyCodes;
const holders = defineMessages({
comment: {
id: 'create_post.comment',
defaultMessage: 'Comment'
},
post: {
id: 'create_post.post',
defaultMessage: 'Post'
},
write: {
id: 'create_post.write',
defaultMessage: 'Write a message...'
}
});
import React from 'react';
class CreatePost extends React.Component {
constructor(props) {
super(props);
this.lastTime = 0;
this.getCurrentDraft = this.getCurrentDraft.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
this.handleUserInput = this.handleUserInput.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.onPreferenceChange = this.onPreferenceChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.sendMessage = this.sendMessage.bind(this);
this.focusTextbox = this.focusTextbox.bind(this);
this.showPostDeletedModal = this.showPostDeletedModal.bind(this);
this.hidePostDeletedModal = this.hidePostDeletedModal.bind(this);
this.showShortcuts = this.showShortcuts.bind(this);
PostStore.clearDraftUploads();
const draft = this.getCurrentDraft();
this.state = {
channelId: ChannelStore.getCurrentId(),
messageText: draft.messageText,
uploadsInProgress: draft.uploadsInProgress,
previews: draft.previews,
submitting: false,
initialText: draft.messageText,
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
showTutorialTip: false,
showPostDeletedModal: false,
typing: false
};
}
getCurrentDraft() {
const draft = PostStore.getCurrentDraft();
const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
if (draft) {
if (draft.message) {
safeDraft.messageText = draft.message;
}
if (draft.previews) {
safeDraft.previews = draft.previews;
}
if (draft.uploadsInProgress) {
safeDraft.uploadsInProgress = draft.uploadsInProgress;
}
}
return safeDraft;
}
handleSubmit(e) {
e.preventDefault();
if (this.state.uploadsInProgress.length > 0 || this.state.submitting) {
return;
}
const post = {};
post.filenames = [];
post.message = this.state.messageText;
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
return;
}
if (post.message.length > Constants.CHARACTER_LIMIT) {
this.setState({postError: `Post length must be less than ${Constants.CHARACTER_LIMIT} characters.`});
return;
}
MessageHistoryStore.storeMessageInHistory(this.state.messageText);
this.setState({submitting: true, serverError: null, typing: false});
if (post.message.indexOf('/') === 0) {
ChannelActions.executeCommand(
this.state.channelId,
post.message,
false,
(data) => {
PostStore.storeDraft(this.state.channelId, null);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
if (data.goto_location && data.goto_location.length > 0) {
browserHistory.push(data.goto_location);
}
},
(err) => {
if (err.sendMessage) {
this.sendMessage(post);
} else {
const state = {};
state.serverError = err.message;
state.submitting = false;
this.setState(state);
}
}
);
} else {
this.sendMessage(post);
}
}
sendMessage(post) {
post.channel_id = this.state.channelId;
post.filenames = this.state.previews;
const time = Utils.getTimestamp();
const userId = UserStore.getCurrentId();
post.pending_post_id = `${userId}:${time}`;
post.user_id = userId;
post.create_at = time;
post.parent_id = this.state.parentId;
GlobalActions.emitUserPostedEvent(post);
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
Client.createPost(post,
(data) => {
PostStore.removePendingPost(post.pending_post_id);
AppDispatcher.handleServerAction({
type: ActionTypes.RECEIVED_POST,
post: data
});
},
(err) => {
if (err.id === 'api.post.create_post.root_id.app_error') {
// this should never actually happen since you can't reply from this textbox
this.showPostDeletedModal();
PostStore.removePendingPost(post.pending_post_id);
} else {
post.state = Constants.POST_FAILED;
PostStore.updatePendingPost(post);
}
this.setState({
submitting: false
});
}
);
}
focusTextbox() {
if (!Utils.isMobile()) {
this.refs.textbox.focus();
}
}
postMsgKeyPress(e) {
if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
e.preventDefault();
ReactDOM.findDOMNode(this.refs.textbox).blur();
this.handleSubmit(e);
}
}
GlobalActions.emitLocalUserTypingEvent(this.state.channelId, '');
}
handleUserInput(messageText) {
const typing = messageText !== '';
this.setState({messageText, typing});
const draft = PostStore.getCurrentDraft();
draft.message = messageText;
PostStore.storeCurrentDraft(draft);
}
handleUploadClick() {
this.focusTextbox();
}
handleUploadStart(clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
PostStore.storeDraft(channelId, draft);
this.setState({uploadsInProgress: draft.uploadsInProgress});
// 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();
}
handleFileUploadComplete(filenames, clientIds, channelId) {
const draft = PostStore.getDraft(channelId);
// remove each finished file from uploads
for (let i = 0; i < clientIds.length; i++) {
const index = draft.uploadsInProgress.indexOf(clientIds[i]);
if (index !== -1) {
draft.uploadsInProgress.splice(index, 1);
}
}
draft.previews = draft.previews.concat(filenames);
PostStore.storeDraft(channelId, draft);
if (channelId === this.state.channelId) {
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
}
}
handleUploadError(err, clientId, channelId) {
let message = err;
if (message && typeof message !== 'string') {
// err is an AppError from the server
message = err.message;
}
if (clientId !== -1) {
const draft = PostStore.getDraft(channelId);
const index = draft.uploadsInProgress.indexOf(clientId);
if (index !== -1) {
draft.uploadsInProgress.splice(index, 1);
}
PostStore.storeDraft(channelId, draft);
if (channelId === this.state.channelId) {
this.setState({uploadsInProgress: draft.uploadsInProgress});
}
}
this.setState({serverError: message});
}
removePreview(id) {
const previews = Object.assign([], this.state.previews);
const uploadsInProgress = this.state.uploadsInProgress;
// id can either be the path of an uploaded file or the client id of an in progress upload
let index = previews.indexOf(id);
if (index === -1) {
index = uploadsInProgress.indexOf(id);
if (index !== -1) {
uploadsInProgress.splice(index, 1);
this.refs.fileUpload.getWrappedInstance().cancelUpload(id);
}
} else {
previews.splice(index, 1);
}
const draft = PostStore.getCurrentDraft();
draft.previews = previews;
draft.uploadsInProgress = uploadsInProgress;
PostStore.storeCurrentDraft(draft);
this.setState({previews, uploadsInProgress});
}
componentWillMount() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
// wait to load these since they may have changed since the component was constructed (particularly in the case of skipping the tutorial)
this.setState({
ctrlSend: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN,
showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER
});
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
PreferenceStore.addChangeListener(this.onPreferenceChange);
this.focusTextbox();
document.addEventListener('keydown', this.showShortcuts);
}
componentDidUpdate(prevProps, prevState) {
if (prevState.channelId !== this.state.channelId) {
this.focusTextbox();
}
}
componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange);
PreferenceStore.removeChangeListener(this.onPreferenceChange);
document.removeEventListener('keydown', this.showShortcuts);
}
showShortcuts(e) {
if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.FORWARD_SLASH) {
e.preventDefault();
ChannelActions.executeCommand(
this.state.channelId,
'/shortcuts ',
false,
null,
(err) => {
this.setState({
serverError: err.message,
submitting: false
});
}
);
}
}
onChange() {
const channelId = ChannelStore.getCurrentId();
if (this.state.channelId !== channelId) {
const draft = this.getCurrentDraft();
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, typing: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
}
}
onPreferenceChange() {
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
this.setState({
showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER,
ctrlSend: PreferenceStore.getBool(Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
fullWidthTextBox: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.CHANNEL_DISPLAY_MODE, Preferences.CHANNEL_DISPLAY_MODE_DEFAULT) === Preferences.CHANNEL_DISPLAY_MODE_FULL_SCREEN
});
}
getFileCount(channelId) {
if (channelId === this.state.channelId) {
return this.state.previews.length + this.state.uploadsInProgress.length;
}
const draft = PostStore.getDraft(channelId);
return draft.previews.length + draft.uploadsInProgress.length;
}
handleKeyDown(e) {
if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
this.postMsgKeyPress(e);
return;
}
if (!e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && e.keyCode === KeyCodes.UP && this.state.messageText === '') {
e.preventDefault();
const channelId = ChannelStore.getCurrentId();
const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
if (!lastPost) {
return;
}
const {formatMessage} = this.props.intl;
var type = (lastPost.root_id && lastPost.root_id.length > 0) ? formatMessage(holders.comment) : formatMessage(holders.post);
AppDispatcher.handleViewAction({
type: ActionTypes.RECEIVED_EDIT_POST,
refocusId: '#post_textbox',
title: type,
message: lastPost.message,
postId: lastPost.id,
channelId: lastPost.channel_id,
comments: PostStore.getCommentCount(lastPost)
});
}
if ((e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && (e.keyCode === Constants.KeyCodes.UP || e.keyCode === Constants.KeyCodes.DOWN)) {
const lastMessage = MessageHistoryStore.nextMessageInHistory(e.keyCode, this.state.messageText, 'post');
if (lastMessage !== null) {
e.preventDefault();
this.setState({
messageText: lastMessage
});
}
}
}
showPostDeletedModal() {
this.setState({
showPostDeletedModal: true
});
}
hidePostDeletedModal() {
this.setState({
showPostDeletedModal: false
});
}
createTutorialTip() {
const screens = [];
screens.push(
<div>
<FormattedHTMLMessage
id='create_post.tutorialTip'
defaultMessage='<h4>Sending Messages</h4><p>Type here to write a message and press <strong>Enter</strong> to post it.</p><p>Click the <strong>Attachment</strong> button to upload an image or a file.</p>'
/>
</div>
);
return (
<TutorialTip
placement='top'
screens={screens}
overlayClass='tip-overlay--chat'
/>
);
}
render() {
let serverError = null;
if (this.state.serverError) {
serverError = (
<div className='has-error'>
<label className='control-label'>{this.state.serverError}</label>
</div>
);
}
let postError = null;
if (this.state.postError) {
postError = <label className='control-label'>{this.state.postError}</label>;
}
let preview = null;
if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) {
preview = (
<FilePreview
files={this.state.previews}
onRemove={this.removePreview}
uploadsInProgress={this.state.uploadsInProgress}
/>
);
}
let postFooterClassName = 'post-create-footer';
if (postError) {
postFooterClassName += ' has-error';
}
let tutorialTip = null;
if (this.state.showTutorialTip) {
tutorialTip = this.createTutorialTip();
}
let centerClass = '';
if (!this.state.fullWidthTextBox) {
centerClass = 'center';
}
return (
<form
id='create_post'
ref='topDiv'
role='form'
className={centerClass}
onSubmit={this.handleSubmit}
>
<div className='post-create'>
<div className='post-create-body'>
<div className='post-body__cell'>
<Textbox
onUserInput={this.handleUserInput}
onKeyPress={this.postMsgKeyPress}
onKeyDown={this.handleKeyDown}
messageText={this.state.messageText}
typing={this.state.typing}
createMessage={this.props.intl.formatMessage(holders.write)}
channelId={this.state.channelId}
id='post_textbox'
ref='textbox'
/>
<FileUpload
ref='fileUpload'
getFileCount={this.getFileCount}
onClick={this.handleUploadClick}
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
postType='post'
channelId=''
/>
</div>
<a
className='send-button theme'
onClick={this.handleSubmit}
>
<i className='fa fa-paper-plane'/>
</a>
{tutorialTip}
</div>
<div className={postFooterClassName}>
<MsgTyping
channelId={this.state.channelId}
parentId=''
/>
{preview}
{postError}
{serverError}
</div>
</div>
<PostDeletedModal
show={this.state.showPostDeletedModal}
onHide={this.hidePostDeletedModal}
/>
</form>
);
}
}
CreatePost.propTypes = {
intl: intlShape.isRequired
};
export default injectIntl(CreatePost);