From 32f7b50bb5c62d27def3f2e6d2839511c0b8f9a9 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 23 Jul 2015 09:39:29 -0400 Subject: adds predictive posting on the client --- model/post.go | 31 +++++++++--------- web/react/components/create_post.jsx | 29 ++++++++++++++--- web/react/components/post.jsx | 31 +++++++++++++++++- web/react/components/post_body.jsx | 32 ++++++++++++------ web/react/components/post_list.jsx | 26 ++++++++++++--- web/react/stores/post_store.jsx | 57 ++++++++++++++++++++++++++++++++- web/react/utils/async_client.jsx | 2 ++ web/sass-files/sass/partials/_post.scss | 18 +++++++++++ 8 files changed, 190 insertions(+), 36 deletions(-) diff --git a/model/post.go b/model/post.go index f6f33b1e8..0c035d4e7 100644 --- a/model/post.go +++ b/model/post.go @@ -14,21 +14,22 @@ const ( ) type Post struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - UserId string `json:"user_id"` - ChannelId string `json:"channel_id"` - RootId string `json:"root_id"` - ParentId string `json:"parent_id"` - OriginalId string `json:"original_id"` - Message string `json:"message"` - ImgCount int64 `json:"img_count"` - Type string `json:"type"` - Props StringMap `json:"props"` - Hashtags string `json:"hashtags"` - Filenames StringArray `json:"filenames"` + Id string `json:"id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` + DeleteAt int64 `json:"delete_at"` + UserId string `json:"user_id"` + ChannelId string `json:"channel_id"` + RootId string `json:"root_id"` + ParentId string `json:"parent_id"` + OriginalId string `json:"original_id"` + Message string `json:"message"` + ImgCount int64 `json:"img_count"` + Type string `json:"type"` + Props StringMap `json:"props"` + Hashtags string `json:"hashtags"` + Filenames StringArray `json:"filenames"` + PendingPostId string `json:"pending_post_id" db:"-"` } func (o *Post) ToJson() string { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 377e7bd34..76f2bf262 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -65,10 +65,17 @@ module.exports = React.createClass({ post.channel_id = this.state.channelId; post.filenames = this.state.previews; - client.createPost(post, ChannelStore.getCurrent(), + var time = utils.getTimestamp(); + post.pending_post_id = user_id + ":"+ time; + post.user_id = user_id; + post.create_at = time; + post.root_id = this.state.rootId; + post.parent_id = this.state.parentId; + + var channel = ChannelStore.getCurrent(); + + client.createPost(post, channel, function(data) { - PostStore.storeDraft(data.channel_id, null); - this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); this.resizePostHolder(); AsyncClient.getPosts(true); @@ -79,13 +86,25 @@ module.exports = React.createClass({ ChannelStore.setChannelMember(member); }.bind(this), function(err) { - var state = {}; - state.serverError = err.message; + var state = {} + + if (err.message === "Invalid RootId parameter") { + if ($('#post_deleted').length > 0) $('#post_deleted').modal('show'); + PostStore.removePendingPost(post.pending_post_id); + } else { + post.did_fail = true; + PostStore.updatePendingPost(post); + } state.submitting = false; this.setState(state); }.bind(this) ); + + post.is_loading = true; + PostStore.storePendingPost(post); + PostStore.storeDraft(channel.id, user_id, null); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); } $('.post-list-holder-by-time').perfectScrollbar('update'); diff --git a/web/react/components/post.jsx b/web/react/components/post.jsx index f099c67ab..c985eaeb2 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -7,6 +7,10 @@ var PostInfo = require('./post_info.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var UserStore = require('../stores/user_store.jsx'); +var PostStore = require('../stores/post_store.jsx'); +var ChannelStore = require('../stores/channel_store.jsx'); +var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var ActionTypes = Constants.ActionTypes; module.exports = React.createClass({ @@ -32,6 +36,31 @@ module.exports = React.createClass({ this.refs.info.forceUpdate(); this.refs.header.forceUpdate(); }, + retryPost: function(e) { + e.preventDefault(); + + var post = this.props.post; + client.createPost(post, post.channel_id, + function(data) { + AsyncClient.getPosts(true); + + var member = ChannelStore.getMember(post.channel_id); + member.msg_count = channel.total_msg_count; + member.last_viewed_at = (new Date).getTime(); + ChannelStore.setChannelMember(member); + }.bind(this), + function(err) { + post.did_fail = true; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }.bind(this) + ); + + post.did_fail = false; + post.is_loading = true; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }, getInitialState: function() { return { }; }, @@ -79,7 +108,7 @@ module.exports = React.createClass({ : null }
- +
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 860c96d84..65e045344 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -59,23 +59,35 @@ module.exports = React.createClass({ postClass += " post-comment"; } + var loading; + if (post.did_fail) { + postClass += " post-fail"; + loading = Retry; + } else if (post.is_loading) { + postClass += " post-waiting"; + loading = ; + } + var embed; if (filenames.length === 0 && this.state.links) { embed = utils.getEmbed(this.state.links[0]); } + var fileAttachmentHolder = ''; + if (filenames && filenames.length > 0) { + fileAttachmentHolder = (); + } + return (
- { comment } -

{inner}

- { filenames && filenames.length > 0 ? - - : "" } - { embed } + {comment} +

{loading}{inner}

+ {fileAttachmentHolder} + {embed}
); } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 8c76eb82c..834d7b0ff 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -20,8 +20,16 @@ function getStateFromStores() { if (channel == null) channel = {}; + var post_list = PostStore.getCurrentPosts(); + var pending_post_list = PostStore.getPendingPosts(channel.id); + + if (pending_post_list) { + post_list.order = pending_post_list.order.concat(post_list.order); + for (var pid in pending_post_list.posts) { post_list.posts[pid] = pending_post_list.posts[pid] }; + } + return { - post_list: PostStore.getCurrentPosts(), + post_list: post_list, channel: channel }; } @@ -186,8 +194,16 @@ module.exports = React.createClass({ if (msg.action == "posted") { var post = JSON.parse(msg.props.post); - var post_list = PostStore.getPosts(msg.channel_id); - if (!post_list) return; + if (post.pending_post_id !== "") { + PostStore.removePendingPost(post.channel_id, post.pending_post_id); + } + + post.pending_post_id = ""; + + postList.posts[post.id] = post; + if (postList.order.indexOf(post.id) === -1) { + postList.order.unshift(post.id); + } post_list.posts[post.id] = post; if (post_list.order.indexOf(post.id) === -1) { @@ -456,7 +472,9 @@ module.exports = React.createClass({ ); } - if (post.create_at > last_viewed && !rendered_last_viewed) { + var userId = UserStore.getCurrentId(); + + if (post.user_id !== userId && post.create_at > last_viewed && !rendered_last_viewed) { rendered_last_viewed = true; postCtls.push(
diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 9ebdf734c..40c69d782 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -19,7 +19,6 @@ var MENTION_DATA_CHANGE_EVENT = 'mention_data_change'; var ADD_MENTION_EVENT = 'add_mention'; var PostStore = assign({}, EventEmitter.prototype, { - emitChange: function emitChange() { this.emit(CHANGE_EVENT); }, @@ -104,6 +103,62 @@ var PostStore = assign({}, EventEmitter.prototype, { this.pStorePosts(channelId, posts); this.emitChange(); }, + storePendingPost: function(post) { + var post_list = this.getPendingPosts(post.channel_id); + if (!post_list) { + post_list = {posts: {}, order: []}; + } + + post_list.posts[post.pending_post_id] = post; + post_list.order.unshift(post.pending_post_id); + this._storePendingPosts(post.channel_id, post_list); + this.emitChange(); + }, + _storePendingPosts: function(channelId, posts) { + BrowserStore.setItem("pending_posts_" + channelId, posts); + }, + getPendingPosts: function(channelId) { + return BrowserStore.getItem("pending_posts_" + channelId); + }, + removePendingPost: function(channelId, pending_post_id) { + this._removePendingPost(channelId, pending_post_id); + this.emitChange(); + }, + _removePendingPost: function(channelId, pending_post_id) { + var post_list = this.getPendingPosts(channelId); + if (!post_list) return; + + if (pending_post_id in post_list.posts) delete post_list.posts[pending_post_id]; + var index = post_list.order.indexOf(pending_post_id); + if (index >= 0) post_list.order.splice(index, 1); + + this._storePendingPosts(channelId, post_list); + }, + clearPendingPosts: function(channelId) { + BrowserStore.removeItem("pending_posts_" + channelId) + }, + removeNonFailedPendingPosts: function(channelId) { + var post_list = this.getPendingPosts(channelId); + if (!post_list) return; + + var posts = post_list.posts; + + for (var id in posts) { + if (!posts[id].did_fail) this._removePendingPost(channelId, id); + } + }, + updatePendingPost: function(post) { + var post_list = this.getPendingPosts(post.channel_id); + if (!post_list) { + post_list = {posts: {}, order: []}; + } + + if (post_list.order.indexOf(post.pending_post_id) === -1) return; + + post_list.posts[post.pending_post_id] = post; + this._storePendingPosts(post.channel_id, post_list); + this.emitChange(); + }, pStorePosts: function pStorePosts(channelId, posts) { BrowserStore.setItem('posts_' + channelId, posts); }, diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 8b6d821d6..8fa022cbc 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -377,6 +377,8 @@ module.exports.getPosts = function(force, id, maxPosts) { post_list: data }); + PostStore.removeNonFailedPendingPosts(channelId); + module.exports.getProfiles(); }, function(err) { diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index c7add21a2..1beebeac6 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -461,3 +461,21 @@ body.ios { } } } + +.post-waiting { + color:lightgrey; +} + +.post-loading-gif { + height:10px; + width:10px; + margin-top:6px; +} + +.post-fail { + color:red; +} + +.post-retry { + color:darkgrey; +} -- cgit v1.2.3-1-g7c22 From 5596a6b37c440522176fdc05217161d7de7e169c Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 23 Jul 2015 12:45:08 -0400 Subject: added client predictive commenting --- web/react/components/create_comment.jsx | 24 ++++++++++++--- web/react/components/create_post.jsx | 30 ++++++++++++++----- web/react/components/post_info.jsx | 7 +++-- web/react/components/post_right.jsx | 53 +++++++++++++++++++++++++++++++-- 4 files changed, 97 insertions(+), 17 deletions(-) diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 885efab7a..fbb05c77f 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -2,15 +2,16 @@ // See License.txt for license information. var client = require('../utils/client.jsx'); -var AsyncClient =require('../utils/async_client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); +var UserStore = require('../stores/user_store.jsx'); var PostStore = require('../stores/post_store.jsx'); var Textbox = require('./textbox.jsx'); var MsgTyping = require('./msg_typing.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'); module.exports = React.createClass({ @@ -39,10 +40,16 @@ module.exports = React.createClass({ return; } + var user_id = UserStore.getCurrentId(); + post.channel_id = this.props.channelId; post.root_id = this.props.rootId; - post.parent_id = this.props.parentId; + post.parent_id = this.props.rootId; post.filenames = this.state.previews; + var time = utils.getTimestamp(); + post.pending_post_id = user_id + ":"+ time; + post.user_id = user_id; + post.create_at = time; this.setState({submitting: true, serverError: null}); @@ -69,10 +76,19 @@ module.exports = React.createClass({ $('#post_deleted').modal('show'); } } else { - this.setState(state); + post.did_fail = true; + PostStore.updatePendingPost(post); } + + state.submitting = false; + this.setState(state); }.bind(this) ); + + post.is_loading = true; + PostStore.storePendingPost(post); + PostStore.storeCommentDraft(this.props.rootId, null); + this.setState({ messageText: '', submitting: false, post_error: null, previews: [], server_error: null, limit_error: null }); }, commentMsgKeyPress: function(e) { if (e.which === 13 && !e.shiftKey && !e.altKey) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 76f2bf262..8110f5886 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -214,20 +214,34 @@ module.exports = React.createClass({ var channelId = ChannelStore.getCurrentId(); if (this.state.channelId !== channelId) { var draft = PostStore.getCurrentDraft(); - this.setState({ - channelId: channelId, messageText: draft['message'], initialText: draft['message'], submitting: false, - serverError: null, postError: null, previews: draft['previews'], uploadsInProgress: draft['uploadsInProgress'] - }); + + var previews = []; + var messageText = ''; + var uploadsInProgress = 0; + if (draft && draft['previews'] && draft['message']) { + previews = draft['previews']; + messageText = draft['message']; + uploadsInProgress = draft['uploadsInProgress']; + } + + this.setState({channelId: channelId, messageText: messageText, initialText: messageText, submitting: false, serverError: null, postError: null, previews: previews, uploadsInProgress: uploadsInProgress}); } }, getInitialState: function() { PostStore.clearDraftUploads(); + PostStore.clearPendingPosts(ChannelStore.getCurrentId()); var draft = PostStore.getCurrentDraft(); - return { - channelId: ChannelStore.getCurrentId(), messageText: draft['message'], uploadsInProgress: draft['uploadsInProgress'], - previews: draft['previews'], submitting: false, initialText: draft['message'] - }; + var previews = []; + var messageText = ''; + var uploadsInProgress = 0; + if (draft && draft["previews"] && draft["message"]) { + previews = draft['previews']; + messageText = draft['message']; + uploadsInProgress = draft['uploadsInProgress']; + } + + return { channelId: ChannelStore.getCurrentId(), messageText: messageText, uploadsInProgress: uploadsInProgress, previews: previews, submitting: false, initialText: messageText}; }, getFileCount: function(channelId) { if (channelId === this.state.channelId) { diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 8eaaf4e8c..93d028e18 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -22,16 +22,19 @@ module.exports = React.createClass({ var comments = ""; var lastCommentClass = this.props.isLastComment ? " comment-icon__container__show" : " comment-icon__container__hide"; - if (this.props.commentCount >= 1) { + if (this.props.commentCount >= 1 && !post.did_fail && !post.is_loading) { comments = {this.props.commentCount}; } + var show_dropdown = isOwner || (this.props.allowReply === "true" && type != "Comment"); + if (post.did_fail || post.is_loading) show_dropdown = false; + return (