diff --git a/model/post.go b/model/post.go index f6f33b1e86..0c035d4e79 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/channel_loader.jsx b/web/react/components/channel_loader.jsx index 6b80f6012c..525b67b5c0 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 885efab7a8..1de768872d 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -1,17 +1,20 @@ // 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 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'); +var ActionTypes = Constants.ActionTypes; module.exports = React.createClass({ lastTime: 0, @@ -26,6 +29,8 @@ module.exports = React.createClass({ return; } + this.setState({submitting: true, serverError: null}); + var post = {}; post.filenames = []; post.message = this.state.messageText; @@ -39,18 +44,23 @@ 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}); + 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); @@ -58,19 +68,27 @@ 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 { - this.setState(state); + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); } + + state.submitting = false; + this.setState(state); }.bind(this) ); }, @@ -137,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 377e7bd34e..3714560eac 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -65,22 +65,47 @@ module.exports = React.createClass({ post.channel_id = this.state.channelId; post.filenames = this.state.previews; - client.createPost(post, ChannelStore.getCurrent(), + var time = utils.getTimestamp(); + 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.get(this.state.channelId); + + PostStore.storePendingPost(post); + PostStore.storeDraft(channel.id, null); + this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null}); + + 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); - 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 = {}; - state.serverError = err.message; + + if (err.message === 'Invalid RootId parameter') { + if ($('#post_deleted').length > 0) { + $('#post_deleted').modal('show'); + } + PostStore.removePendingPost(post.pending_post_id); + } else { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + } state.submitting = false; this.setState(state); @@ -195,20 +220,33 @@ 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(); 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.jsx b/web/react/components/post.jsx index f099c67abb..b798dc7ca5 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,36 @@ 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 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); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post: data + }); + }.bind(this), + function(err) { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }.bind(this) + ); + + post.state = Constants.POST_LOADING; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }, getInitialState: function() { return { }; }, @@ -40,9 +74,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; @@ -79,7 +113,7 @@ module.exports = React.createClass({ : null }
- +
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx index 860c96d842..e5ab5b624a 100644 --- a/web/react/components/post_body.jsx +++ b/web/react/components/post_body.jsx @@ -4,15 +4,16 @@ 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) { var linkData = utils.extractLinks(nextProps.post.message); - this.setState({ links: linkData["links"], message: linkData["text"] }); + this.setState({links: linkData.links, message: linkData.text}); }, getInitialState: function() { var linkData = utils.extractLinks(this.props.post.message); - return { links: linkData["links"], message: linkData["text"] }; + return {links: linkData.links, message: linkData.text}; }, render: function() { var post = this.props.post; @@ -20,43 +21,52 @@ module.exports = React.createClass({ var parentPost = this.props.parentPost; var inner = utils.textToJsx(this.state.message); - var comment = ""; - var reply = ""; - var postClass = ""; + var comment = ''; + var reply = ''; + var postClass = ''; if (parentPost) { var profile = UserStore.getProfile(parentPost.user_id); - var apostrophe = ""; - var name = "..."; + var apostrophe = ''; + var name = '...'; if (profile != null) { if (profile.username.slice(-1) === 's') { - apostrophe = "'"; + apostrophe = '\''; } else { - apostrophe = "'s"; + apostrophe = '\'s'; } - name = {profile.username}; + name = {profile.username}; } - var message = "" - if(parentPost.message) { - message = utils.replaceHtmlEntities(parentPost.message) + var message = ''; + if (parentPost.message) { + message = utils.replaceHtmlEntities(parentPost.message); } else if (parentPost.filenames.length) { message = parentPost.filenames[0].split('/').pop(); if (parentPost.filenames.length === 2) { - message += " plus 1 other file"; + message += ' plus 1 other file'; } else if (parentPost.filenames.length > 2) { - message += " plus " + (parentPost.filenames.length - 1) + " other files"; + message += ' plus ' + (parentPost.filenames.length - 1) + ' other files'; } } comment = ( -

- Commented on {name}{apostrophe} message: {message} +

+ Commented on {name}{apostrophe} message: {message}

); - postClass += " post-comment"; + postClass += ' post-comment'; + } + + var loading; + if (post.state === Constants.POST_FAILED) { + postClass += ' post-fail'; + loading = Retry; + } else if (post.state === Constants.POST_LOADING) { + postClass += ' post-waiting'; + loading = ; } var embed; @@ -64,18 +74,21 @@ module.exports = React.createClass({ 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_info.jsx b/web/react/components/post_info.jsx index 8eaaf4e8c8..f6ab0ed8a2 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -12,41 +12,68 @@ module.exports = React.createClass({ }, render: function() { var post = this.props.post; - var isOwner = UserStore.getCurrentId() == post.user_id; - var isAdmin = UserStore.getCurrentUser().roles.indexOf("admin") > -1 + 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) { - comments = {this.props.commentCount}; + var comments = ''; + var lastCommentClass = ' comment-icon__container__hide'; + if (this.props.isLastComment) { + lastCommentClass = ' comment-icon__container__show'; + } + + if (this.props.commentCount >= 1 && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { + comments = {this.props.commentCount}; + } + + var showDropdown = isOwner || (this.props.allowReply === 'true' && type !== 'Comment'); + if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING) { + showDropdown = false; + } + + var dropdownContents = []; + var dropdown; + if (showDropdown) { + var dataComments = 0; + if (type === 'Post') { + dataComments = this.props.commentCount; + } + + if (isOwner) { + dropdownContents.push(
  • Edit
  • ); + } + + if (isOwner || isAdmin) { + dropdownContents.push(
  • Delete
  • ); + } + + if (this.props.allowReply === 'true') { + dropdownContents.push(
  • Reply
  • ); + } + + dropdown = ( +
    +
    • -
      - : "" } -
      - { comments } -
    • +
        +
      • +
      • +
        + {dropdown} +
        + {comments} +
      ); } diff --git a/web/react/components/post_list.jsx b/web/react/components/post_list.jsx index 8c76eb82c4..c210853ac9 100644 --- a/web/react/components/post_list.jsx +++ b/web/react/components/post_list.jsx @@ -4,7 +4,7 @@ var PostStore = require('../stores/post_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx'); var UserStore = require('../stores/user_store.jsx'); -var UserProfile = require( './user_profile.jsx' ); +var UserProfile = require('./user_profile.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var Post = require('./post.jsx'); var LoadingScreen = require('./loading_screen.jsx'); @@ -18,16 +18,28 @@ var ActionTypes = Constants.ActionTypes; function getStateFromStores() { var channel = ChannelStore.getCurrent(); - if (channel == null) channel = {}; + if (channel == null) { + channel = {}; + } + + var postList = PostStore.getCurrentPosts(); + var pendingPostList = PostStore.getPendingPosts(channel.id); + + if (pendingPostList) { + postList.order = pendingPostList.order.concat(postList.order); + for (var pid in pendingPostList.posts) { + postList.posts[pid] = pendingPostList.posts[pid]; + } + } return { - post_list: PostStore.getCurrentPosts(), + postList: postList, channel: channel }; } module.exports = React.createClass({ - displayName: "PostList", + displayName: 'PostList', scrollPosition: 0, preventScrollTrigger: false, gotMorePosts: false, @@ -37,74 +49,75 @@ module.exports = React.createClass({ componentDidMount: function() { var user = UserStore.getCurrentUser(); if (user.props && user.props.theme) { - utils.changeCss('div.theme', 'background-color:'+user.props.theme+';'); - utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme+';'); - utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme+';'); - utils.changeCss('.mention', 'background: ' + user.props.theme+';'); - utils.changeCss('.mention-link', 'color: ' + user.props.theme+';'); - utils.changeCss('.mention-link', 'color: ' + user.props.theme+';'); - utils.changeCss('.mention-link', 'color: ' + user.props.theme+';'); - utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) +';'); - utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme+';}'); + utils.changeCss('div.theme', 'background-color:' + user.props.theme + ';'); + utils.changeCss('.btn.btn-primary', 'background: ' + user.props.theme + ';'); + utils.changeCss('.modal .modal-header', 'background: ' + user.props.theme + ';'); + utils.changeCss('.mention', 'background: ' + user.props.theme + ';'); + utils.changeCss('.mention-link', 'color: ' + user.props.theme + ';'); + utils.changeCss('@media(max-width: 768px){.search-bar__container', 'background: ' + user.props.theme + ';}'); + utils.changeCss('.search-item-container:hover', 'background: ' + utils.changeOpacity(user.props.theme, 0.05) + ';'); } - if (user.props.theme != '#000000' && user.props.theme != '#585858') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) +';'); - utils.changeCss('a.theme', 'color:'+user.props.theme+'; fill:'+user.props.theme+'!important;'); - } else if (user.props.theme == '#000000') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) +';'); + + if (user.props.theme !== '#000000' && user.props.theme !== '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, -10) + ';'); + utils.changeCss('a.theme', 'color:' + user.props.theme + '; fill:' + user.props.theme + '!important;'); + } else if (user.props.theme === '#000000') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +50) + ';'); $('.team__header').addClass('theme--black'); - } else if (user.props.theme == '#585858') { - utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) +';'); + } else if (user.props.theme === '#585858') { + utils.changeCss('.btn.btn-primary:hover, .btn.btn-primary:active, .btn.btn-primary:focus', 'background: ' + utils.changeColor(user.props.theme, +10) + ';'); $('.team__header').addClass('theme--gray'); } - PostStore.addChangeListener(this._onChange); - ChannelStore.addChangeListener(this._onChange); - UserStore.addStatusesChangeListener(this._onTimeChange); - SocketStore.addChangeListener(this._onSocketChange); + PostStore.addChangeListener(this.onChange); + ChannelStore.addChangeListener(this.onChange); + UserStore.addStatusesChangeListener(this.onTimeChange); + SocketStore.addChangeListener(this.onSocketChange); - $(".post-list-holder-by-time").perfectScrollbar(); + $('.post-list-holder-by-time').perfectScrollbar(); this.resize(); - var post_holder = $(".post-list-holder-by-time")[0]; - this.scrollPosition = $(post_holder).scrollTop() + $(post_holder).innerHeight(); - this.oldScrollHeight = post_holder.scrollHeight; + var postHolder = $('.post-list-holder-by-time')[0]; + this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); + this.oldScrollHeight = postHolder.scrollHeight; this.oldZoom = (window.outerWidth - 8) / window.innerWidth; - $('.modal').on('show.bs.modal', function () { + $('.modal').on('show.bs.modal', function onShow() { $('.modal-body').css('overflow-y', 'auto'); $('.modal-body').css('max-height', $(window).height() * 0.7); }); // Timeout exists for the DOM to fully render before making changes var self = this; - $(window).resize(function(){ - $(post_holder).perfectScrollbar('update'); + $(window).resize(function resize() { + $(postHolder).perfectScrollbar('update'); // this only kind of works, detecting zoom in browsers is a nightmare var newZoom = (window.outerWidth - 8) / window.innerWidth; - if (self.scrollPosition >= post_holder.scrollHeight || (self.oldScrollHeight != post_holder.scrollHeight && self.scrollPosition >= self.oldScrollHeight) || self.oldZoom != newZoom) self.resize(); + if (self.scrollPosition >= postHolder.scrollHeight || (self.oldScrollHeight !== postHolder.scrollHeight && self.scrollPosition >= self.oldScrollHeight) || self.oldZoom !== newZoom) { + self.resize(); + } self.oldZoom = newZoom; if ($('#create_post').length > 0) { var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; - $(".post-list-holder-by-time").css("height", height + "px"); + $('.post-list-holder-by-time').css('height', height + 'px'); } }); - $(post_holder).scroll(function(e){ + $(postHolder).scroll(function scroll() { if (!self.preventScrollTrigger) { - self.scrollPosition = $(post_holder).scrollTop() + $(post_holder).innerHeight(); + self.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); } self.preventScrollTrigger = false; }); - $('body').on('click.userpopover', function(e){ - if ($(e.target).attr('data-toggle') !== 'popover' - && $(e.target).parents('.popover.in').length === 0) { + $('body').on('click.userpopover', function popOver(e) { + if ($(e.target).attr('data-toggle') !== 'popover' && + $(e.target).parents('.popover.in').length === 0) { $('.user-popover').popover('hide'); } }); @@ -112,67 +125,62 @@ module.exports = React.createClass({ $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); - $('body').on('mouseenter mouseleave', '.post', function(ev){ - if(ev.type === 'mouseenter'){ + $('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) { + if (ev.type === 'mouseenter') { $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after'); $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before'); - } - else { + } else { $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after'); $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before'); } }); - $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function(ev){ - if(ev.type === 'mouseenter'){ + $('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) { + if (ev.type === 'mouseenter') { $(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment'); $(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment'); - } - else { + } else { $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); } }); - }, componentDidUpdate: function() { this.resize(); - var post_holder = $(".post-list-holder-by-time")[0]; - this.scrollPosition = $(post_holder).scrollTop() + $(post_holder).innerHeight(); - this.oldScrollHeight = post_holder.scrollHeight; + var postHolder = $('.post-list-holder-by-time')[0]; + this.scrollPosition = $(postHolder).scrollTop() + $(postHolder).innerHeight(); + this.oldScrollHeight = postHolder.scrollHeight; $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); }, componentWillUnmount: function() { - PostStore.removeChangeListener(this._onChange); - ChannelStore.removeChangeListener(this._onChange); - UserStore.removeStatusesChangeListener(this._onTimeChange); - SocketStore.removeChangeListener(this._onSocketChange); + PostStore.removeChangeListener(this.onChange); + ChannelStore.removeChangeListener(this.onChange); + UserStore.removeStatusesChangeListener(this.onTimeChange); + SocketStore.removeChangeListener(this.onSocketChange); $('body').off('click.userpopover'); - $('.modal').off('show.bs.modal') + $('.modal').off('show.bs.modal'); }, resize: function() { - var post_holder = $(".post-list-holder-by-time")[0]; + var postHolder = $('.post-list-holder-by-time')[0]; this.preventScrollTrigger = true; if (this.gotMorePosts) { this.gotMorePosts = false; - $(post_holder).scrollTop($(post_holder).scrollTop() + (post_holder.scrollHeight-this.oldScrollHeight) ); + $(postHolder).scrollTop($(postHolder).scrollTop() + (postHolder.scrollHeight - this.oldScrollHeight)); + } else if ($('#new_message')[0] && !this.scrolledToNew) { + $(postHolder).scrollTop($(postHolder).scrollTop() + $('#new_message').offset().top - 63); + this.scrolledToNew = true; } else { - if ($("#new_message")[0] && !this.scrolledToNew) { - $(post_holder).scrollTop($(post_holder).scrollTop() + $("#new_message").offset().top - 63); - this.scrolledToNew = true; - } else { - $(post_holder).scrollTop(post_holder.scrollHeight); - } + $(postHolder).scrollTop(postHolder.scrollHeight); } - $(post_holder).perfectScrollbar('update'); + $(postHolder).perfectScrollbar('update'); }, - _onChange: function() { + onChange: function() { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { - if (this.state.post_list && this.state.post_list.order) { - if (this.state.channel.id === newState.channel.id && this.state.post_list.order.length != newState.post_list.order.length && newState.post_list.order.length > Constants.POST_CHUNK_SIZE) { + if (this.state.postList && this.state.postList.order) { + if (this.state.channel.id === newState.channel.id && this.state.postList.order.length !== newState.postList.order.length && newState.postList.order.length > Constants.POST_CHUNK_SIZE) { this.gotMorePosts = true; } } @@ -182,112 +190,127 @@ module.exports = React.createClass({ this.setState(newState); } }, - _onSocketChange: function(msg) { - if (msg.action == "posted") { - var post = JSON.parse(msg.props.post); + 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({postList: postList}); + } - var post_list = PostStore.getPosts(msg.channel_id); - if (!post_list) return; + PostStore.storePosts(post.channel_id, postList); + } else if (msg.action === 'post_edited') { + if (this.state.channel.id === msg.channel_id) { + postList = this.state.postList; + if (!(msg.props.post_id in postList.posts)) { + return; + } - post_list.posts[post.id] = post; - if (post_list.order.indexOf(post.id) === -1) { - post_list.order.unshift(post.id); + post = postList.posts[msg.props.post_id]; + post.message = msg.props.message; + + postList.posts[post.id] = post; + this.setState({postList: postList}); + + PostStore.storePosts(msg.channel_id, postList); + } else { + AsyncClient.getPosts(true, msg.channel_id); + } + } else if (msg.action === 'post_deleted') { + var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; + var activeRootPostId = ''; + if (activeRoot && activeRoot.id.length > 0) { + activeRootPostId = activeRoot.id; } if (this.state.channel.id === msg.channel_id) { - this.setState({ post_list: post_list }); - }; + postList = this.state.postList; + if (!(msg.props.post_id in this.state.postList.posts)) { + return; + } - PostStore.storePosts(post.channel_id, post_list); - } else if (msg.action == "post_edited") { - if (this.state.channel.id == msg.channel_id) { - var post_list = this.state.post_list; - if (!(msg.props.post_id in post_list.posts)) return; + delete postList.posts[msg.props.post_id]; + var index = postList.order.indexOf(msg.props.post_id); + if (index > -1) { + postList.order.splice(index, 1); + } - var post = post_list.posts[msg.props.post_id]; - post.message = msg.props.message; + this.setState({postList: postList}); - post_list.posts[post.id] = post; - this.setState({ post_list: post_list }); - - PostStore.storePosts(msg.channel_id, post_list); - } else { - AsyncClient.getPosts(true, msg.channel_id); - } - } else if (msg.action == "post_deleted") { - var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; - var activeRootPostId = activeRoot && activeRoot.id.length > 0 ? activeRoot.id : ""; - - if (this.state.channel.id == msg.channel_id) { - var post_list = this.state.post_list; - if (!(msg.props.post_id in this.state.post_list.posts)) return; - - delete post_list.posts[msg.props.post_id]; - var index = post_list.order.indexOf(msg.props.post_id); - if (index > -1) post_list.order.splice(index, 1); - - this.setState({ post_list: post_list }); - - PostStore.storePosts(msg.channel_id, post_list); + PostStore.storePosts(msg.channel_id, postList); } else { AsyncClient.getPosts(true, msg.channel_id); } - if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() != msg.user_id) { + if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { $('#post_deleted').modal('show'); } - } else if (msg.action == "new_user") { + } else if (msg.action === 'new_user') { AsyncClient.getProfiles(); } }, - _onTimeChange: function() { - if (!this.state.post_list) return; - for (var id in this.state.post_list.posts) { - if (!this.refs[id]) continue; + onTimeChange: function() { + if (!this.state.postList) { + return; + } + + for (var id in this.state.postList.posts) { + if (!this.refs[id]) { + continue; + } this.refs[id].forceUpdateInfo(); } }, getMorePosts: function(e) { e.preventDefault(); - if (!this.state.post_list) return; + if (!this.state.postList) { + return; + } - var posts = this.state.post_list.posts; - var order = this.state.post_list.order; - var channel_id = this.state.channel.id; + var posts = this.state.postList.posts; + var order = this.state.postList.order; + var channelId = this.state.channel.id; - $(this.refs.loadmore.getDOMNode()).text("Retrieving more messages..."); + $(this.refs.loadmore.getDOMNode()).text('Retrieving more messages...'); var self = this; - var currentPos = $(".post-list").scrollTop; + var currentPos = $('.post-list').scrollTop; Client.getPosts( - channel_id, + channelId, order.length, Constants.POST_CHUNK_SIZE, - function(data) { - $(self.refs.loadmore.getDOMNode()).text("Load more messages"); + function success(data) { + $(self.refs.loadmore.getDOMNode()).text('Load more messages'); - if (!data) return; + if (!data) { + return; + } - if (data.order.length === 0) return; + if (data.order.length === 0) { + return; + } - var post_list = {} - post_list.posts = $.extend(posts, data.posts); - post_list.order = order.concat(data.order); + var postList = {}; + postList.posts = $.extend(posts, data.posts); + postList.order = order.concat(data.order); AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_POSTS, - id: channel_id, - post_list: post_list + id: channelId, + postList: postList }); Client.getProfiles(); - $(".post-list").scrollTop(currentPos); + $('.post-list').scrollTop(currentPos); }, - function(err) { - $(self.refs.loadmore.getDOMNode()).text("Load more messages"); - AsyncClient.dispatchError(err, "getPosts"); + function fail(err) { + $(self.refs.loadmore.getDOMNode()).text('Load more messages'); + AsyncClient.dispatchError(err, 'getPosts'); } ); }, @@ -298,81 +321,86 @@ module.exports = React.createClass({ var order = []; var posts; - var last_viewed = Number.MAX_VALUE; + var lastViewed = Number.MAX_VALUE; - if (ChannelStore.getCurrentMember() != null) - last_viewed = ChannelStore.getCurrentMember().last_viewed_at; - - if (this.state.post_list != null) { - posts = this.state.post_list.posts; - order = this.state.post_list.order; + if (ChannelStore.getCurrentMember() != null) { + lastViewed = ChannelStore.getCurrentMember().lastViewed_at; } - var rendered_last_viewed = false; + if (this.state.postList != null) { + posts = this.state.postList.posts; + order = this.state.postList.order; + } - var user_id = ""; + var renderedLastViewed = false; + + var userId = ''; if (UserStore.getCurrentId()) { - user_id = UserStore.getCurrentId(); + userId = UserStore.getCurrentId(); } else { return
      ; } var channel = this.state.channel; - var more_messages =

      Beginning of Channel

      ; + var moreMessages =

      Beginning of Channel

      ; - var userStyle = { color: UserStore.getCurrentUser().props.theme } + var userStyle = {color: UserStore.getCurrentUser().props.theme}; if (channel != null) { if (order.length > 0 && order.length % Constants.POST_CHUNK_SIZE === 0) { - more_messages = Load more messages; + moreMessages = Load more messages; } else if (channel.type === 'D') { - var teammate = utils.getDirectTeammate(channel.id) + var teammate = utils.getDirectTeammate(channel.id); if (teammate) { - var teammate_name = teammate.nickname.length > 0 ? teammate.nickname : teammate.username; - more_messages = ( -
      -
      - + var teammateName = teammate.username; + if (teammate.nickname.length > 0) { + teammateName = teammate.nickname; + } + + moreMessages = ( +
      +
      +
      -
      +
      -

      - {"This is the start of your private message history with " + teammate_name + "." }
      - {"Private messages and files shared here are not shown to people outside this area."} +

      + {'This is the start of your private message history with ' + teammateName + '.'}
      + {'Private messages and files shared here are not shown to people outside this area.'}

      - Set a description + Set a description
      ); } else { - more_messages = ( -
      -

      {"This is the start of your private message history with this " + strings.Team + "mate. Private messages and files shared here are not shown to people outside this area."}

      + moreMessages = ( +
      +

      {'This is the start of your private message history with this ' + strings.Team + 'mate. Private messages and files shared here are not shown to people outside this area.'}

      ); } } else if (channel.type === 'P' || channel.type === 'O') { - var ui_name = channel.display_name + var uiName = channel.display_name; var members = ChannelStore.getCurrentExtraInfo().members; - var creator_name = ""; + var creatorName = ''; for (var i = 0; i < members.length; i++) { if (members[i].roles.indexOf('admin') > -1) { - creator_name = members[i].username; + creatorName = members[i].username; break; } } if (ChannelStore.isDefault(channel)) { - more_messages = ( -
      -

      Beginning of {ui_name}

      -

      - Welcome to {ui_name}! + moreMessages = ( +

      +

      Beginning of {uiName}

      +

      + Welcome to {uiName}!

      - {"This is the first channel " + strings.Team + "mates see when they"} + {'This is the first channel ' + strings.Team + 'mates see when they'}
      sign up - use it for posting updates everyone needs to know.

      @@ -384,29 +412,44 @@ module.exports = React.createClass({

      ); } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { - more_messages = ( -
      -

      Beginning of {ui_name}

      -

      - {"This is the start of " + ui_name + ", a channel for non-work-related conversations."} + moreMessages = ( +

      +

      Beginning of {uiName}

      +

      + {'This is the start of ' + uiName + ', a channel for non-work-related conversations.'}

      - Set a description + Set a description
      ); } else { - var ui_type = channel.type === 'P' ? "private group" : "channel"; - more_messages = ( -
      -

      Beginning of {ui_name}

      -

      - { creator_name != "" ? "This is the start of the " + ui_name + " " + ui_type + ", created by " + creator_name + " on " + utils.displayDate(channel.create_at) + "." - : "This is the start of the " + ui_name + " " + ui_type + ", created on "+ utils.displayDate(channel.create_at) + "." } - { channel.type === 'P' ? " Only invited members can see this private group." : " Any member can join and read this channel." } + var uiType; + var memberMessage; + if (channel.type === 'P') { + uiType = 'private group'; + memberMessage = ' Only invited members can see this private group.'; + } else { + uiType = 'channel'; + memberMessage = ' Any member can join and read this channel.'; + } + + var createMessage; + if (creatorName !== '') { + createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created by ' + creatorName + ' on ' + utils.displayDate(channel.create_at) + '.'; + } else { + createMessage = 'This is the start of the ' + uiName + ' ' + uiType + ', created on ' + utils.displayDate(channel.create_at) + '.'; + } + + moreMessages = ( +

      +

      Beginning of {uiName}

      +

      + {createMessage} + {memberMessage}

      - Set a description - Invite others to this {ui_type} + Set a description + Invite others to this {uiType}
      ); } @@ -419,17 +462,25 @@ module.exports = React.createClass({ var previousPostDay = new Date(0); var currentPostDay; - for (var i = order.length-1; i >= 0; i--) { + for (var i = order.length - 1; i >= 0; i--) { var post = posts[order[i]]; - var parentPost = post.parent_id ? posts[post.parent_id] : null; + var parentPost = null; + if (post.parent_id) { + parentPost = posts[post.parent_id]; + } var sameUser = ''; var sameRoot = false; var hideProfilePic = false; - var prevPost = (i < order.length - 1) ? posts[order[i + 1]] : null; + var prevPost; + if (i < order.length - 1) { + prevPost = posts[order[i + 1]]; + } if (prevPost) { - sameUser = (prevPost.user_id === post.user_id) && (post.create_at - prevPost.create_at <= 1000*60*5) ? "same--user" : ""; + if ((prevPost.user_id === post.user_id) && (post.create_at - prevPost.create_at <= 1000 * 60 * 5)) { + sameUser = 'same--user'; + } sameRoot = utils.isComment(post) && (prevPost.id === post.root_id || prevPost.root_id === post.root_id); // we only hide the profile pic if the previous post is not a comment, the current post is not a comment, and the previous post was made by the same user as the current post @@ -438,7 +489,7 @@ module.exports = React.createClass({ // check if it's the last comment in a consecutive string of comments on the same post // 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 isLastComment = utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id); var postCtl = ( -
      -
      {currentPostDay.toDateString()}
      +
      +
      +
      {currentPostDay.toDateString()}
      ); } - if (post.create_at > last_viewed && !rendered_last_viewed) { - rendered_last_viewed = true; + if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) { + renderedLastViewed = true; postCtls.push( -
      -
      -
      New Messages
      +
      +
      +
      New Messages
      ); } @@ -469,15 +520,15 @@ module.exports = React.createClass({ previousPostDay = currentPostDay; } } else { - postCtls.push(); + postCtls.push(); } return ( -
      -
      -
      - { more_messages } - { postCtls } +
      +
      +
      + {moreMessages} + {postCtls}
      diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index e46979ff7c..175e1080da 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -3,15 +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({ @@ -44,12 +46,15 @@ RhsHeaderPost = React.createClass({ }); }, render: function() { - var back = this.props.fromSearch ? : ""; + var back; + if (this.props.fromSearch) { + back = ; + } return ( -
      - {back}Message Details - +
      + {back}Message Details +
      ); } @@ -59,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 = ( + + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + + ); } return ( -
      -
      { channelName }
      -
      - +
      +
      { channelName }
      +
      +
      -
      -
        -
      • -
      • -
      • -
        - { isOwner ? - - : "" } +
        +
          +
        • +
        • +
        • +
          + {ownerOptions}
        -
        +

        {message}

        - { post.filenames && post.filenames.length > 0 ? - - : "" } + {fileAttachment}

        @@ -119,56 +139,104 @@ RootPost = React.createClass({ }); CommentPost = React.createClass({ + retryComment: function(e) { + e.preventDefault(); + + var post = this.props.post; + client.createPost(post, post.channel_id, + function success(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); + + AppDispatcher.handleServerAction({ + type: ActionTypes.RECIEVED_POST, + post: data + }); + }.bind(this), + function fail() { + post.state = Constants.POST_FAILED; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }.bind(this) + ); + + post.state = Constants.POST_LOADING; + PostStore.updatePendingPost(post); + this.forceUpdate(); + }, render: function() { var post = this.props.post; - var commentClass = "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; - return ( -
        -
        - + var loading; + var postClass = ''; + if (post.state === Constants.POST_FAILED) { + postClass += ' post-fail'; + loading = Retry; + } else if (post.state === Constants.POST_LOADING) { + postClass += ' post-waiting'; + loading = ; + } + + var ownerOptions; + if (isOwner && post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING) { + ownerOptions = ( + -
        -
          -
        • -
        • -
        • - { isOwner ? - - : "" } + ); + } + + var fileAttachment; + if (post.filenames && post.filenames.length > 0) { + fileAttachment = ( + + ); + } + + return ( +
          +
          + +
          +
          +
            +
          • +
          • +
          • + {ownerOptions}
          -
          -

          {message}

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

          {loading}{message}

          + {fileAttachment}
          @@ -177,31 +245,45 @@ CommentPost = React.createClass({ }); function getStateFromStores() { - return { post_list: PostStore.getSelectedPost() }; + var postList = PostStore.getSelectedPost(); + if (!postList || postList.order.length < 1) { + return {postList: {}}; + } + + var channelId = postList.posts[postList.order[0]].channel_id; + var pendingPostList = PostStore.getPendingPosts(channelId); + + if (pendingPostList) { + for (var pid in pendingPostList.posts) { + postList.posts[pid] = pendingPostList.posts[pid]; + } + } + + return {postList: postList}; } module.exports = React.createClass({ componentDidMount: function() { - PostStore.addSelectedPostChangeListener(this._onChange); - PostStore.addChangeListener(this._onChangeAll); - UserStore.addStatusesChangeListener(this._onTimeChange); + PostStore.addSelectedPostChangeListener(this.onChange); + PostStore.addChangeListener(this.onChangeAll); + UserStore.addStatusesChangeListener(this.onTimeChange); this.resize(); var self = this; - $(window).resize(function(){ + $(window).resize(function() { self.resize(); }); }, 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() { - PostStore.removeSelectedPostChangeListener(this._onChange); - PostStore.removeChangeListener(this._onChangeAll); - UserStore.removeStatusesChangeListener(this._onTimeChange); + PostStore.removeSelectedPostChangeListener(this.onChange); + PostStore.removeChangeListener(this.onChangeAll); + UserStore.removeStatusesChangeListener(this.onTimeChange); }, - _onChange: function() { + onChange: function() { if (this.isMounted()) { var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { @@ -209,24 +291,22 @@ module.exports = React.createClass({ } } }, - _onChangeAll: function() { + onChangeAll: function() { if (this.isMounted()) { - // if something was changed in the channel like adding a // comment or post then lets refresh the sidebar list var currentSelected = PostStore.getSelectedPost(); - if (!currentSelected || currentSelected.order.length == 0) { + if (!currentSelected || currentSelected.order.length === 0) { return; } var currentPosts = PostStore.getPosts(currentSelected.posts[currentSelected.order[0]].channel_id); - if (!currentPosts || currentPosts.order.length == 0) { + if (!currentPosts || currentPosts.order.length === 0) { return; } - - if (currentPosts.posts[currentPosts.order[0]].channel_id == currentSelected.posts[currentSelected.order[0]].channel_id) { + if (currentPosts.posts[currentPosts.order[0]].channel_id === currentSelected.posts[currentSelected.order[0]].channel_id) { currentSelected.posts = {}; for (var postId in currentPosts.posts) { currentSelected.posts[postId] = currentPosts.posts[postId]; @@ -238,9 +318,11 @@ module.exports = React.createClass({ this.setState(getStateFromStores()); } }, - _onTimeChange: function() { - for (var id in this.state.post_list.posts) { - if (!this.refs[id]) continue; + onTimeChange: function() { + for (var id in this.state.postList.posts) { + if (!this.refs[id]) { + continue; + } this.refs[id].forceUpdate(); } }, @@ -249,68 +331,70 @@ 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() { + var postList = this.state.postList; - var post_list = this.state.post_list; - - if (post_list == null) { + if (postList == null) { return (
          ); } - var selected_post = post_list.posts[post_list.order[0]]; - var root_post = null; + var selectedPost = postList.posts[postList.order[0]]; + var rootPost = null; - if (selected_post.root_id == "") { - root_post = selected_post; - } - else { - root_post = post_list.posts[selected_post.root_id]; + if (selectedPost.root_id === '') { + rootPost = selectedPost; + } else { + rootPost = postList.posts[selectedPost.root_id]; } - var posts_array = []; + var postsArray = []; - for (var postId in post_list.posts) { - var cpost = post_list.posts[postId]; - if (cpost.root_id == root_post.id) { - posts_array.push(cpost); + for (var postId in postList.posts) { + var cpost = postList.posts[postId]; + if (cpost.root_id === rootPost.id) { + postsArray.push(cpost); } } - posts_array.sort(function(a,b) { - if (a.create_at < b.create_at) + postsArray.sort(function postSort(a, b) { + if (a.create_at < b.create_at) { return -1; - if (a.create_at > b.create_at) + } + if (a.create_at > b.create_at) { return 1; + } return 0; }); - var results = this.state.results; var currentId = UserStore.getCurrentId(); - var searchForm = currentId == null ? null : ; + var searchForm; + if (currentId != null) { + searchForm = ; + } return ( -
          +
          -
          {searchForm}
          -
          +
          {searchForm}
          +
          -
          - -
          - { posts_array.map(function(cpost) { - return +
          + +
          + {postsArray.map(function mapPosts(comPost) { + return ; })}
          -
          - +
          +
          diff --git a/web/react/stores/browser_store.jsx b/web/react/stores/browser_store.jsx index 4eed754cc9..436b8d76d9 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 9ebdf734ce..3e4fde30aa 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); }, @@ -110,6 +109,108 @@ var PostStore = assign({}, EventEmitter.prototype, { 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: []}; + } + + postList.posts[post.pending_post_id] = post; + postList.order.unshift(post.pending_post_id); + this._storePendingPosts(post.channel_id, postList); + this.emitChange(); + }, + _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); + }, + removePendingPost: function(channelId, pendingPostId) { + this._removePendingPost(channelId, pendingPostId); + this.emitChange(); + }, + _removePendingPost: function(channelId, pendingPostId) { + var postList = this.getPendingPosts(channelId); + if (!postList) { + return; + } + + if (pendingPostId in postList.posts) { + delete postList.posts[pendingPostId]; + } + var index = postList.order.indexOf(pendingPostId); + if (index >= 0) { + postList.order.splice(index, 1); + } + + this._storePendingPosts(channelId, postList); + }, + clearPendingPosts: function() { + BrowserStore.actionOnItemsWithPrefix('pending_posts_', function clearPending(key) { + BrowserStore.removeItem(key); + }); + }, + updatePendingPost: function(post) { + var postList = this.getPendingPosts(post.channel_id); + if (!postList) { + postList = {posts: {}, order: []}; + } + + if (postList.order.indexOf(post.pending_post_id) === -1) { + return; + } + + postList.posts[post.pending_post_id] = post; + this._storePendingPosts(post.channel_id, postList); + this.emitChange(); + }, storeSearchResults: function storeSearchResults(results, isMentionSearch) { BrowserStore.setItem('search_results', results); BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch)); @@ -181,6 +282,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 8ebb854c94..c3c331828e 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; + initialize: function() { + if (!UserStore.getCurrentId()) { + return; + } - if (!self) self = this; - self.setMaxListeners(0); + var 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); + 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(evt) { - console.log("websocket closed"); - console.log(evt); - conn = null; - setTimeout(function(){self.initialize(self)}, 3000); - }; + conn.onclose = function closeConn(evt) { + console.log('websocket closed'); + console.log(evt); + conn = null; + setTimeout( + function reconnect() { + self.initialize(); + }, + 3000 + ); + }; - conn.onerror = function(evt) { - console.log("websocket error"); - console.log(evt); - }; + conn.onerror = function connError(evt) { + console.log('websocket error'); + console.log(evt); + }; - conn.onmessage = function(evt) { - AppDispatcher.handleServerAction({ - type: ActionTypes.RECIEVED_MSG, - msg: JSON.parse(evt.data) - }); - }; + 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(); + } } - }, - 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 8b6d821d60..349fe90213 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, @@ -383,7 +392,7 @@ module.exports.getPosts = function(force, id, maxPosts) { 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 1fe0faccf2..41b02c8d6b 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", diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss index c7add21a27..e665be6b9c 100644 --- a/web/sass-files/sass/partials/_post.scss +++ b/web/sass-files/sass/partials/_post.scss @@ -315,6 +315,21 @@ body.ios { font-size: 0.97em; white-space: pre-wrap; } + + .post-loading-gif { + height:10px; + width:10px; + margin-top:6px; + } + + .post-fail { + color: #D58A8A; + } + + .post-waiting { + color: #999; + } + .comment-icon__container { margin-left: 7px; fill: $primary-color;