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"