// 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() { super(); this.gotMorePosts = false; this.scrolled = false; this.prevScrollTop = 0; this.seenNewMessages = false; this.isUserScroll = true; this.userHasSeenNew = 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.state = this.getStateFromStores(); this.state.numToDisplay = Constants.POST_CHUNK_SIZE; this.state.isFirstLoadComplete = false; } getStateFromStores() { var channel = ChannelStore.getCurrent(); if (channel == null) { channel = {}; } var postList = PostStore.getCurrentPosts(); if (postList != null) { var deletedPosts = PostStore.getUnseenDeletedPosts(channel.id); if (deletedPosts && Object.keys(deletedPosts).length > 0) { for (var pid in deletedPosts) { 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(channel.id); if (pendingPostList) { postList.order = pendingPostList.order.concat(postList.order); for (var ppid in pendingPostList.posts) { postList.posts[ppid] = pendingPostList.posts[ppid]; } } } var lastViewed = Number.MAX_VALUE; if (ChannelStore.getCurrentMember() != null) { lastViewed = ChannelStore.getCurrentMember().last_viewed_at; } return { postList: postList, channel: channel, lastViewed: lastViewed }; } componentDidMount() { PostStore.addChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onTimeChange); SocketStore.addChangeListener(this.onSocketChange); var postHolder = $('.post-list-holder-by-time'); $('.modal').on('show.bs.modal', function onShow() { $('.modal-body').css('overflow-y', 'auto'); $('.modal-body').css('max-height', $(window).height() * 0.7); }); $(window).resize(function resize() { if ($('#create_post').length > 0) { var height = $(window).height() - $('#create_post').height() - $('#error_bar').outerHeight() - 50; postHolder.css('height', height + 'px'); } if (!this.scrolled) { this.scrollToBottom(); } }.bind(this)); postHolder.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)); $('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'); } }); $('.post-list__content div .post').removeClass('post--last'); $('.post-list__content div:last-child .post').addClass('post--last'); $('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 { $(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 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 { $(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment'); $(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment'); } }); this.scrollToBottom(); if (this.state.channel.id != null) { this.loadFirstPosts(this.state.channel.id); } } componentDidUpdate(prevProps, prevState) { $('.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.state.channel.id !== prevState.channel.id) { 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.state.lastViewed = utils.getTimestamp(); this.scrollToBottom(true); // the user clicked 'load more messages' } else if (this.gotMorePosts) { var lastPost = oldPosts[oldOrder[prevState.numToDisplay]]; $('#' + lastPost.id)[0].scrollIntoView(); } else { this.scrollTo(this.prevScrollTop); } } componentWillUpdate() { var postHolder = $('.post-list-holder-by-time'); this.prevScrollTop = postHolder.scrollTop(); } componentWillUnmount() { 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'); } scrollTo(val) { this.isUserScroll = false; var postHolder = $('.post-list-holder-by-time'); postHolder[0].scrollTop = val; } scrollToBottom(force) { this.isUserScroll = false; var postHolder = $('.post-list-holder-by-time'); if ($('#new_message')[0] && !this.userHasSeenNew && !force) { $('#new_message')[0].scrollIntoView(); } else { postHolder.addClass('hide-scroll'); postHolder[0].scrollTop = postHolder[0].scrollHeight; postHolder.removeClass('hide-scroll'); } } loadFirstPosts(id) { Client.getPosts( id, PostStore.getLatestUpdate(id), function success() { this.setState({isFirstLoadComplete: true}); }.bind(this), function fail() { this.setState({isFirstLoadComplete: true}); }.bind(this) ); } onChange() { var newState = this.getStateFromStores(); // Special case where the channel wasn't yet set in componentDidMount if (!this.state.isFirstLoadComplete && this.state.channel.id == null && newState.channel.id != null) { this.loadFirstPosts(newState.channel.id); } if (!utils.areStatesEqual(newState, this.state)) { if (this.state.channel.id !== newState.channel.id) { PostStore.clearUnseenDeletedPosts(this.state.channel.id); this.userHasSeenNew = false; newState.numToDisplay = Constants.POST_CHUNK_SIZE; } else { newState.lastViewed = this.state.lastViewed; } this.setState(newState); } } onSocketChange(msg) { var postList; 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); postList = this.state.postList; 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 private message history with ' + teammateName + '.'}
{'Private messages and files shared here are not shown to people outside this area.'}
{'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.'}
Welcome to {channel.display_name}!
This is the first channel {strings.Team}mates 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…”.
{'This is the start of ' + channel.display_name + ', a channel for non-work-related conversations.'}
{createMessage}
{memberMessage}
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(