diff options
-rw-r--r-- | web/react/components/channel_loader.jsx | 17 | ||||
-rw-r--r-- | web/react/components/create_comment.jsx | 31 | ||||
-rw-r--r-- | web/react/components/create_post.jsx | 53 | ||||
-rw-r--r-- | web/react/components/post.jsx | 12 | ||||
-rw-r--r-- | web/react/components/post_body.jsx | 5 | ||||
-rw-r--r-- | web/react/components/post_info.jsx | 10 | ||||
-rw-r--r-- | web/react/components/post_list.jsx | 27 | ||||
-rw-r--r-- | web/react/components/post_right.jsx | 209 | ||||
-rw-r--r-- | web/react/stores/browser_store.jsx | 48 | ||||
-rw-r--r-- | web/react/stores/post_store.jsx | 72 | ||||
-rw-r--r-- | web/react/stores/socket_store.jsx | 128 | ||||
-rw-r--r-- | web/react/utils/async_client.jsx | 29 | ||||
-rw-r--r-- | web/react/utils/constants.jsx | 3 |
13 files changed, 374 insertions, 270 deletions
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 6b80f6012..525b67b5c 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -9,6 +9,7 @@ var BrowserStore = require('../stores/browser_store.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); +var PostStore = require('../stores/post_store.jsx'); var Constants = require('../utils/constants.jsx'); module.exports = React.createClass({ @@ -24,20 +25,24 @@ module.exports = React.createClass({ AsyncClient.getMyTeam(); /* End of async loads */ + /* Perform pending post clean-up */ + PostStore.clearPendingPosts(); + /* End pending post clean-up */ /* Start interval functions */ - setInterval(function(){AsyncClient.getStatuses();}, 30000); + setInterval( + function pollStatuses() { + AsyncClient.getStatuses(); + }, 30000); /* End interval functions */ - /* Start device tracking setup */ - var iOS = /(iPad|iPhone|iPod)/g.test( navigator.userAgent ); + var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent); if (iOS) { - $("body").addClass("ios"); + $('body').addClass('ios'); } /* End device tracking setup */ - /* Start window active tracking setup */ window.isActive = true; @@ -57,7 +62,7 @@ module.exports = React.createClass({ }, _onSocketChange: function(msg) { if (msg && msg.user_id) { - UserStore.setStatus(msg.user_id, "online"); + UserStore.setStatus(msg.user_id, 'online'); } }, render: function() { diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index fbb05c77f..1de768872 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -1,6 +1,7 @@ // 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 SocketStore = require('../stores/socket_store.jsx'); @@ -13,6 +14,7 @@ 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, @@ -27,6 +29,8 @@ module.exports = React.createClass({ return; } + this.setState({submitting: true, serverError: null}); + var post = {}; post.filenames = []; post.message = this.state.messageText; @@ -47,17 +51,16 @@ module.exports = React.createClass({ post.parent_id = this.props.rootId; post.filenames = this.state.previews; var time = utils.getTimestamp(); - post.pending_post_id = user_id + ":"+ time; + post.pending_post_id = user_id + ':'+ time; post.user_id = user_id; post.create_at = time; - this.setState({submitting: true, serverError: null}); + PostStore.storePendingPost(post); + PostStore.storeCommentDraft(this.props.rootId, null); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); client.createPost(post, ChannelStore.getCurrent(), function(data) { - PostStore.storeCommentDraft(this.props.rootId, null); - this.setState({messageText: '', submitting: false, postError: null, serverError: null}); - this.clearPreviews(); AsyncClient.getPosts(true, this.props.channelId); var channel = ChannelStore.get(this.props.channelId); @@ -65,18 +68,22 @@ module.exports = React.createClass({ member.msg_count = channel.total_msg_count; member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post: data + }); }.bind(this), function(err) { var state = {}; - state.serverError = err.message; - state.submitting = false; 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; + post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); } @@ -84,11 +91,6 @@ module.exports = React.createClass({ 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) { @@ -153,9 +155,6 @@ module.exports = React.createClass({ this.setState({serverError: err}); } }, - clearPreviews: function() { - this.setState({previews: []}); - }, removePreview: function(id) { var previews = this.state.previews; var uploadsInProgress = this.state.uploadsInProgress; diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 8110f5886..8e9e95751 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -66,33 +66,44 @@ module.exports = React.createClass({ post.filenames = this.state.previews; var time = utils.getTimestamp(); - post.pending_post_id = user_id + ":"+ time; - post.user_id = user_id; + var userId = UserStore.getCurrentId(); + post.pending_post_id = userId + ':' + time; + post.user_id = userId; post.create_at = time; post.root_id = this.state.rootId; post.parent_id = this.state.parentId; - var channel = ChannelStore.getCurrent(); + var channel = ChannelStore.get(this.state.channelId); + + PostStore.storePendingPost(post); + PostStore.storeDraft(channel.id, userId, null); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); client.createPost(post, channel, function(data) { this.resizePostHolder(); AsyncClient.getPosts(true); - var channel = ChannelStore.get(this.state.channelId); - var member = ChannelStore.getMember(this.state.channelId); + var member = ChannelStore.getMember(channel.id); member.msg_count = channel.total_msg_count; member.last_viewed_at = Date.now(); ChannelStore.setChannelMember(member); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post: data + }); }.bind(this), function(err) { - var state = {} + var state = {}; - if (err.message === "Invalid RootId parameter") { - if ($('#post_deleted').length > 0) $('#post_deleted').modal('show'); + 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; + post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); } @@ -100,11 +111,6 @@ module.exports = React.createClass({ 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'); @@ -218,10 +224,10 @@ module.exports = React.createClass({ var previews = []; var messageText = ''; var uploadsInProgress = 0; - if (draft && draft['previews'] && draft['message']) { - previews = draft['previews']; - messageText = draft['message']; - uploadsInProgress = draft['uploadsInProgress']; + 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}); @@ -229,19 +235,18 @@ module.exports = React.createClass({ }, getInitialState: function() { PostStore.clearDraftUploads(); - PostStore.clearPendingPosts(ChannelStore.getCurrentId()); var draft = PostStore.getCurrentDraft(); var previews = []; var messageText = ''; var uploadsInProgress = 0; - if (draft && draft["previews"] && draft["message"]) { - previews = draft['previews']; - messageText = draft['message']; - uploadsInProgress = draft['uploadsInProgress']; + 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}; + 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.jsx b/web/react/components/post.jsx index c985eaeb2..cc505f91f 100644 --- a/web/react/components/post.jsx +++ b/web/react/components/post.jsx @@ -44,20 +44,20 @@ module.exports = React.createClass({ function(data) { AsyncClient.getPosts(true); + var channel = ChannelStore.get(post.channel_id); 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; + post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); this.forceUpdate(); }.bind(this) ); - post.did_fail = false; - post.is_loading = true; + post.state = Constants.POST_LOADING; PostStore.updatePendingPost(post); this.forceUpdate(); }, @@ -69,9 +69,9 @@ module.exports = React.createClass({ var parentPost = this.props.parentPost; var posts = this.props.posts; - var type = "Post" - if (post.root_id.length > 0) { - type = "Comment" + var type = "Post"; + if (post.root_id && post.root_id.length > 0) { + type = "Comment"; } var commentCount = 0; diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 65e045344..77a8fd7b5 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -4,6 +4,7 @@ var FileAttachmentList = require('./file_attachment_list.jsx'); var UserStore = require('../stores/user_store.jsx'); var utils = require('../utils/utils.jsx'); +var Constants = require('../utils/constants.jsx'); module.exports = React.createClass({ componentWillReceiveProps: function(nextProps) { @@ -60,10 +61,10 @@ module.exports = React.createClass({ } var loading; - if (post.did_fail) { + if (post.state === Constants.POST_FAILED) { postClass += " post-fail"; loading = <a className="post-retry pull-right" href="#" onClick={this.props.retryPost}>Retry</a>; - } else if (post.is_loading) { + } else if (post.state === Constants.POST_LOADING) { postClass += " post-waiting"; loading = <img className="post-loading-gif pull-right" src="/static/images/load.gif"/>; } diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index 93d028e18..37e525717 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -15,19 +15,19 @@ module.exports = React.createClass({ var isOwner = UserStore.getCurrentId() == post.user_id; var isAdmin = UserStore.getCurrentUser().roles.indexOf("admin") > -1 - var type = "Post" - if (post.root_id.length > 0) { - type = "Comment" + var type = "Post"; + if (post.root_id && post.root_id.length > 0) { + type = "Comment"; } var comments = ""; var lastCommentClass = this.props.isLastComment ? " comment-icon__container__show" : " comment-icon__container__hide"; - if (this.props.commentCount >= 1 && !post.did_fail && !post.is_loading) { + if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { comments = <a href="#" className={"comment-icon__container theme" + lastCommentClass} onClick={this.props.handleCommentClick}><span className="comment-icon" dangerouslySetInnerHTML={{__html: Constants.COMMENT_ICON }} />{this.props.commentCount}</a>; } var show_dropdown = isOwner || (this.props.allowReply === "true" && type != "Comment"); - if (post.did_fail || post.is_loading) show_dropdown = false; + if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING) show_dropdown = false; return ( <ul className="post-header post-info"> diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 834d7b0ff..5f977972b 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -190,26 +190,13 @@ module.exports = React.createClass({ this.setState(newState); } }, - _onSocketChange: function(msg) { - if (msg.action == "posted") { - var post = JSON.parse(msg.props.post); - - 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) { - post_list.order.unshift(post.id); - } - + onSocketChange: function(msg) { + var postList; + var post; + if (msg.action === 'posted') { + post = JSON.parse(msg.props.post); + PostStore.storePost(post); + } else if (msg.action === 'post_edited') { if (this.state.channel.id === msg.channel_id) { this.setState({ post_list: post_list }); }; diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 69e514e11..8672df439 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -3,16 +3,17 @@ var PostStore = require('../stores/post_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); -var UserProfile = require( './user_profile.jsx' ); +var UserProfile = require('./user_profile.jsx'); var UserStore = require('../stores/user_store.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var utils = require('../utils/utils.jsx'); -var SearchBox =require('./search_bar.jsx'); -var CreateComment = require( './create_comment.jsx' ); +var SearchBox = require('./search_bar.jsx'); +var CreateComment = require('./create_comment.jsx'); var Constants = require('../utils/constants.jsx'); var FileAttachmentList = require('./file_attachment_list.jsx'); var FileUploadOverlay = require('./file_upload_overlay.jsx'); var client = require('../utils/client.jsx'); +var AsyncClient = require('../utils/async_client.jsx'); var ActionTypes = Constants.ActionTypes; RhsHeaderPost = React.createClass({ @@ -45,12 +46,15 @@ RhsHeaderPost = React.createClass({ }); }, render: function() { - var back = this.props.fromSearch ? <a href="#" onClick={this.handleBack} className="sidebar--right__back"><i className="fa fa-chevron-left"></i></a> : ""; + var back; + if (this.props.fromSearch) { + back = <a href='#' onClick={this.handleBack} className='sidebar--right__back'><i className='fa fa-chevron-left'></i></a>; + } return ( - <div className="sidebar--right__header"> - <span className="sidebar--right__title">{back}Message Details</span> - <button type="button" className="sidebar--right__close" aria-label="Close" onClick={this.handleClose}></button> + <div className='sidebar--right__header'> + <span className='sidebar--right__title'>{back}Message Details</span> + <button type='button' className='sidebar--right__close' aria-label='Close' onClick={this.handleClose}></button> </div> ); } @@ -60,57 +64,72 @@ RootPost = React.createClass({ render: function() { var post = this.props.post; var message = utils.textToJsx(post.message); - var isOwner = UserStore.getCurrentId() == post.user_id; + var isOwner = UserStore.getCurrentId() === post.user_id; var timestamp = UserStore.getProfile(post.user_id).update_at; var channel = ChannelStore.get(post.channel_id); - var type = "Post"; + var type = 'Post'; if (post.root_id.length > 0) { - type = "Comment"; + type = 'Comment'; } - var currentUserCss = ""; + var currentUserCss = ''; if (UserStore.getCurrentId() === post.user_id) { - currentUserCss = "current--user"; + currentUserCss = 'current--user'; } + var channelName; if (channel) { - channelName = (channel.type === 'D') ? "Private Message" : channel.display_name; + if (channel.type === 'D') { + channelName = 'Private Message'; + } else { + channelName = channel.display_name; + } + } + + var ownerOptions; + if (isOwner) { + ownerOptions = ( + <div> + <a href='#' className='dropdown-toggle theme' type='button' data-toggle='dropdown' aria-expanded='false' /> + <ul className='dropdown-menu' role='menu'> + <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> + <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#delete_post' data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={this.props.commentCount}>Delete</a></li> + </ul> + </div> + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + <FileAttachmentList + filenames={post.filenames} + modalId={'rhs_view_image_modal_' + post.id} + channelId={post.channel_id} + userId={post.user_id} /> + ); } return ( - <div className={"post post--root " + currentUserCss}> - <div className="post-right-channel__name">{ channelName }</div> - <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" /> + <div className={'post post--root ' + currentUserCss}> + <div className='post-right-channel__name'>{ channelName }</div> + <div className='post-profile-img__container'> + <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> </div> - <div className="post__content"> - <ul className="post-header"> - <li className="post-header-col"><strong><UserProfile userId={post.user_id} /></strong></li> - <li className="post-header-col"><time className="post-right-root-time">{ utils.displayDate(post.create_at)+' '+utils.displayTime(post.create_at) }</time></li> - <li className="post-header-col post-header__reply"> - <div className="dropdown"> - { isOwner ? - <div> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> - <ul className="dropdown-menu" role="menu"> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={this.props.commentCount}>Delete</a></li> - </ul> - </div> - : "" } + <div className='post__content'> + <ul className='post-header'> + <li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li> + <li className='post-header-col'><time className='post-right-root-time'>{utils.displayDate(post.create_at) + ' ' + utils.displayTime(post.create_at)}</time></li> + <li className='post-header-col post-header__reply'> + <div className='dropdown'> + {ownerOptions} </div> </li> </ul> <div className="post-body"> <p>{message}</p> - { post.filenames && post.filenames.length > 0 ? - <FileAttachmentList - filenames={post.filenames} - modalId={"rhs_view_image_modal_" + post.id} - channelId={post.channel_id} - userId={post.user_id} /> - : "" } + {fileAttachment} </div> </div> <hr /> @@ -134,78 +153,84 @@ CommentPost = React.createClass({ ChannelStore.setChannelMember(member); }.bind(this), function(err) { - post.did_fail = true; + post.state = Constants.POST_FAILED; PostStore.updatePendingPost(post); this.forceUpdate(); }.bind(this) ); - post.did_fail = false; - post.is_loading = true; + post.state = Constants.POST_LOADING; PostStore.updatePendingPost(post); this.forceUpdate(); }, render: function() { var post = this.props.post; - var commentClass = "post"; - var post = this.props.post; - - var currentUserCss = ""; + var currentUserCss = ''; if (UserStore.getCurrentId() === post.user_id) { - currentUserCss = "current--user"; + currentUserCss = 'current--user'; } - var isOwner = UserStore.getCurrentId() == post.user_id; + var isOwner = UserStore.getCurrentId() === post.user_id; - var type = "Post" + var type = 'Post'; if (post.root_id.length > 0) { - type = "Comment" + type = 'Comment'; } var message = utils.textToJsx(post.message); var timestamp = UserStore.getCurrentUser().update_at; var loading; - var postClass = ""; - if (post.did_fail) { - postClass += " post-fail"; - loading = <a className="post-retry pull-right" href="#" onClick={this.retryComment}>Retry</a>; - } else if (post.is_loading) { - postClass += " post-waiting"; - loading = <img className="post-loading-gif pull-right" src="/static/images/load.gif"/>; + var postClass = ''; + if (post.state === Constants.POST_FAILED) { + postClass += ' post-fail'; + loading = <a className='post-retry pull-right' href='#' onClick={this.retryComment}>Retry</a>; + } else if (post.state === Constants.POST_LOADING) { + postClass += ' post-waiting'; + loading = <img className='post-loading-gif pull-right' src='/static/images/load.gif'/>; + } + + var ownerOptions; + if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { + ownerOptions = ( + <div className='dropdown' onClick={function(e){$('.post-list-holder-by-time').scrollTop($('.post-list-holder-by-time').scrollTop() + 50);}}> + <a href='#' className='dropdown-toggle theme' type='button' data-toggle='dropdown' aria-expanded='false' /> + <ul className='dropdown-menu' role='menu'> + <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#edit_post' data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> + <li role='presentation'><a href='#' role='menuitem' data-toggle='modal' data-target='#delete_post' data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={0}>Delete</a></li> + </ul> + </div> + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + <FileAttachmentList + filenames={post.filenames} + modalId={'rhs_comment_view_image_modal_' + post.id} + channelId={post.channel_id} + userId={post.user_id} /> + ); } return ( - <div className={commentClass + " " + currentUserCss}> - <div className="post-profile-img__container"> - <img className="post-profile-img" src={"/api/v1/users/" + post.user_id + "/image?time=" + timestamp} height="36" width="36" /> + <div className={'post ' + currentUserCss}> + <div className='post-profile-img__container'> + <img className='post-profile-img' src={'/api/v1/users/' + post.user_id + '/image?time=' + timestamp} height='36' width='36' /> </div> - <div className="post__content"> - <ul className="post-header"> - <li className="post-header-col"><strong><UserProfile userId={post.user_id} /></strong></li> - <li className="post-header-col"><time className="post-right-comment-time">{ utils.displayDateTime(post.create_at) }</time></li> - <li className="post-header-col post-header__reply"> - { isOwner && !post.did_fail && !post.is_loading ? - <div className="dropdown" onClick={function(e){$('.post-list-holder-by-time').scrollTop($(".post-list-holder-by-time").scrollTop() + 50);}}> - <a href="#" className="dropdown-toggle theme" type="button" data-toggle="dropdown" aria-expanded="false" /> - <ul className="dropdown-menu" role="menu"> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#edit_post" data-title={type} data-message={post.message} data-postid={post.id} data-channelid={post.channel_id}>Edit</a></li> - <li role="presentation"><a href="#" role="menuitem" data-toggle="modal" data-target="#delete_post" data-title={type} data-postid={post.id} data-channelid={post.channel_id} data-comments={0}>Delete</a></li> - </ul> - </div> - : "" } + <div className='post__content'> + <ul className='post-header'> + <li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li> + <li className='post-header-col'><time className='post-right-comment-time'>{utils.displayDateTime(post.create_at)}</time></li> + <li className='post-header-col post-header__reply'> + {ownerOptions} </li> </ul> <div className="post-body"> <p className={postClass}>{loading}{message}</p> - { post.filenames && post.filenames.length > 0 ? - <FileAttachmentList - filenames={post.filenames} - modalId={"rhs_comment_view_image_modal_" + post.id} - channelId={post.channel_id} - userId={post.user_id} /> - : "" } + {fileAttachment} </div> </div> </div> @@ -239,8 +264,8 @@ module.exports = React.createClass({ }); }, componentDidUpdate: function() { - $(".post-right__scroll").scrollTop($(".post-right__scroll")[0].scrollHeight); - $(".post-right__scroll").perfectScrollbar('update'); + $('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight); + $('.post-right__scroll').perfectScrollbar('update'); this.resize(); }, componentWillUnmount: function() { @@ -296,10 +321,10 @@ module.exports = React.createClass({ }, resize: function() { var height = $(window).height() - $('#error_bar').outerHeight() - 100; - $(".post-right__scroll").css("height", height + "px"); - $(".post-right__scroll").scrollTop(100000); - $(".post-right__scroll").perfectScrollbar(); - $(".post-right__scroll").perfectScrollbar('update'); + $('.post-right__scroll').css('height', height + 'px'); + $('.post-right__scroll').scrollTop(100000); + $('.post-right__scroll').perfectScrollbar(); + $('.post-right__scroll').perfectScrollbar('update'); }, render: function() { @@ -314,7 +339,7 @@ module.exports = React.createClass({ var selected_post = post_list.posts[post_list.order[0]]; var root_post = null; - if (selected_post.root_id == "") { + if (selected_post.root_id == '') { root_post = selected_post; } else { @@ -343,11 +368,11 @@ module.exports = React.createClass({ var searchForm = currentId == null ? null : <SearchBox />; return ( - <div className="post-right__container"> + <div className='post-right__container'> <FileUploadOverlay overlayType='right' /> - <div className="search-bar__container sidebar--right__search-header">{searchForm}</div> - <div className="sidebar-right__body"> + <div className='search-bar__container sidebar--right__search-header'>{searchForm}</div> + <div className='sidebar-right__body'> <RhsHeaderPost fromSearch={this.props.fromSearch} isMentionSearch={this.props.isMentionSearch} /> <div className="post-right__scroll"> <RootPost post={root_post} commentCount={posts_array.length}/> @@ -356,7 +381,7 @@ module.exports = React.createClass({ return <CommentPost ref={cpost.id} key={cpost.id} post={cpost} selected={ (cpost.id == selected_post.id) } /> })} </div> - <div className="post-create__container"> + <div className='post-create__container'> <CreateComment channelId={root_post.channel_id} rootId={root_post.id} /> </div> </div> diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 4eed754cc..436b8d76d 100644 --- a/web/react/stores/browser_store.jsx +++ b/web/react/stores/browser_store.jsx @@ -3,7 +3,9 @@ var UserStore; function getPrefix() { - if (!UserStore) UserStore = require('./user_store.jsx'); + if (!UserStore) { + UserStore = require('./user_store.jsx'); + } return UserStore.getCurrentId() + '_'; } @@ -11,15 +13,15 @@ function getPrefix() { var BROWSER_STORE_VERSION = '.4'; module.exports = { - _initialized: false, + initialized: false, - _initialize: function() { - var currentVersion = localStorage.getItem("local_storage_version"); + initialize: function() { + var currentVersion = localStorage.getItem('local_storage_version'); if (currentVersion !== BROWSER_STORE_VERSION) { this.clear(); - localStorage.setItem("local_storage_version", BROWSER_STORE_VERSION); + localStorage.setItem('local_storage_version', BROWSER_STORE_VERSION); } - this._initialized = true; + this.initialized = true; }, getItem: function(name, defaultValue) { @@ -31,19 +33,25 @@ module.exports = { }, removeItem: function(name) { - if (!this._initialized) this._initialize(); + if (!this.initialized) { + this.initialize(); + } localStorage.removeItem(getPrefix() + name); }, setGlobalItem: function(name, value) { - if (!this._initialized) this._initialize(); + if (!this.initialized) { + this.initialize(); + } localStorage.setItem(name, JSON.stringify(value)); }, getGlobalItem: function(name, defaultValue) { - if (!this._initialized) this._initialize(); + if (!this.initialized) { + this.initialize(); + } var result = null; try { @@ -58,7 +66,9 @@ module.exports = { }, removeGlobalItem: function(name) { - if (!this._initialized) this._initialize(); + if (!this.initialized) { + this.initialize(); + } localStorage.removeItem(name); }, @@ -70,10 +80,12 @@ module.exports = { /** * Preforms the given action on each item that has the given prefix - * Signiture for action is action(key, value) + * Signature for action is action(key, value) */ - actionOnItemsWithPrefix: function (prefix, action) { - if (!this._initialized) this._initialize(); + actionOnItemsWithPrefix: function(prefix, action) { + if (!this.initialized) { + this.initialize(); + } var globalPrefix = getPrefix(); var globalPrefixiLen = globalPrefix.length; @@ -87,14 +99,14 @@ module.exports = { isLocalStorageSupported: function() { try { - sessionStorage.setItem("testSession", '1'); - sessionStorage.removeItem("testSession"); + sessionStorage.setItem('testSession', '1'); + sessionStorage.removeItem('testSession'); - localStorage.setItem("testLocal", '1'); - if (localStorage.getItem("testLocal") != '1') { + localStorage.setItem('testLocal', '1'); + if (localStorage.getItem('testLocal') !== '1') { return false; } - localStorage.removeItem("testLocal", '1'); + localStorage.removeItem('testLocal', '1'); return true; } catch (e) { diff --git a/web/react/stores/post_store.jsx b/web/react/stores/post_store.jsx index 76b31204f..7fa9fc125 100644 --- a/web/react/stores/post_store.jsx +++ b/web/react/stores/post_store.jsx @@ -103,7 +103,38 @@ var PostStore = assign({}, EventEmitter.prototype, { this.pStorePosts(channelId, posts); this.emitChange(); }, + pStorePosts: function pStorePosts(channelId, posts) { + BrowserStore.setItem('posts_' + channelId, posts); + }, + getPosts: function getPosts(channelId) { + return BrowserStore.getItem('posts_' + channelId); + }, + storePost: function(post) { + this.pStorePost(post); + this.emitChange(); + }, + pStorePost: function(post) { + var postList = PostStore.getPosts(post.channel_id); + if (!postList) { + return; + } + + if (post.pending_post_id !== '') { + this.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); + } + + this.pStorePosts(post.channel_id, postList); + }, storePendingPost: function(post) { + post.state = Constants.POST_LOADING; + var postList = this.getPendingPosts(post.channel_id); if (!postList) { postList = {posts: {}, order: []}; @@ -114,8 +145,29 @@ var PostStore = assign({}, EventEmitter.prototype, { this._storePendingPosts(post.channel_id, postList); this.emitChange(); }, - _storePendingPosts: function(channelId, posts) { - BrowserStore.setItem('pending_posts_' + channelId, posts); + _storePendingPosts: function(channelId, postList) { + var posts = postList.posts; + + // sort failed posts to the bottom + postList.order.sort(function postSort(a, b) { + if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) { + return 1; + } + if (posts[a].state === Constants.POST_FAILED && posts[b].state === Constants.POST_LOADING) { + return -1; + } + + if (posts[a].create_at > posts[b].create_at) { + return -1; + } + if (posts[a].create_at < posts[b].create_at) { + return 1; + } + + return 0; + }); + + BrowserStore.setItem('pending_posts_' + channelId, postList); }, getPendingPosts: function(channelId) { return BrowserStore.getItem('pending_posts_' + channelId); @@ -140,8 +192,10 @@ var PostStore = assign({}, EventEmitter.prototype, { this._storePendingPosts(channelId, postList); }, - clearPendingPosts: function(channelId) { - BrowserStore.removeItem('pending_posts_' + channelId); + clearPendingPosts: function() { + BrowserStore.actionOnItemsWithPrefix('pending_posts_', function clearPending(key) { + BrowserStore.removeItem(key); + }); }, removeNonFailedPendingPosts: function(channelId) { var postList = this.getPendingPosts(channelId); @@ -171,12 +225,6 @@ var PostStore = assign({}, EventEmitter.prototype, { this._storePendingPosts(post.channel_id, postList); this.emitChange(); }, - pStorePosts: function pStorePosts(channelId, posts) { - BrowserStore.setItem('posts_' + channelId, posts); - }, - getPosts: function getPosts(channelId) { - return BrowserStore.getItem('posts_' + channelId); - }, storeSearchResults: function storeSearchResults(results, isMentionSearch) { BrowserStore.setItem('search_results', results); BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch)); @@ -248,6 +296,10 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) { PostStore.pStorePosts(action.id, action.post_list); PostStore.emitChange(); break; + case ActionTypes.RECIEVED_POST: + PostStore.pStorePost(action.post); + PostStore.emitChange(); + break; case ActionTypes.RECIEVED_SEARCH: PostStore.storeSearchResults(action.results, action.is_mention_search); PostStore.emitSearchChange(); diff --git a/web/react/stores/socket_store.jsx b/web/react/stores/socket_store.jsx index 8ebb854c9..c3c331828 100644 --- a/web/react/stores/socket_store.jsx +++ b/web/react/stores/socket_store.jsx @@ -15,72 +15,80 @@ var CHANGE_EVENT = 'change'; var conn; var SocketStore = assign({}, EventEmitter.prototype, { - initialize: function(self) { - if (!UserStore.getCurrentId()) return; - - if (!self) self = this; - self.setMaxListeners(0); - - if (window["WebSocket"] && !conn) { - var protocol = window.location.protocol == "https:" ? "wss://" : "ws://"; - var port = window.location.protocol == "https:" ? ":8443" : ""; - var conn_url = protocol + location.host + port + "/api/v1/websocket"; - console.log("connecting to " + conn_url); - conn = new WebSocket(conn_url); - - conn.onclose = function(evt) { - console.log("websocket closed"); - console.log(evt); - conn = null; - setTimeout(function(){self.initialize(self)}, 3000); - }; - - conn.onerror = function(evt) { - console.log("websocket error"); - console.log(evt); - }; - - conn.onmessage = function(evt) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_MSG, - msg: JSON.parse(evt.data) - }); - }; - } - }, - emitChange: function(msg) { - this.emit(CHANGE_EVENT, msg); - }, - addChangeListener: function(callback) { - this.on(CHANGE_EVENT, callback); - }, - removeChangeListener: function(callback) { - this.removeListener(CHANGE_EVENT, callback); - }, - sendMessage: function (msg) { - if (conn && conn.readyState === WebSocket.OPEN) { - conn.send(JSON.stringify(msg)); - } else if (!conn || conn.readyState === WebSocket.Closed) { - conn = null; - this.initialize(); + initialize: function() { + if (!UserStore.getCurrentId()) { + return; + } + + var self = this; + self.setMaxListeners(0); + + if (window.WebSocket && !conn) { + var protocol = 'ws://'; + var port = ''; + if (window.location.protocol === 'https:') { + protocol = 'wss://'; + port = ':8443'; + } + var connUrl = protocol + location.host + port + '/api/v1/websocket'; + console.log('connecting to ' + connUrl); + conn = new WebSocket(connUrl); + + conn.onclose = function closeConn(evt) { + console.log('websocket closed'); + console.log(evt); + conn = null; + setTimeout( + function reconnect() { + self.initialize(); + }, + 3000 + ); + }; + + conn.onerror = function connError(evt) { + console.log('websocket error'); + console.log(evt); + }; + + conn.onmessage = function connMessage(evt) { + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_MSG, + msg: JSON.parse(evt.data) + }); + }; + } + }, + emitChange: function(msg) { + this.emit(CHANGE_EVENT, msg); + }, + addChangeListener: function(callback) { + this.on(CHANGE_EVENT, callback); + }, + removeChangeListener: function(callback) { + this.removeListener(CHANGE_EVENT, callback); + }, + sendMessage: function(msg) { + if (conn && conn.readyState === WebSocket.OPEN) { + conn.send(JSON.stringify(msg)); + } else if (!conn || conn.readyState === WebSocket.Closed) { + conn = null; + this.initialize(); + } } - } }); SocketStore.dispatchToken = AppDispatcher.register(function(payload) { - var action = payload.action; + var action = payload.action; - switch(action.type) { - case ActionTypes.RECIEVED_MSG: - SocketStore.emitChange(action.msg); - break; - default: - } + switch (action.type) { + case ActionTypes.RECIEVED_MSG: + SocketStore.emitChange(action.msg); + break; + + default: + } }); SocketStore.initialize(); module.exports = SocketStore; - - - - diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx index 8fa022cbc..349fe9021 100644 --- a/web/react/utils/async_client.jsx +++ b/web/react/utils/async_client.jsx @@ -346,24 +346,33 @@ module.exports.search = function(terms) { module.exports.getPosts = function(force, id, maxPosts) { if (PostStore.getCurrentPosts() == null || force) { - var channelId = id ? id : ChannelStore.getCurrentId(); + var channelId = id; + if (channelId == null) { + channelId = ChannelStore.getCurrentId(); + } - if (isCallInProgress('getPosts_'+channelId)) return; + if (isCallInProgress('getPosts_' + channelId)) { + return; + } - var post_list = PostStore.getCurrentPosts(); + var postList = PostStore.getCurrentPosts(); - if (!maxPosts) { maxPosts = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS }; + var max = maxPosts; + if (max == null) { + max = Constants.POST_CHUNK_SIZE * Constants.MAX_POST_CHUNKS; + } // if we already have more than POST_CHUNK_SIZE posts, // let's get the amount we have but rounded up to next multiple of POST_CHUNK_SIZE, // with a max at maxPosts - var numPosts = Math.min(maxPosts, Constants.POST_CHUNK_SIZE); - if (post_list && post_list.order.length > 0) { - numPosts = Math.min(maxPosts, Constants.POST_CHUNK_SIZE * Math.ceil(post_list.order.length / Constants.POST_CHUNK_SIZE)); + var numPosts = Math.min(max, Constants.POST_CHUNK_SIZE); + if (postList && postList.order.length > 0) { + numPosts = Math.min(max, Constants.POST_CHUNK_SIZE * Math.ceil(postList.order.length / Constants.POST_CHUNK_SIZE)); } if (channelId != null) { - callTracker['getPosts_'+channelId] = utils.getTimestamp(); + callTracker['getPosts_' + channelId] = utils.getTimestamp(); + client.getPosts( channelId, 0, @@ -377,15 +386,13 @@ module.exports.getPosts = function(force, id, maxPosts) { post_list: data }); - PostStore.removeNonFailedPendingPosts(channelId); - module.exports.getProfiles(); }, function(err) { dispatchError(err, 'getPosts'); }, function() { - callTracker['getPosts_'+channelId] = 0; + callTracker['getPosts_' + channelId] = 0; } ); } diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 1fe0faccf..41b02c8d6 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -15,6 +15,7 @@ module.exports = { RECIEVED_CHANNEL_EXTRA_INFO: null, RECIEVED_POSTS: null, + RECIEVED_POST: null, RECIEVED_SEARCH: null, RECIEVED_POST_SELECTED: null, RECIEVED_MENTION_DATA: null, @@ -62,6 +63,8 @@ module.exports = { GOOGLE_SERVICE: 'google', POST_CHUNK_SIZE: 60, MAX_POST_CHUNKS: 3, + POST_LOADING: "loading", + POST_FAILED: "failed", RESERVED_TEAM_NAMES: [ "www", "web", |