// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. 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 AsyncClient = require('../utils/async_client.jsx'); var Post = require('./post.jsx'); var LoadingScreen = require('./loading_screen.jsx'); var SocketStore = require('../stores/socket_store.jsx'); var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; export default class PostList extends React.Component { constructor(props) { super(props); this.gotMorePosts = false; this.scrolled = false; this.prevScrollTop = 0; this.seenNewMessages = false; this.isUserScroll = true; this.userHasSeenNew = false; this.loadInProgress = false; this.onChange = this.onChange.bind(this); this.onTimeChange = this.onTimeChange.bind(this); this.onSocketChange = this.onSocketChange.bind(this); this.createChannelIntroMessage = this.createChannelIntroMessage.bind(this); this.loadMorePosts = this.loadMorePosts.bind(this); this.loadFirstPosts = this.loadFirstPosts.bind(this); this.activate = this.activate.bind(this); this.deactivate = this.deactivate.bind(this); this.resize = this.resize.bind(this); this.state = this.getStateFromStores(props.channelId); this.state.numToDisplay = Constants.POST_CHUNK_SIZE; this.state.isFirstLoadComplete = false; } getStateFromStores(id) { var postList = PostStore.getPosts(id); if (postList != null) { var deletedPosts = PostStore.getUnseenDeletedPosts(id); if (deletedPosts && Object.keys(deletedPosts).length > 0) { for (var pid in deletedPosts) { if (deletedPosts.hasOwnProperty(pid)) { postList.posts[pid] = deletedPosts[pid]; postList.order.unshift(pid); } } postList.order.sort(function postSort(a, b) { if (postList.posts[a].create_at > postList.posts[b].create_at) { return -1; } if (postList.posts[a].create_at < postList.posts[b].create_at) { return 1; } return 0; }); } var pendingPostList = PostStore.getPendingPosts(id); if (pendingPostList) { postList.order = pendingPostList.order.concat(postList.order); for (var ppid in pendingPostList.posts) { if (pendingPostList.posts.hasOwnProperty(ppid)) { postList.posts[ppid] = pendingPostList.posts[ppid]; } } } } return { postList: postList }; } componentDidMount() { window.onload = () => this.scrollToBottom(); if (this.props.isActive) { this.activate(); this.loadFirstPosts(this.props.channelId); } } componentWillUnmount() { this.deactivate(); } activate() { this.gotMorePosts = false; this.scrolled = false; this.prevScrollTop = 0; this.seenNewMessages = false; this.isUserScroll = true; this.userHasSeenNew = false; PostStore.clearUnseenDeletedPosts(this.props.channelId); PostStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onTimeChange); SocketStore.addChangeListener(this.onSocketChange); var postHolder = $(React.findDOMNode(this.refs.postlist)); $(window).on('resize.' + this.props.channelId, function resize() { this.resize(); if (!this.scrolled) { this.scrollToBottom(); } }.bind(this)); postHolder.on('scroll', function scroll() { var position = postHolder.scrollTop() + postHolder.height() + 14; var bottom = postHolder[0].scrollHeight; if (position >= bottom) { this.scrolled = false; } else { this.scrolled = true; } if (this.isUserScroll) { this.userHasSeenNew = true; } this.isUserScroll = true; }.bind(this)); $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); if (!this.state.isFirstLoadComplete) { this.loadFirstPosts(this.props.channelId); } this.resize(); this.onChange(); this.scrollToBottom(); } deactivate() { PostStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onTimeChange); SocketStore.removeChangeListener(this.onSocketChange); $('body').off('click.userpopover'); $(window).off('resize.' + this.props.channelId); var postHolder = $(React.findDOMNode(this.refs.postlist)); postHolder.off('scroll'); } componentDidUpdate(prevProps, prevState) { if (!this.props.isActive) { return; } $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); if (this.state.postList == null || prevState.postList == null) { this.scrollToBottom(); return; } var order = this.state.postList.order || []; var posts = this.state.postList.posts || {}; var oldOrder = prevState.postList.order || []; var oldPosts = prevState.postList.posts || {}; var userId = UserStore.getCurrentId(); var firstPost = posts[order[0]] || {}; var isNewPost = oldOrder.indexOf(order[0]) === -1; if (this.props.isActive && !prevProps.isActive) { this.scrollToBottom(); } else if (oldOrder.length === 0) { this.scrollToBottom(); // the user is scrolled to the bottom } else if (!this.scrolled) { this.scrollToBottom(); // there's a new post and // it's by the user and not a comment } else if (isNewPost && userId === firstPost.user_id && !utils.isComment(firstPost)) { this.scrollToBottom(true); // the user clicked 'load more messages' } else if (this.gotMorePosts && oldOrder.length > 0) { let index; if (prevState.numToDisplay >= oldOrder.length) { index = oldOrder.length - 1; } else { index = prevState.numToDisplay; } const lastPost = oldPosts[oldOrder[index]]; $('#post_' + lastPost.id)[0].scrollIntoView(); this.gotMorePosts = false; } else { this.scrollTo(this.prevScrollTop); } } componentWillUpdate() { var postHolder = $(React.findDOMNode(this.refs.postlist)); this.prevScrollTop = postHolder.scrollTop(); } componentWillReceiveProps(nextProps) { if (nextProps.isActive === true && this.props.isActive === false) { this.activate(); } else if (nextProps.isActive === false && this.props.isActive === true) { this.deactivate(); } } resize() { const postHolder = $(React.findDOMNode(this.refs.postlist)); if ($('#create_post').length > 0) { const height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; postHolder.css('height', height + 'px'); } } scrollTo(val) { this.isUserScroll = false; var postHolder = $(React.findDOMNode(this.refs.postlist)); postHolder[0].scrollTop = val; } scrollToBottom(force) { this.isUserScroll = false; var postHolder = $(React.findDOMNode(this.refs.postlist)); if ($('#new_message_' + this.props.channelId)[0] && !this.userHasSeenNew && !force) { $('#new_message_' + this.props.channelId)[0].scrollIntoView(); } else { postHolder.addClass('hide-scroll'); postHolder[0].scrollTop = postHolder[0].scrollHeight; postHolder.removeClass('hide-scroll'); } } loadFirstPosts(id) { if (this.loadInProgress) { return; } if (this.props.channelId == null) { return; } this.loadInProgress = true; Client.getPosts( id, PostStore.getLatestUpdate(id), function success() { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); }.bind(this), function fail() { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); }.bind(this) ); } onChange() { var newState = this.getStateFromStores(this.props.channelId); if (!utils.areStatesEqual(newState.postList, this.state.postList)) { this.setState(newState); } } onSocketChange(msg) { var post; if (msg.action === 'posted' || msg.action === 'post_edited') { post = JSON.parse(msg.props.post); PostStore.storePost(post); } 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; } post = JSON.parse(msg.props.post); PostStore.storeUnseenDeletedPost(post); PostStore.removePost(post, true); PostStore.emitChange(); if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { $('#post_deleted').modal('show'); } } else if (msg.action === 'new_user') { AsyncClient.getProfiles(); } } onTimeChange() { if (!this.state.postList) { return; } for (var id in this.state.postList.posts) { if (!this.refs[id]) { continue; } this.refs[id].forceUpdateInfo(); } } createDMIntroMessage(channel) { var teammate = utils.getDirectTeammate(channel.id); if (teammate) { var teammateName = teammate.username; if (teammate.nickname.length > 0) { teammateName = teammate.nickname; } return (

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

Set a description
); } return (

{'This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'}

); } createChannelIntroMessage(channel) { if (channel.type === 'D') { return this.createDMIntroMessage(channel); } else if (ChannelStore.isDefault(channel)) { return this.createDefaultIntroMessage(channel); } else if (channel.name === Constants.OFFTOPIC_CHANNEL) { return this.createOffTopicIntroMessage(channel); } else if (channel.type === 'O' || channel.type === 'P') { return this.createStandardIntroMessage(channel); } } createDefaultIntroMessage(channel) { return (

Beginning of {channel.display_name}

Welcome to {channel.display_name}!

This is the first channel teammates see when they
sign up - use it for posting updates everyone needs to know.

To create a new channel or join an existing one, go to
the Left Hand Sidebar under “Channels” and click “More…”.

); } createOffTopicIntroMessage(channel) { return (

Beginning of {channel.display_name}

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

Set a description Invite others to this channel
); } getChannelCreator(channel) { if (channel.creator_id.length > 0) { var creator = UserStore.getProfile(channel.creator_id); if (creator) { return creator.username; } } var members = ChannelStore.getExtraInfo(channel.id).members; for (var i = 0; i < members.length; i++) { if (utils.isAdmin(members[i].roles)) { return members[i].username; } } } createStandardIntroMessage(channel) { var uiName = channel.display_name; var creatorName = ''; 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) + '.'; } return (

Beginning of {uiName}

{createMessage} {memberMessage}

Set a description Invite others to this {uiType}
); } createPosts(posts, order) { var postCtls = []; var previousPostDay = new Date(0); var userId = UserStore.getCurrentId(); var renderedLastViewed = false; var lastViewed = Number.MAX_VALUE; if (ChannelStore.getMember(this.props.channelId) != null) { lastViewed = ChannelStore.getMember(this.props.channelId).last_viewed_at; } var numToDisplay = this.state.numToDisplay; if (order.length - 1 < numToDisplay) { numToDisplay = order.length - 1; } for (var i = numToDisplay; i >= 0; i--) { var post = posts[order[i]]; var parentPost = posts[post.parent_id]; var sameUser = false; var sameRoot = false; var hideProfilePic = false; var prevPost = posts[order[i + 1]]; if (prevPost) { sameUser = prevPost.user_id === post.user_id && post.create_at - prevPost.create_at <= 1000 * 60 * 5; 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 hideProfilePic = (prevPost.user_id === post.user_id) && !utils.isComment(prevPost) && !utils.isComment(post); } // 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 postCtl = ( ); let currentPostDay = utils.getDateForUnixTicks(post.create_at); if (currentPostDay.toDateString() !== previousPostDay.toDateString()) { postCtls.push(

{currentPostDay.toDateString()}
); } if (post.user_id !== userId && post.create_at > lastViewed && !renderedLastViewed) { renderedLastViewed = true; // Temporary fix to solve ie10/11 rendering issue let newSeparatorId = ''; if (!utils.isBrowserIE()) { newSeparatorId = 'new_message_' + this.props.channelId; } postCtls.push(

New Messages
); } postCtls.push(postCtl); previousPostDay = currentPostDay; } return postCtls; } loadMorePosts() { if (this.state.postList == null) { return; } var posts = this.state.postList.posts; var order = this.state.postList.order; var channelId = this.props.channelId; $(React.findDOMNode(this.refs.loadmore)).text('Retrieving more messages...'); Client.getPostsPage( channelId, order.length, Constants.POST_CHUNK_SIZE, function success(data) { $(React.findDOMNode(this.refs.loadmore)).text('Load more messages'); this.gotMorePosts = true; this.setState({numToDisplay: this.state.numToDisplay + Constants.POST_CHUNK_SIZE}); if (!data) { return; } if (data.order.length === 0) { return; } var postList = {}; postList.posts = $.extend(posts, data.posts); postList.order = order.concat(data.order); AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_POSTS, id: channelId, post_list: postList }); Client.getProfiles(); }.bind(this), function fail(err) { $(React.findDOMNode(this.refs.loadmore)).text('Load more messages'); AsyncClient.dispatchError(err, 'getPosts'); }.bind(this) ); } render() { var order = []; var posts; var channel = ChannelStore.get(this.props.channelId); if (this.state.postList != null) { posts = this.state.postList.posts; order = this.state.postList.order; } var moreMessages =

Beginning of Channel

; if (channel != null) { if (order.length >= this.state.numToDisplay) { moreMessages = ( Load more messages ); } else { moreMessages = this.createChannelIntroMessage(channel); } } var postCtls = []; if (posts && this.state.isFirstLoadComplete) { postCtls = this.createPosts(posts, order); } else { postCtls.push( ); } var activeClass = ''; if (!this.props.isActive) { activeClass = 'inactive'; } return (
{moreMessages} {postCtls}
); } } PostList.defaultProps = { isActive: false, channelId: null }; PostList.propTypes = { isActive: React.PropTypes.bool, channelId: React.PropTypes.string };