diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index e51602f8a..5280d9b5e 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -12,12 +12,12 @@ import { selectChannel, leaveChannel as serviceLeaveChannel } from 'mattermost-redux/actions/channels'; -import {getPosts, getPostsSince} from 'mattermost-redux/actions/posts'; +import {getPosts, getPostsBefore, getPostsSince} from 'mattermost-redux/actions/posts'; import {getFilesForPost} from 'mattermost-redux/actions/files'; import {savePreferences, deletePreferences} from 'mattermost-redux/actions/preferences'; import {getTeamMembersByIds} from 'mattermost-redux/actions/teams'; import {getProfilesInChannel} from 'mattermost-redux/actions/users'; -import {General, Posts, Preferences} from 'mattermost-redux/constants'; +import {General, Preferences} from 'mattermost-redux/constants'; import { getChannelByName, getDirectChannelName, @@ -136,22 +136,22 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) { }; } -export function loadPostsIfNecessary(channel) { +export function loadPostsIfNecessary(channelId) { return async (dispatch, getState) => { const state = getState(); const {posts, postsInChannel} = state.entities.posts; - const postsIds = postsInChannel[channel.id]; + const postsIds = postsInChannel[channelId]; // Get the first page of posts if it appears we haven't gotten it yet, like the webapp - if (!postsIds || postsIds.length < Posts.POST_CHUNK_SIZE) { - return getPosts(channel.id)(dispatch, getState); + if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) { + return getPosts(channelId)(dispatch, getState); } const postsForChannel = postsIds.map((id) => posts[id]); const latestPostTime = getLastCreateAt(postsForChannel); - return getPostsSince(channel.id, latestPostTime)(dispatch, getState); + return getPostsSince(channelId, latestPostTime)(dispatch, getState); }; } @@ -314,6 +314,14 @@ export function unmarkFavorite(channelId) { }; } +export function refreshChannel(channelId) { + return async (dispatch, getState) => { + dispatch(setChannelRefreshing()); + await getPosts(channelId)(dispatch, getState); + dispatch(setChannelRefreshing(false)); + }; +} + export function leaveChannel(channel, reset = false) { return async (dispatch, getState) => { const {currentTeamId} = getState().entities.teams; @@ -351,3 +359,51 @@ export function setChannelDisplayName(displayName) { displayName }; } + +// Returns true if there are more posts to load +export function increasePostVisibility(channelId, focusedPostId) { + return async (dispatch, getState) => { + const state = getState(); + const {loadingPosts, postVisibility} = state.views.channel; + const currentPostVisibility = postVisibility[channelId] || 0; + + if (loadingPosts[channelId]) { + return true; + } + + dispatch(batchActions([ + { + type: ViewTypes.LOADING_POSTS, + data: true, + channelId + } + ])); + + const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE); + + let posts; + if (focusedPostId) { + posts = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState); + } else { + posts = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState); + } + + if (posts) { + // make sure to increment the posts visibility + // only if we got results + dispatch({ + type: ViewTypes.INCREASE_POST_VISIBILITY, + data: channelId, + amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE + }); + } + + dispatch({ + type: ViewTypes.LOADING_POSTS, + data: false, + channelId + }); + + return posts && posts.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE; + }; +} diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index 1153eeb27..bedc5a1bf 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -86,13 +86,17 @@ export default class ChannelDrawer extends PureComponent { handleDrawerClose = () => { this.resetDrawer(); - setTimeout(() => { + + if (this.closeLeftHandle) { InteractionManager.clearInteractionHandle(this.closeLeftHandle); - }); + this.closeLeftHandle = null; + } }; handleDrawerCloseStart = () => { - this.closeLeftHandle = InteractionManager.createInteractionHandle(); + if (!this.closeLeftHandle) { + this.closeLeftHandle = InteractionManager.createInteractionHandle(); + } }; handleDrawerOpen = () => { @@ -100,13 +104,17 @@ export default class ChannelDrawer extends PureComponent { if (this.state.openDrawerOffset === DRAWER_INITIAL_OFFSET) { Keyboard.dismiss(); } - setTimeout(() => { + + if (this.openLeftHandle) { InteractionManager.clearInteractionHandle(this.openLeftHandle); - }); + this.openLeftHandle = null; + } }; handleDrawerOpenStart = () => { - this.openLeftHandle = InteractionManager.createInteractionHandle(); + if (!this.openLeftHandle) { + this.openLeftHandle = InteractionManager.createInteractionHandle(); + } }; handleDrawerTween = (ratio) => { diff --git a/app/components/inverted_flat_list/index.js b/app/components/inverted_flat_list/index.js new file mode 100644 index 000000000..235d569f4 --- /dev/null +++ b/app/components/inverted_flat_list/index.js @@ -0,0 +1,139 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import {FlatList, RefreshControl, ScrollView, StyleSheet, View} from 'react-native'; + +import VirtualList from './virtual_list'; + +export default class InvertibleFlatList extends PureComponent { + static propTypes = { + horizontal: PropTypes.bool, + inverted: PropTypes.bool, + ListFooterComponent: PropTypes.func, + renderItem: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + }; + + static defaultProps = { + horizontal: false, + inverted: true + }; + + constructor(props) { + super(props); + + this.inversionDirection = props.horizontal ? styles.horizontal : styles.vertical; + } + + getMetrics = () => { + return this.flatListRef.getMetrics(); + }; + + recordInteraction = () => { + this.flatListRef.recordInteraction(); + }; + + renderFooter = () => { + const {ListFooterComponent: footer} = this.props; + if (!footer) { + return null; + } + + return ( + + {footer()} + + ); + }; + + renderItem = (info) => { + return ( + + {this.props.renderItem(info)} + + ); + }; + + renderScrollComponent = (props) => { + const {theme} = this.props; + + if (props.onRefresh) { + return ( + + } + /> + ); + } + + return ; + }; + + scrollToEnd = (params) => { + this.flatListRef.scrollToEnd(params); + }; + + scrollToIndex = (params) => { + this.flatListRef.scrollToIndex(params); + }; + + scrollToItem = (params) => { + this.flatListRef.scrollToItem(params); + }; + + scrollToOffset = (params) => { + this.flatListRef.scrollToOffset(params); + }; + + setFlatListRef = (flatListRef) => { + this.flatListRef = flatListRef; + }; + + render() { + const {inverted, ...forwardedProps} = this.props; + + // If not inverted, render as an ordinary FlatList + if (!inverted) { + return ( + + ); + } + + return ( + + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1 + }, + vertical: { + transform: [{scaleY: -1}] + }, + horizontal: { + transform: [{scaleX: -1}] + } +}); + diff --git a/app/components/inverted_flat_list/virtual_list.js b/app/components/inverted_flat_list/virtual_list.js new file mode 100644 index 000000000..5bb05424b --- /dev/null +++ b/app/components/inverted_flat_list/virtual_list.js @@ -0,0 +1,54 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {VirtualizedList} from 'react-native'; + +/* eslint-disable */ + +export default class Virtualized extends VirtualizedList { + _onScroll = (e) => { + if (this.props.onScroll) { + this.props.onScroll(e); + } + const timestamp = e.timeStamp; + const visibleLength = this._selectLength(e.nativeEvent.layoutMeasurement); + const contentLength = this._selectLength(e.nativeEvent.contentSize); + const offset = this._selectOffset(e.nativeEvent.contentOffset); + const dt = Math.max(1, timestamp - this._scrollMetrics.timestamp); + const dOffset = offset - this._scrollMetrics.offset; + const velocity = dOffset / dt; + this._scrollMetrics = {contentLength, dt, offset, timestamp, velocity, visibleLength}; + const {data, getItemCount, onEndReached, onEndReachedThreshold, windowSize} = this.props; + this._updateViewableItems(data); + if (!data) { + return; + } + const distanceFromEnd = contentLength - visibleLength - offset; + const itemCount = getItemCount(data); + if (this.state.last === itemCount - 1 && + distanceFromEnd <= onEndReachedThreshold && + (this._hasDataChangedSinceEndReached || + this._scrollMetrics.contentLength !== this._sentEndForContentLength)) { + // Only call onEndReached once for a given dataset + content length. + this._hasDataChangedSinceEndReached = false; + this._sentEndForContentLength = this._scrollMetrics.contentLength; + onEndReached({distanceFromEnd}); + } + const {first, last} = this.state; + if ((first > 0 && velocity < 0) || (last < itemCount - 1 && velocity > 0)) { + const distanceToContentEdge = Math.min( + Math.abs(this._getFrameMetricsApprox(first).offset - offset), + Math.abs(this._getFrameMetricsApprox(last).offset - (offset + visibleLength)), + ); + const hiPri = distanceToContentEdge < (windowSize * visibleLength / 4); + if (hiPri) { + // Don't worry about interactions when scrolling quickly; focus on filling content as fast + // as possible. + this._updateCellsToRenderBatcher.dispose({abort: true}); + this._updateCellsToRender(); + return; + } + } + this._updateCellsToRenderBatcher.schedule(); + }; +} diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index fdd6d39d0..df1c1b515 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -4,11 +4,9 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {setChannelRefreshing} from 'app/actions/views/channel'; +import {refreshChannel} from 'app/actions/views/channel'; import {getTheme} from 'app/selectors/preferences'; -import {getPosts} from 'mattermost-redux/actions/posts'; - import PostList from './post_list'; function mapStateToProps(state, ownProps) { @@ -25,8 +23,7 @@ function mapStateToProps(state, ownProps) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ - getPosts, - setChannelRefreshing + refreshChannel }, dispatch) }; } diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 0992ae6d8..143d947f9 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -1,16 +1,14 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; +import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import { - ListView, - RefreshControl, View } from 'react-native'; -import InvertibleScrollView from 'react-native-invertible-scroll-view'; +import FlatList from 'app/components/inverted_flat_list'; -import {General, Posts} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import {addDatesToPostList} from 'mattermost-redux/utils/post_utils'; import ChannelIntro from 'app/components/channel_intro'; @@ -21,72 +19,56 @@ import NewMessagesDivider from './new_messages_divider'; const LOAD_MORE_POSTS = 'load-more-posts'; -export default class PostList extends Component { +export default class PostList extends PureComponent { static propTypes = { actions: PropTypes.shape({ - getPosts: PropTypes.func.isRequired, - setChannelRefreshing: PropTypes.func.isRequired + refreshChannel: PropTypes.func.isRequired }).isRequired, channel: PropTypes.object, channelIsLoading: PropTypes.bool.isRequired, - posts: PropTypes.array.isRequired, - theme: PropTypes.object.isRequired, + currentUserId: PropTypes.string, + indicateNewMessages: PropTypes.bool, + isLoadingMore: PropTypes.bool, + lastViewedAt: PropTypes.number, loadMore: PropTypes.func, navigator: PropTypes.object, - isLoadingMore: PropTypes.bool, - showLoadMore: PropTypes.bool, onPostPress: PropTypes.func, + posts: PropTypes.array.isRequired, refreshing: PropTypes.bool, renderReplies: PropTypes.bool, - indicateNewMessages: PropTypes.bool, - currentUserId: PropTypes.string, - lastViewedAt: PropTypes.number + showLoadMore: PropTypes.bool, + theme: PropTypes.object.isRequired }; static defaultProps = { channel: {} }; - constructor(props) { - super(props); - this.state = { - posts: this.getPostsWithDates(props), - dataSource: new ListView.DataSource({ - rowHasChanged: (a, b) => a !== b - }) - }; - } + getPostsWithDates = () => { + const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props; + const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt}); - componentWillReceiveProps(nextProps) { - if (nextProps.posts !== this.props.posts) { - const posts = this.getPostsWithDates(nextProps); - this.setState({posts}, () => { - if (nextProps.refreshing) { - this.props.actions.setChannelRefreshing(false); - } - }); - } else if (nextProps.refreshing) { - this.props.actions.setChannelRefreshing(false); - } - } - - getPostsWithDates(props) { - const {posts, indicateNewMessages, currentUserId, lastViewedAt} = props; - return addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt}); - } - - getPostsWithLoadMore() { - const {showLoadMore} = this.props; - const {posts} = this.state; if (showLoadMore) { - return [...posts, LOAD_MORE_POSTS]; + return [...list, LOAD_MORE_POSTS]; } - return posts; - } - loadMore = () => { - const {loadMore} = this.props; - if (typeof loadMore === 'function') { + return list; + }; + + keyExtractor = (item) => { + if (item instanceof Date) { + return item.getTime(); + } + if (item === General.START_OF_NEW_MESSAGES || item === LOAD_MORE_POSTS) { + return item; + } + + return item.id; + }; + + loadMorePosts = () => { + const {loadMore, isLoadingMore} = this.props; + if (typeof loadMore === 'function' && !isLoadingMore) { loadMore(); } }; @@ -95,18 +77,18 @@ export default class PostList extends Component { const {actions, channel} = this.props; if (Object.keys(channel).length) { - actions.setChannelRefreshing(true); - actions.getPosts(channel.id); + actions.refreshChannel(channel.id); } }; renderChannelIntro = () => { const {channel, channelIsLoading, navigator, posts} = this.props; + // Check the webapp for atEnd, and replace it here if (channel.hasOwnProperty('id')) { const firstPostHasRendered = channel.total_msg_count ? posts.length > 0 : true; const messageCount = channel.total_msg_count - posts.length; - if (channelIsLoading || !firstPostHasRendered || messageCount > Posts.POST_CHUNK_SIZE) { + if (channelIsLoading || !firstPostHasRendered || messageCount > 0) { return null; } @@ -120,18 +102,27 @@ export default class PostList extends Component { return null; }; - renderRow = (row) => { - if (row instanceof Date) { - return this.renderDateHeader(row); + renderDateHeader = (date) => { + return ( + + ); + }; + + renderItem = ({item}) => { + if (item instanceof Date) { + return this.renderDateHeader(item); } - if (row === General.START_OF_NEW_MESSAGES) { + if (item === General.START_OF_NEW_MESSAGES) { return ( ); } - if (row === LOAD_MORE_POSTS) { + if (item === LOAD_MORE_POSTS) { return ( { - return ( - - ); + return this.renderPost(item); }; renderPost = (post) => { @@ -166,44 +148,29 @@ export default class PostList extends Component { ); }; - renderRefreshControl = () => { - const {theme, refreshing} = this.props; - - return ( - - ); - }; - - renderScrollComponent = (props) => { - return ( - - ); - }; - render() { - const {dataSource} = this.state; + const {channel, refreshing, theme} = this.props; + + const refreshControl = { + refreshing + }; + + if (Object.keys(channel).length) { + refreshControl.onRefresh = this.onRefresh; + } return ( - ); } diff --git a/app/constants/view.js b/app/constants/view.js index dd299919d..a9fd2f025 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -1,6 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. +import {Posts} from 'mattermost-redux/constants'; import keyMirror from 'mattermost-redux/utils/key_mirror'; const ViewTypes = keyMirror({ @@ -42,7 +43,14 @@ const ViewTypes = keyMirror({ SET_LAST_CHANNEL_FOR_TEAM: null, GITLAB: null, - SAML: null + SAML: null, + + INCREASE_POST_VISIBILITY: null, + RECEIVED_FOCUSED_POST: null, + LOADING_POSTS: null }); -export default ViewTypes; +export default { + ...ViewTypes, + POST_VISIBILITY_CHUNK_SIZE: Posts.POST_CHUNK_SIZE / 2 +}; diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index 9e195ccda..a6e586bb1 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -2,7 +2,7 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {ChannelTypes, FileTypes} from 'mattermost-redux/action_types'; +import {ChannelTypes, FileTypes, PostTypes} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; @@ -199,10 +199,54 @@ function tooltipVisible(state = false, action) { } } +function postVisibility(state = {}, action) { + switch (action.type) { + case ChannelTypes.SELECT_CHANNEL: { + const nextState = {...state}; + nextState[action.data] = ViewTypes.POST_VISIBILITY_CHUNK_SIZE; + return nextState; + } + case ViewTypes.INCREASE_POST_VISIBILITY: { + const nextState = {...state}; + nextState[action.data] += action.amount; + return nextState; + } + case ViewTypes.RECEIVED_FOCUSED_POST: { + const nextState = {...state}; + nextState[action.channelId] = ViewTypes.POST_VISIBILITY_CHUNK_SIZE; + return nextState; + } + case PostTypes.RECEIVED_POST: { + if (action.data && state[action.data.channel_id]) { + const nextState = {...state}; + nextState[action.data.channel_id] += 1; + return nextState; + } + return state; + } + default: + return state; + } +} + +function loadingPosts(state = {}, action) { + switch (action.type) { + case ViewTypes.LOADING_POSTS: { + const nextState = {...state}; + nextState[action.channelId] = action.data; + return nextState; + } + default: + return state; + } +} + export default combineReducers({ displayName, drafts, loading, refreshing, - tooltipVisible + tooltipVisible, + postVisibility, + loadingPosts }); diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index d3cbc4c02..c5ca16cd8 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -26,7 +26,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDrawerButton from './channel_drawer_button'; import ChannelTitle from './channel_title'; -import ChannelPostList from './channel_post_list/index'; +import ChannelPostList from './channel_post_list'; class Channel extends PureComponent { static propTypes = { diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index a65b10d08..19e88bc64 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -11,7 +11,7 @@ import { View } from 'react-native'; -import {General, Posts, RequestStatus} from 'mattermost-redux/constants'; +import {General} from 'mattermost-redux/constants'; import ChannelLoader from 'app/components/channel_loader'; import PostList from 'app/components/post_list'; @@ -24,90 +24,59 @@ class ChannelPostList extends PureComponent { static propTypes = { actions: PropTypes.shape({ loadPostsIfNecessary: PropTypes.func.isRequired, - getPostsBefore: PropTypes.func.isRequired, + increasePostVisibility: PropTypes.func.isRequired, selectPost: PropTypes.func.isRequired }).isRequired, - applicationInitializing: PropTypes.bool.isRequired, channel: PropTypes.object.isRequired, channelIsLoading: PropTypes.bool, channelIsRefreshing: PropTypes.bool, + channelName: PropTypes.string, intl: intlShape.isRequired, + loadingPosts: PropTypes.bool, myMember: PropTypes.object.isRequired, navigator: PropTypes.object, - postsRequests: PropTypes.shape({ - getPosts: PropTypes.object.isRequired, - getPostsBefore: PropTypes.object.isRequired, - getPostsSince: PropTypes.object.isRequired - }).isRequired, posts: PropTypes.array.isRequired, + postVisibility: PropTypes.number, theme: PropTypes.object.isRequired, networkOnline: PropTypes.bool.isRequired }; - state = { - didInitialPostsLoad: false, - hasFirstPost: false, - lastViewedAt: this.props.myMember.last_viewed_at, - loaderOpacity: new Animated.Value(1) + static defaultProps = { + loadingPosts: false, + postVisibility: 0 }; + constructor(props) { + super(props); + + this.state = { + loaderOpacity: new Animated.Value(1) + }; + } + componentDidMount() { - this.props.actions.loadPostsIfNecessary(this.props.channel); + this.loadPosts(this.props.channel.id); } componentWillReceiveProps(nextProps) { - if (this.props.channel.id === nextProps.channel.id) { - const didInitialPostsLoad = this.didPostsLoad(nextProps, 'getPosts') || - this.didPostsLoad(nextProps, 'getPostsSince'); - if (didInitialPostsLoad) { - this.setState({didInitialPostsLoad}); - } - const didMorePostsLoad = this.didPostsLoad(nextProps, 'getPostsBefore'); - let hasFirstPost = false; - if (didInitialPostsLoad) { - hasFirstPost = nextProps.posts.length < Posts.POST_CHUNK_SIZE; - } else if (didMorePostsLoad) { - hasFirstPost = (nextProps.posts.length - this.props.posts.length) < General.POST_CHUNK_SIZE; - } - if (hasFirstPost) { - this.setState({hasFirstPost}); - } - if (!nextProps.applicationInitializing) { - this.loaderAnimationRunner(); - } - } else { + // Show the loader if the channel names change + if (this.props.channelName !== nextProps.channelName) { this.setState({ - didInitialPostsLoad: false, - hasFirstPost: false, - lastViewedAt: nextProps.myMember.last_viewed_at, loaderOpacity: new Animated.Value(1) }); - this.props.actions.loadPostsIfNecessary(nextProps.channel); + } + + if (this.props.channel.id !== nextProps.channel.id) { + // Load the posts when the channel actually changes + this.loadPosts(nextProps.channel.id); } } - loaderAnimationRunner = () => { + channelLoaded = () => { Animated.timing(this.state.loaderOpacity, { toValue: 0, duration: 500 - }).start(); - }; - - didPostsLoad(nextProps, postsRequest) { - const nextGetPostsStatus = nextProps.postsRequests[postsRequest].status; - const getPostsStatus = this.props.postsRequests[postsRequest].status; - return getPostsStatus === RequestStatus.STARTED && nextGetPostsStatus === RequestStatus.SUCCESS; - } - - loadMorePosts = () => { - const {channel, posts, postsRequests} = this.props; - const {id: channelId} = channel; - const oldestPost = posts[posts.length - 1]; - const {didInitialPostsLoad, hasFirstPost} = this.state; - if (didInitialPostsLoad && !hasFirstPost && oldestPost && postsRequests.getPostsBefore.status !== RequestStatus.STARTED) { - return this.props.actions.getPostsBefore(channelId, oldestPost.id); - } - return null; + }).start(() => this.setState({channelLoaded: true})); }; goToThread = (post) => { @@ -149,46 +118,60 @@ class ChannelPostList extends PureComponent { } }; + loadMorePosts = () => { + const {actions, channel} = this.props; + actions.increasePostVisibility(channel.id); + }; + + loadPosts = async (channelId) => { + this.setState({channelLoaded: false}); + await this.props.actions.loadPostsIfNecessary(channelId); + this.channelLoaded(); + }; + render() { const { - actions, - applicationInitializing, channel, channelIsLoading, channelIsRefreshing, + loadingPosts, + myMember, navigator, + networkOnline, posts, - postsRequests, - theme, - networkOnline + postVisibility, + theme } = this.props; + const {channelLoaded, loaderOpacity} = this.state; + let component; - if (!posts.length && channel.total_msg_count > 0 && (postsRequests.getPosts.status === RequestStatus.FAILURE || !networkOnline)) { + if (!posts.length && channel.total_msg_count > 0 && !networkOnline) { + // If no posts has been loaded and we are offline component = ( actions.loadPostsIfNecessary(channel)} + retry={() => this.loadPosts(channel.id)} theme={theme} /> ); - } else if (!applicationInitializing && !channelIsLoading && posts && (postsRequests.getPosts.status !== RequestStatus.STARTED || channelIsRefreshing || !this.state.didInitialPostsLoad)) { + } else if ((channelIsLoading || !channelLoaded) && !channelIsRefreshing && !loadingPosts) { + component = ; + } else { component = ( 0 && !this.state.hasFirstPost} + isLoadingMore={loadingPosts} + showLoadMore={(channel.total_msg_count - posts.length) > 0} onPostPress={this.goToThread} renderReplies={true} indicateNewMessages={true} - currentUserId={this.props.myMember.user_id} - lastViewedAt={this.state.lastViewedAt} + currentUserId={myMember.user_id} + lastViewedAt={myMember.last_viewed_at} channel={channel} navigator={navigator} /> ); - } else { - component = ; } return ( @@ -202,7 +185,7 @@ class ChannelPostList extends PureComponent { width: deviceWidth, top: 0, left: 0, - opacity: this.state.loaderOpacity + opacity: loaderOpacity }} > diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index 6424266d8..7e7cf4a66 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -4,31 +4,37 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {selectPost, getPostsBefore} from 'mattermost-redux/actions/posts'; +import {selectPost} from 'mattermost-redux/actions/posts'; +import {RequestStatus} from 'mattermost-redux/constants'; import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts'; import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; -import {loadPostsIfNecessary} from 'app/actions/views/channel'; +import {loadPostsIfNecessary, increasePostVisibility} from 'app/actions/views/channel'; import {getTheme} from 'app/selectors/preferences'; import ChannelPostList from './channel_post_list'; -const getPostsInCurrentChannelWithReplyProps = makeGetPostsInChannel(); +function makeMapStateToProps() { + const getPostsInChannel = makeGetPostsInChannel(); -function mapStateToProps(state, ownProps) { - const {loading, refreshing} = state.views.channel; - const {currentChannelId} = state.entities.channels; + return function mapStateToProps(state, ownProps) { + const channelId = ownProps.channel.id; + const {displayName, refreshing} = state.views.channel; + const {getPosts} = state.requests.posts; + const posts = getPostsInChannel(state, channelId) || []; - return { - ...ownProps, - applicationInitializing: state.views.root.appInitializing, - channelIsLoading: loading, - channelIsRefreshing: refreshing, - myMember: getMyCurrentChannelMembership(state), - postsRequests: state.requests.posts, - posts: getPostsInCurrentChannelWithReplyProps(state, currentChannelId) || [], - theme: getTheme(state), - networkOnline: state.offline.online + return { + channelIsLoading: (getPosts.status === RequestStatus.STARTED), + channelIsRefreshing: refreshing, + channelName: displayName, + posts, + postVisibility: state.views.channel.postVisibility[channelId], + loadingPosts: state.views.channel.loadingPosts[channelId], + myMember: getMyCurrentChannelMembership(state), + networkOnline: state.offline.online, + theme: getTheme(state), + ...ownProps + }; }; } @@ -36,10 +42,10 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ loadPostsIfNecessary, - getPostsBefore, + increasePostVisibility, selectPost }, dispatch) }; } -export default connect(mapStateToProps, mapDispatchToProps)(ChannelPostList); +export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelPostList); diff --git a/package.json b/package.json index 699a77614..b846c4f84 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "react-native-device-info": "0.10.2", "react-native-drawer": "2.3.0", "react-native-image-picker": "jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4", - "react-native-invertible-scroll-view": "1.0.0", "react-native-keyboard-aware-scroll-view": "0.2.8", "react-native-linear-gradient": "2.0.0", "react-native-navigation": "1.1.88", diff --git a/yarn.lock b/yarn.lock index 57ce48401..a9308c971 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4480,13 +4480,6 @@ react-native-image-picker@jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f58 version "0.26.2" resolved "https://codeload.github.com/jp928/react-native-image-picker/tar.gz/6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4" -react-native-invertible-scroll-view@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/react-native-invertible-scroll-view/-/react-native-invertible-scroll-view-1.0.0.tgz#60ceb384dc950c34eba3a5aeda39f97cdc7d4e23" - dependencies: - react-clone-referenced-element "^1.0.1" - react-native-scrollable-mixin "^1.0.1" - react-native-keyboard-aware-scroll-view@0.2.8: version "0.2.8" resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.2.8.tgz#e9843c0d7467a2e6a2a737883bd9c2b7c7e38e35" @@ -4548,10 +4541,6 @@ react-native-orientation@enahum/react-native-orientation.git: version "1.17.0" resolved "https://codeload.github.com/enahum/react-native-orientation/tar.gz/75ba7da814b99a949324846ce3a63fcbaeee9fdc" -react-native-scrollable-mixin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/react-native-scrollable-mixin/-/react-native-scrollable-mixin-1.0.1.tgz#34a32167b64248594154fd0d6a8b03f22740548e" - react-native-search-bar@enahum/react-native-search-bar.git: version "2.16.0" resolved "https://codeload.github.com/enahum/react-native-search-bar/tar.gz/88e6f5ba68012440ec21bde8388ca9e3124921c0"