Re-factor post_list with VirtualizedList (#665)

* Re-factor postlist with VirtualizedList

* Remove react-native-invertible-scroll-view dependency

* Fix navigator not being pass to post

* Feedback review

* Feedback second review
This commit is contained in:
enahum 2017-06-23 16:07:06 -04:00 committed by GitHub
parent a614a2835b
commit 2a7c487463
13 changed files with 481 additions and 231 deletions

View file

@ -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;
};
}

View file

@ -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) => {

View file

@ -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 (
<View style={[styles.container, this.inversionDirection]}>
{footer()}
</View>
);
};
renderItem = (info) => {
return (
<View style={[styles.container, this.inversionDirection]}>
{this.props.renderItem(info)}
</View>
);
};
renderScrollComponent = (props) => {
const {theme} = this.props;
if (props.onRefresh) {
return (
<ScrollView
{...props}
refreshControl={
<RefreshControl
refreshing={props.refreshing}
onRefresh={props.onRefresh}
tintColor={theme.centerChannelColor}
colors={[theme.centerChannelColor]}
style={this.inversionDirection}
/>
}
/>
);
}
return <ScrollView {...props}/>;
};
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 (
<FlatList
{...forwardedProps}
/>
);
}
return (
<View style={[styles.container, this.inversionDirection]}>
<VirtualList
ref={this.setFlatListRef}
{...forwardedProps}
ListFooterComponent={this.renderFooter}
renderItem={this.renderItem}
renderScrollComponent={this.renderScrollComponent}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1
},
vertical: {
transform: [{scaleY: -1}]
},
horizontal: {
transform: [{scaleX: -1}]
}
});

View file

@ -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();
};
}

View file

@ -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)
};
}

View file

@ -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 (
<DateHeader
theme={this.props.theme}
date={date}
/>
);
};
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 (
<NewMessagesDivider
theme={this.props.theme}
/>
);
}
if (row === LOAD_MORE_POSTS) {
if (item === LOAD_MORE_POSTS) {
return (
<LoadMorePosts
loading={this.props.isLoadingMore}
@ -140,16 +131,7 @@ export default class PostList extends Component {
);
}
return this.renderPost(row);
};
renderDateHeader = (date) => {
return (
<DateHeader
theme={this.props.theme}
date={date}
/>
);
return this.renderPost(item);
};
renderPost = (post) => {
@ -166,44 +148,29 @@ export default class PostList extends Component {
);
};
renderRefreshControl = () => {
const {theme, refreshing} = this.props;
return (
<RefreshControl
refreshing={refreshing}
onRefresh={this.onRefresh}
tintColor={theme.centerChannelColor}
colors={[theme.centerChannelColor]}
/>
);
};
renderScrollComponent = (props) => {
return (
<InvertibleScrollView
{...props}
inverted={true}
/>
);
};
render() {
const {dataSource} = this.state;
const {channel, refreshing, theme} = this.props;
const refreshControl = {
refreshing
};
if (Object.keys(channel).length) {
refreshControl.onRefresh = this.onRefresh;
}
return (
<ListView
renderScrollComponent={this.renderScrollComponent}
dataSource={dataSource.cloneWithRows(this.getPostsWithLoadMore())}
renderFooter={this.renderChannelIntro}
renderRow={this.renderRow}
onEndReached={this.loadMore}
enableEmptySections={true}
showsVerticalScrollIndicator={true}
initialListSize={30}
onEndReachedThreshold={200}
pageSize={10}
refreshControl={this.renderRefreshControl()}
<FlatList
data={this.getPostsWithDates()}
initialNumToRender={20}
inverted={true}
keyExtractor={this.keyExtractor}
ListFooterComponent={this.renderChannelIntro}
onEndReached={this.loadMorePosts}
onEndReachedThreshold={700}
{...refreshControl}
renderItem={this.renderItem}
theme={theme}
/>
);
}

View file

@ -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
};

View file

@ -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
});

View file

@ -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 = {

View file

@ -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 = (
<PostListRetry
retry={() => 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 = <ChannelLoader theme={theme}/>;
} else {
component = (
<PostList
posts={posts}
posts={posts.slice(0, postVisibility)}
loadMore={this.loadMorePosts}
isLoadingMore={postsRequests.getPostsBefore.status === RequestStatus.STARTED}
showLoadMore={posts.length > 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 = <ChannelLoader theme={theme}/>;
}
return (
@ -202,7 +185,7 @@ class ChannelPostList extends PureComponent {
width: deviceWidth,
top: 0,
left: 0,
opacity: this.state.loaderOpacity
opacity: loaderOpacity
}}
>
<ChannelLoader theme={theme}/>

View file

@ -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);

View file

@ -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",

View file

@ -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"