// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // See License.txt for license information. const Post = require('./post.jsx'); const UserProfile = require('./user_profile.jsx'); const AsyncClient = require('../utils/async_client.jsx'); const LoadingScreen = require('./loading_screen.jsx'); const PostStore = require('../stores/post_store.jsx'); const ChannelStore = require('../stores/channel_store.jsx'); const UserStore = require('../stores/user_store.jsx'); const SocketStore = require('../stores/socket_store.jsx'); const PreferenceStore = require('../stores/preference_store.jsx'); const Utils = require('../utils/utils.jsx'); const Client = require('../utils/client.jsx'); const Constants = require('../utils/constants.jsx'); const ActionTypes = Constants.ActionTypes; const SocketEvents = Constants.SocketEvents; const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); 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.handleResize = this.handleResize.bind(this); this.resizePostList = this.resizePostList.bind(this); const state = this.getStateFromStores(props.channelId); state.numToDisplay = Constants.POST_CHUNK_SIZE; state.isFirstLoadComplete = false; state.windowHeight = Utils.windowHeight(); this.state = state; } 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((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 }; } 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); PreferenceStore.addChangeListener(this.onTimeChange); SocketStore.addChangeListener(this.onSocketChange); const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); window.addEventListener('resize', this.handleResize); postHolder.on('scroll', () => { const position = postHolder.scrollTop() + postHolder.height() + 14; const bottom = postHolder[0].scrollHeight; if (position >= bottom) { this.scrolled = false; } else { this.scrolled = true; } if (this.isUserScroll) { this.userHasSeenNew = true; } this.isUserScroll = true; $('.top-visible-post').removeClass('top-visible-post'); $(ReactDOM.findDOMNode(this.refs.postlistcontent)).children().each(function select() { if ($(this).position().top + $(this).height() / 2 > 0) { $(this).addClass('top-visible-post'); return false; } }); }); $('.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.handleResize(); this.onChange(); this.scrollToBottom(); } deactivate() { PostStore.removeChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onTimeChange); SocketStore.removeChangeListener(this.onSocketChange); PreferenceStore.removeChangeListener(this.onTimeChange); $('body').off('click.userpopover'); window.removeEventListener('resize', this.handleResize); var postHolder = $(ReactDOM.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); } if (prevState.windowHeight !== this.state.windowHeight) { this.handleResize(); } } componentWillUpdate() { var postHolder = $(ReactDOM.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(); } } handleResize() { this.setState({ windowHeight: Utils.windowHeight() }); this.resizePostList(); if (!this.scrolled) { this.scrollToBottom(); } } resizePostList() { const postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); if ($('#create_post').length > 0) { const height = this.state.windowHeight - $('#create_post').height() - $('#error_bar').outerHeight() - 50; postHolder.css('height', height + 'px'); } } scrollTo(val) { this.isUserScroll = false; var postHolder = $(ReactDOM.findDOMNode(this.refs.postlist)); postHolder[0].scrollTop = val; } scrollToBottom(force) { this.isUserScroll = false; var postHolder = $(ReactDOM.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), () => { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); }, () => { this.loadInProgress = false; this.setState({isFirstLoadComplete: true}); } ); } onChange() { var newState = this.getStateFromStores(this.props.channelId); if (!Utils.areStatesEqual(newState.postList, this.state.postList)) { this.setState(newState); } } onSocketChange(msg) { if (msg.action === SocketEvents.POST_DELETED) { var activeRoot = $(document.activeElement).closest('.comment-create-body')[0]; var activeRootPostId = ''; if (activeRoot && activeRoot.id.length > 0) { activeRootPostId = activeRoot.id; } if (activeRootPostId === msg.props.post_id && UserStore.getCurrentId() !== msg.user_id) { $('#post_deleted').modal('show'); } } } 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.'}
{'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.'}
{'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 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(