From e32aee8977bf99b0f5ca446cb028b04c25e2b918 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 16 Jul 2015 15:16:11 -0400 Subject: Added events for when the active thread (ie the message thread that a user is responding to) changes --- web/react/stores/post_store.jsx | 16 ++++++++++++++++ web/react/utils/constants.jsx | 1 + 2 files changed, 17 insertions(+) (limited to 'web/react') diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index e773bb688..1f07dca62 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -18,6 +18,7 @@ var SEARCH_TERM_CHANGE_EVENT = 'search_term_change'; var SELECTED_POST_CHANGE_EVENT = 'selected_post_change'; var MENTION_DATA_CHANGE_EVENT = 'mention_data_change'; var ADD_MENTION_EVENT = 'add_mention'; +var ACTIVE_THREAD_CHANGED_EVENT = 'active_thread_changed'; var PostStore = assign({}, EventEmitter.prototype, { @@ -93,6 +94,18 @@ var PostStore = assign({}, EventEmitter.prototype, { this.removeListener(ADD_MENTION_EVENT, callback); }, + emitActiveThreadChanged: function(rootId, parentId) { + this.emit(ACTIVE_THREAD_CHANGED_EVENT, rootId, parentId); + }, + + addActiveThreadChangedListener: function(callback) { + this.on(ACTIVE_THREAD_CHANGED_EVENT, callback); + }, + + removeActiveThreadChangedListener: function(callback) { + this.removeListener(ACTIVE_THREAD_CHANGED_EVENT, callback); + }, + getCurrentPosts: function() { var currentId = ChannelStore.getCurrentId(); @@ -186,6 +199,9 @@ PostStore.dispatchToken = AppDispatcher.register(function(payload) { case ActionTypes.RECIEVED_ADD_MENTION: PostStore.emitAddMention(action.id, action.username); break; + case ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED: + PostStore.emitActiveThreadChanged(action.root_id, action.parent_id); + break; default: } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 3aadfb4b0..943787630 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -18,6 +18,7 @@ module.exports = { RECIEVED_POST_SELECTED: null, RECIEVED_MENTION_DATA: null, RECIEVED_ADD_MENTION: null, + RECEIVED_ACTIVE_THREAD_CHANGED: null, RECIEVED_PROFILES: null, RECIEVED_ME: null, -- cgit v1.2.3-1-g7c22 From 25b2e75dc664bfb80470713331c33c88e726dcf5 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 16 Jul 2015 15:25:28 -0400 Subject: Added function to reply to earlier messages by prefixing a message with carets --- web/react/components/create_post.jsx | 68 ++++++++++++++++++++++++++++++++++++ web/react/components/post.jsx | 2 +- web/react/components/post_list.jsx | 17 +++++++-- 3 files changed, 84 insertions(+), 3 deletions(-) (limited to 'web/react') diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index d38a6798f..681ca252f 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -68,6 +68,9 @@ module.exports = React.createClass({ post.channel_id = this.state.channel_id; post.filenames = this.state.previews; + post.root_id = this.state.rootId; + post.parent_id = this.state.parentId; + client.createPost(post, ChannelStore.getCurrent(), function(data) { PostStore.storeDraft(data.channel_id, data.user_id, null); @@ -92,6 +95,17 @@ module.exports = React.createClass({ } $(".post-list-holder-by-time").perfectScrollbar('update'); + + if (this.state.rootId || this.state.parentId) { + this.setState({rootId: "", parentId: ""}); + + // 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(); @@ -112,6 +126,60 @@ module.exports = React.createClass({ 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; + + 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 === "") { + if (caretCount == 1) { + rootId = postId; + break; + } else { + caretCount -= 1; + } + } + } + + if (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 + }); + + // save these so that we don't need to recalculate them when we send this post + this.setState({rootId: rootId, parentId: parentId}); + } else { + // we couldn't find a post to respond to + this.setState({rootId: "", parentId: ""}); + } + } else { + if (this.state.rootId || this.state.parentId) { + this.setState({rootId: "", parentId: ""}); + + AppDispatcher.handleViewAction({ + type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, + root_id: "", + parent_id: "" + }); + } + } + var draft = PostStore.getCurrentDraft(); if (!draft) { draft = {} diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index e72a2d001..e3586ecde 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -83,7 +83,7 @@ module.exports = React.createClass({ : null } -
+
diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index c058455ba..0fe668aef 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -22,7 +22,8 @@ function getStateFromStores() { return { post_list: PostStore.getCurrentPosts(), - channel: channel + channel: channel, + activeThreadRootId: "" }; } @@ -51,6 +52,7 @@ module.exports = React.createClass({ ChannelStore.addChangeListener(this._onChange); UserStore.addStatusesChangeListener(this._onTimeChange); SocketStore.addChangeListener(this._onSocketChange); + PostStore.addActiveThreadChangedListener(this._onActiveThreadChanged); $(".post-list-holder-by-time").perfectScrollbar(); @@ -131,6 +133,7 @@ module.exports = React.createClass({ ChannelStore.removeChangeListener(this._onChange); UserStore.removeStatusesChangeListener(this._onTimeChange); SocketStore.removeChangeListener(this._onSocketChange); + PostStore.removeActiveThreadChangedListener(this._onActiveThreadChanged); $('body').off('click.userpopover'); }, resize: function() { @@ -228,6 +231,9 @@ module.exports = React.createClass({ this.refs[id].forceUpdateInfo(); } }, + _onActiveThreadChanged: function(rootId, parentId) { + this.setState({"activeThreadRootId": rootId}); + }, getMorePosts: function(e) { e.preventDefault(); @@ -419,7 +425,14 @@ module.exports = React.createClass({ // it is the last comment if it is last post in the channel or the next post has a different root post var isLastComment = utils.isComment(post) && (i === 0 || posts[order[i-1]].root_id != post.root_id); - var postCtl = ; + // check if this is part of the thread that we're currently replying to + var isActiveThread = this.state.activeThreadRootId && (post.id === this.state.activeThreadRootId || post.root_id === this.state.activeThreadRootId); + + var postCtl = ( + + ); currentPostDay = utils.getDateForUnixTicks(post.create_at); if (currentPostDay.toDateString() != previousPostDay.toDateString()) { -- cgit v1.2.3-1-g7c22 From 644c78755cb7cf8ef818106b9629df1018d0d736 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 16 Jul 2015 16:03:06 -0400 Subject: Change the create_post component to track the active thread using the event dispatcher so that it stays in sync with the post_list --- web/react/components/create_post.jsx | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) (limited to 'web/react') diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 681ca252f..7bc5e2481 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -152,26 +152,29 @@ module.exports = React.createClass({ } if (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 + // 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 { + // we couldn't find a post to respond to so clear the active thread AppDispatcher.handleViewAction({ type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, - root_id: rootId, - parent_id: parentId + root_id: "", + parent_id: "" }); - - // save these so that we don't need to recalculate them when we send this post - this.setState({rootId: rootId, parentId: parentId}); - } else { - // we couldn't find a post to respond to - this.setState({rootId: "", parentId: ""}); } } else { if (this.state.rootId || this.state.parentId) { - this.setState({rootId: "", parentId: ""}); - + // clear the active thread since there no longer is one AppDispatcher.handleViewAction({ type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, root_id: "", @@ -242,10 +245,12 @@ module.exports = React.createClass({ }, 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(); @@ -262,6 +267,11 @@ module.exports = React.createClass({ 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(); -- cgit v1.2.3-1-g7c22 From 5b917dd4a6fc4259cd17ad0b2e106f3b71254ef2 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Thu, 16 Jul 2015 16:58:05 -0400 Subject: Trim the carets from the beginning of reply messages when they're posted --- web/react/components/create_post.jsx | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'web/react') diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 7bc5e2481..a3e354599 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -71,6 +71,11 @@ module.exports = React.createClass({ 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); -- cgit v1.2.3-1-g7c22 From 7b28880294865c7441ce8b4b3efc14b1417cb5e5 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 21 Jul 2015 15:54:47 -0400 Subject: Track caretCount as part of createPost's state so that we don't unnecessarily search for a thread to reply to when the user is typing --- web/react/components/create_post.jsx | 44 ++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) (limited to 'web/react') diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index a3e354599..7b7e38ac2 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -102,7 +102,7 @@ module.exports = React.createClass({ $(".post-list-holder-by-time").perfectScrollbar('update'); if (this.state.rootId || this.state.parentId) { - this.setState({rootId: "", parentId: ""}); + this.setState({rootId: "", parentId: "", caretCount: 0}); // clear the active thread since we've now sent our message AppDispatcher.handleViewAction({ @@ -138,25 +138,30 @@ module.exports = React.createClass({ // the number of carets indicates how many message threads back we're replying to var caretCount = replyMatch[0].length; - var posts = PostStore.getCurrentPosts(); + // 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 rootId = ""; + var posts = PostStore.getCurrentPosts(); - // 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]; + var rootId = ""; - if (posts.posts[postId].parent_id === "") { - if (caretCount == 1) { - rootId = postId; - break; - } else { + // 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; + } } } - } - if (rootId) { // 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 @@ -169,16 +174,11 @@ module.exports = React.createClass({ parent_id: parentId }); } - } else { - // we couldn't find a post to respond to so clear the active thread - AppDispatcher.handleViewAction({ - type: ActionTypes.RECEIVED_ACTIVE_THREAD_CHANGED, - root_id: "", - parent_id: "" - }); } } else { - if (this.state.rootId || this.state.parentId) { + 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, @@ -287,7 +287,7 @@ module.exports = React.createClass({ previews = draft['previews']; messageText = draft['message']; } - return { channel_id: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: 0, previews: previews, submitting: false, initialText: messageText }; + 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 -- cgit v1.2.3-1-g7c22 From d4fd000ccfc4f9c128cf178755dd3b1df0cf3894 Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 21 Jul 2015 15:55:34 -0400 Subject: Added handling for when the user is ^ responding to a post that gets deleted --- web/react/components/create_post.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'web/react') diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 7b7e38ac2..efb5efd80 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -92,7 +92,13 @@ module.exports = React.createClass({ }.bind(this), function(err) { var state = {} - state.server_error = err.message; + + if (err.message === "Invalid RootId parameter") { + if ($('#post_deleted').length > 0) $('#post_deleted').modal('show'); + } else { + state.server_error = err.message; + } + state.submitting = false; this.setState(state); }.bind(this) -- cgit v1.2.3-1-g7c22 From 0967a131a152e056c1cb971f895b2d1f8df4d0ed Mon Sep 17 00:00:00 2001 From: hmhealey Date: Tue, 21 Jul 2015 16:01:30 -0400 Subject: Prevent users from sending empty ^ reply messages --- web/react/components/create_post.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'web/react') diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index efb5efd80..a2448b569 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -31,6 +31,11 @@ module.exports = React.createClass({ post.message = this.state.messageText; + // if this is a reply, trim off any carets from the beginning of a message + if (this.state.rootId && post.message.startsWith("^")) { + post.message = post.message.replace(/^\^+\s*/g, ""); + } + if (post.message.trim().length === 0 && this.state.previews.length === 0) { return; } @@ -71,11 +76,6 @@ module.exports = React.createClass({ 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); -- cgit v1.2.3-1-g7c22