diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index 96c09e38e..59cb7cc65 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; import {refreshChannelWithRetry} from 'app/actions/views/channel'; -import {makePreparePostIdsForPostList} from 'app/selectors/post_list'; +import {makePreparePostIdsForPostList, START_OF_NEW_MESSAGES} from 'app/selectors/post_list'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; @@ -13,10 +13,16 @@ import PostList from './post_list'; function makeMapStateToProps() { const preparePostIds = makePreparePostIdsForPostList(); - return (state, ownProps) => { + const postIds = preparePostIds(state, ownProps); + const measureCellLayout = postIds.indexOf(START_OF_NEW_MESSAGES) > -1 || Boolean(ownProps.highlightPostId); + + const {deviceHeight} = state.device.dimension; + return { - postIds: preparePostIds(state, ownProps), + deviceHeight, + measureCellLayout, + postIds, theme: getTheme(state) }; }; diff --git a/app/components/post_list/new_messages_divider.js b/app/components/post_list/new_messages_divider.js index 4725b2f28..204a25869 100644 --- a/app/components/post_list/new_messages_divider.js +++ b/app/components/post_list/new_messages_divider.js @@ -14,15 +14,29 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; function NewMessagesDivider(props) { const style = getStyleFromTheme(props.theme); + let text = ( + + ); + + if (props.moreMessages) { + text = ( + + ); + } + return ( - + {text} @@ -30,6 +44,7 @@ function NewMessagesDivider(props) { } NewMessagesDivider.propTypes = { + moreMessages: PropTypes.bool, style: ViewPropTypes.style, theme: PropTypes.object }; diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index d095ee57e..17718e3e7 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -19,6 +19,13 @@ import {makeExtraData} from 'app/utils/list_view'; import DateHeader from './date_header'; import LoadMorePosts from './load_more_posts'; import NewMessagesDivider from './new_messages_divider'; +import withLayout from './with_layout'; + +const DateHeaderWithLayout = withLayout(DateHeader); +const NewMessagesDividerWithLayout = withLayout(NewMessagesDivider); +const PostWithLayout = withLayout(Post); + +const INITAL_BATCH_TO_RENDER = 15; export default class PostList extends PureComponent { static propTypes = { @@ -27,11 +34,13 @@ export default class PostList extends PureComponent { }).isRequired, channelId: PropTypes.string, currentUserId: PropTypes.string, + deviceHeight: PropTypes.number.isRequired, highlightPostId: PropTypes.string, indicateNewMessages: PropTypes.bool, isSearchResult: PropTypes.bool, lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types loadMore: PropTypes.func, + measureCellLayout: PropTypes.bool, navigator: PropTypes.object, onPostPress: PropTypes.func, onRefresh: PropTypes.func, @@ -49,9 +58,11 @@ export default class PostList extends PureComponent { newMessagesIndex = -1; scrollToMessageTries = 0; makeExtraData = makeExtraData(); + itemMeasurements = {}; state = { - managedConfig: {} + managedConfig: {}, + scrollToMessage: false }; componentWillMount() { @@ -60,20 +71,24 @@ export default class PostList extends PureComponent { componentDidMount() { this.setManagedConfig(); - this.scrollList(); } componentWillReceiveProps(nextProps) { if (nextProps.postIds !== this.props.postIds) { this.newMessagesIndex = -1; } + if (this.props.channelId !== nextProps.channelId) { + this.itemMeasurements = {}; + this.newMessageScrolledTo = false; + } } componentDidUpdate(prevProps) { const initialPosts = !prevProps.postIds.length && prevProps.postIds !== this.props.postIds; - if ((prevProps.channelId !== this.props.channelId || initialPosts || this.props.isSearchResult) && this.refs.list) { - this.scrollToMessageTries = 0; - this.scrollList(); + if ((prevProps.channelId !== this.props.channelId || initialPosts) && this.refs.list) { + this.scrollToBottomOffset(); + } else if ((this.props.measureCellLayout || this.props.isSearchResult) && this.state.scrollToMessage) { + this.scrollListToMessageOffset(); } } @@ -81,30 +96,47 @@ export default class PostList extends PureComponent { mattermostManaged.removeEventListener(this.listenerId); } - scrollList = () => { + scrollToBottomOffset = () => { InteractionManager.runAfterInteractions(() => { - if (this.props.postIds.length && this.newMessagesIndex !== -1) { - if (this.refs.list) { - this.refs.list.scrollToIndex({ - index: this.newMessagesIndex, - viewPosition: 1, - viewOffset: -10, - animated: true - }); - } - } else if (this.refs.list) { - this.refs.list.scrollToOffset({y: 0, animated: false}); + if (this.refs.list) { + this.refs.list.scrollToOffset({offset: 0, animated: false}); } }); - }; + } - scrollListFailed = ({index}) => { - if (this.scrollToMessageTries < 3) { - this.scrollToMessageTries++; - setTimeout(() => { - this.newMessagesIndex = index; - this.scrollList(); - }, 300); + getMeasurementOffset = (index) => { + const orderedKeys = Object.keys(this.itemMeasurements).sort().slice(0, index); + return orderedKeys.map((i) => this.itemMeasurements[i]).reduce((a, b) => a + b, 0); + } + + scrollListToMessageOffset = () => { + const index = this.moreNewMessages ? this.props.postIds.length : this.newMessagesIndex; + if (index !== -1) { + let offset = this.getMeasurementOffset(index); + + const windowHeight = this.state.postListHeight; + if (index !== this.props.postIds.length - 1) { + if (offset < windowHeight) { + return; // no need to scroll since item is in view + } else if (offset > windowHeight) { + offset = (offset - (windowHeight / 2)) + this.itemMeasurements[index]; + } + } + + InteractionManager.runAfterInteractions(() => { + if (this.refs.list) { + if (!this.moreNewMessages) { + this.newMessageScrolledTo = true; + } + + this.refs.list.scrollToOffset({offset, animated: true}); + this.newMessagesIndex = -1; + this.moreNewMessages = false; + this.setState({ + scrollToMessage: false + }); + } + }); } } @@ -140,17 +172,37 @@ export default class PostList extends PureComponent { } }; + measureItem = (index, height) => { + this.itemMeasurements[index] = height; + if (this.props.postIds.length === Object.values(this.itemMeasurements).length) { + if (this.newMessagesIndex !== -1 && !this.newMessageScrolledTo) { + this.setState({ + scrollToMessage: true + }); + } + } + } + renderItem = ({item, index}) => { if (item === START_OF_NEW_MESSAGES) { this.newMessagesIndex = index; + + // postIds includes a date item after the new message indicator so 2 + // needs to be added to the index for the length check to be correct. + this.moreNewMessages = this.props.postIds.length === index + 2; + return ( - ); } else if (item.indexOf(DATE_LINE) === 0) { const date = item.substring(DATE_LINE.length); - return this.renderDateHeader(new Date(date)); + return this.renderDateHeader(new Date(date), index); } const postId = item; @@ -163,9 +215,14 @@ export default class PostList extends PureComponent { return this.renderPost(postId, previousPostId, nextPostId, index); }; - renderDateHeader = (date) => { + renderDateHeader = (date, index) => { return ( - + ); }; @@ -186,17 +243,20 @@ export default class PostList extends PureComponent { } return ( - ); }; @@ -223,6 +283,13 @@ export default class PostList extends PureComponent { ); }; + onLayout = (event) => { + const {height} = event.nativeEvent.layout; + this.setState({ + postListHeight: height + }); + } + render() { const { channelId, @@ -241,16 +308,17 @@ export default class PostList extends PureComponent { return ( { + const {height} = event.nativeEvent.layout; + this.props.onLayoutCalled(this.props.index, height); + }; + + render() { + const {index, onLayoutCalled, shouldCallOnLayout, ...otherProps} = this.props; //eslint-disable-line no-unused-vars + + if (shouldCallOnLayout) { + return ( + + + + ); + } + + return ; + } + }; +} + +export default withLayout; diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index d5ba2d273..a9d76f65d 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -2098,6 +2098,7 @@ "mobile.post_textbox.empty.message": "You are trying to send an empty message.\nPlease make sure you have a message or at least one attached file.", "mobile.post_textbox.empty.ok": "OK", "mobile.post_textbox.empty.title": "Empty Message", + "mobile.posts_view.moreMsg": "More New Messages Above", "mobile.rename_channel.display_name_maxLength": "Channel name must be less than {maxLength, number} characters", "mobile.rename_channel.display_name_minLength": "Channel name must be {minLength, number} or more characters", "mobile.rename_channel.display_name_required": "Channel name is required",