// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var PostStore = require('../stores/post_store.jsx'); var UserStore = require('../stores/user_store.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var MsgTyping = require('./msg_typing.jsx'); var Textbox = require('./textbox.jsx'); var FileUpload = require('./file_upload.jsx'); var FilePreview = require('./file_preview.jsx'); var utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; module.exports = React.createClass({ lastTime: 0, handleSubmit: function(e) { e.preventDefault(); if (this.state.uploadsInProgress > 0) return; if (this.state.submitting) return; var 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({ post_error: 'Post length must be less than '+Constants.CHARACTER_LIMIT+' characters.' }); return; } this.setState({ submitting: true }); var user_id = UserStore.getCurrentId(); if (post.message.indexOf("/") == 0) { client.executeCommand( this.state.channel_id, post.message, false, function(data) { PostStore.storeDraft(data.channel_id, user_id, null); this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null }); if (data.goto_location.length > 0) { window.location.href = data.goto_location; } }.bind(this), function(err){ var state = {} state.server_error = err.message; state.submitting = false; this.setState(state); }.bind(this) ); } else { post.channel_id = this.state.channel_id; post.filenames = this.state.previews; post.root_id = this.state.rootId; post.parent_id = this.state.parentId; // if this is a reply, trim off any carets from the beginning of a message if (post.root_id && post.message.startsWith("^")) { post.message = post.message.replace(/^\^+\s*/g, ""); } client.createPost(post, ChannelStore.getCurrent(), function(data) { PostStore.storeDraft(data.channel_id, data.user_id, null); this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null }); this.resizePostHolder(); AsyncClient.getPosts(true); var channel = ChannelStore.get(this.state.channel_id); var member = ChannelStore.getMember(this.state.channel_id); member.msg_count = channel.total_msg_count; member.last_viewed_at = (new Date).getTime(); ChannelStore.setChannelMember(member); }.bind(this), function(err) { var state = {} state.server_error = err.message; state.submitting = false; this.setState(state); }.bind(this) ); } $(".post-list-holder-by-time").perfectScrollbar('update'); if (this.state.rootId || this.state.parentId) { this.setState({rootId: "", parentId: "", caretCount: 0}); // clear the active thread since we've now sent our message AppDispatcher.handleViewAction({ type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, root_id: "", parent_id: "" }); } }, componentDidUpdate: function() { this.resizePostHolder(); }, postMsgKeyPress: function(e) { if (e.which == 13 && !e.shiftKey && !e.altKey) { e.preventDefault(); this.refs.textbox.getDOMNode().blur(); this.handleSubmit(e); } var t = new Date().getTime(); if ((t - this.lastTime) > 5000) { SocketStore.sendMessage({channel_id: this.state.channel_id, action: "typing", props: {"parent_id": ""}, state: {} }); this.lastTime = t; } }, handleUserInput: function(messageText) { this.resizePostHolder(); this.setState({messageText: messageText}); // look to see if the message begins with any carets to indicate that it's a reply var replyMatch = messageText.match(/^\^+/g); if (replyMatch) { // the number of carets indicates how many message threads back we're replying to var caretCount = replyMatch[0].length; // note that if someone else replies to this thread while a user is typing a reply, the message to which they're replying // won't change unless they change the number of carets. this is probably the desired behaviour since we don't want the // active message thread to change without the user noticing if (caretCount != this.state.caretCount) { this.setState({caretCount: caretCount}); var posts = PostStore.getCurrentPosts(); var rootId = ""; // find the nth most recent post that isn't a comment on another (ie it has no parent) where n is caretCount for (var i = 0; i < posts.order.length; i++) { var postId = posts.order[i]; if (posts.posts[postId].parent_id === "") { caretCount -= 1; if (caretCount < 1) { rootId = postId; break; } } } // only dispatch an event if something changed if (rootId != this.state.rootId) { // set the parent id to match the root id so that we're replying to the first post in the thread var parentId = rootId; // alert the post list so that it can display the active thread AppDispatcher.handleViewAction({ type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, root_id: rootId, parent_id: parentId }); } } } else { if (this.state.caretCount > 0) { this.setState({caretCount: 0}); // clear the active thread since there no longer is one AppDispatcher.handleViewAction({ type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, root_id: "", parent_id: "" }); } } var draft = PostStore.getCurrentDraft(); if (!draft) { draft = {} draft['previews'] = []; draft['uploadsInProgress'] = 0; } draft['message'] = messageText; PostStore.storeCurrentDraft(draft); }, resizePostHolder: function() { var height = $(window).height() - $(this.refs.topDiv.getDOMNode()).height() - $('#error_bar').outerHeight() - 50; $(".post-list-holder-by-time").css("height", height + "px"); $(window).trigger('resize'); }, handleFileUpload: function(newPreviews, channel_id) { var draft = PostStore.getDraft(channel_id, UserStore.getCurrentId()); if (!draft) { draft = {} draft['message'] = ''; draft['uploadsInProgress'] = 0; draft['previews'] = []; } if (channel_id === this.state.channel_id) { var num = this.state.uploadsInProgress; var oldPreviews = this.state.previews; var previews = oldPreviews.concat(newPreviews); draft['previews'] = previews; draft['uploadsInProgress'] = num-1; PostStore.storeCurrentDraft(draft); this.setState({previews: previews, uploadsInProgress:num-1}); } else { draft['previews'] = draft['previews'].concat(newPreviews); draft['uploadsInProgress'] = draft['uploadsInProgress'] > 0 ? draft['uploadsInProgress'] - 1 : 0; PostStore.storeDraft(channel_id, UserStore.getCurrentId(), draft); } }, handleUploadError: function(err) { this.setState({ server_error: err }); }, removePreview: function(filename) { var previews = this.state.previews; for (var i = 0; i < previews.length; i++) { if (previews[i] === filename) { previews.splice(i, 1); break; } } var draft = PostStore.getCurrentDraft(); if (!draft) { draft = {} draft['message'] = ''; draft['uploadsInProgress'] = 0; } draft['previews'] = previews; PostStore.storeCurrentDraft(draft); this.setState({previews: previews}); }, componentDidMount: function() { ChannelStore.addChangeListener(this._onChange); PostStore.addActiveThreadChangedListener(this._onActiveThreadChanged); this.resizePostHolder(); }, componentWillUnmount: function() { ChannelStore.removeChangeListener(this._onChange); PostStore.removeActiveThreadChangedListener(this._onActiveThreadChanged); }, _onChange: function() { var channel_id = ChannelStore.getCurrentId(); if (this.state.channel_id != channel_id) { var draft = PostStore.getCurrentDraft(); var previews = []; var messageText = ''; var uploadsInProgress = 0; if (draft) { previews = draft['previews']; messageText = draft['message']; uploadsInProgress = draft['uploadsInProgress']; } this.setState({ channel_id: channel_id, messageText: messageText, initialText: messageText, submitting: false, post_error: null, previews: previews, uploadsInProgress: uploadsInProgress }); } }, _onActiveThreadChanged: function(rootId, parentId) { // note that we register for our own events and set the state from there so we don't need to manually set // our state and dispatch an event each time the active thread changes this.setState({"rootId": rootId, "parentId": parentId}); }, getInitialState: function() { PostStore.clearDraftUploads(); var draft = PostStore.getCurrentDraft(); var previews = []; var messageText = ''; if (draft) { previews = draft['previews']; messageText = draft['message']; } return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText, caretCount: 0 }; }, setUploads: function(val) { var oldInProgress = this.state.uploadsInProgress var newInProgress = oldInProgress + val; if (newInProgress + this.state.previews.length > Constants.MAX_UPLOAD_FILES) { newInProgress = Constants.MAX_UPLOAD_FILES - this.state.previews.length; this.setState({limit_error: "Uploads limited to " + Constants.MAX_UPLOAD_FILES + " files maximum. Please use additional posts for more files."}); } else { this.setState({limit_error: null}); } var numToUpload = newInProgress - oldInProgress; if (numToUpload <= 0) return 0; var draft = PostStore.getCurrentDraft(); if (!draft) { draft = {} draft['message'] = ''; draft['previews'] = []; } draft['uploadsInProgress'] = newInProgress; PostStore.storeCurrentDraft(draft); this.setState({uploadsInProgress: newInProgress}); return numToUpload; }, render: function() { var server_error = this.state.server_error ?
: null; var post_error = this.state.post_error ? : null; var limit_error = this.state.limit_error ? : null; var preview = ; if (this.state.previews.length > 0 || this.state.uploadsInProgress > 0) { preview = (