Refactor postlist to include Android Pie fixes and smoother scrolling (#2161)

This commit is contained in:
Elias Nahum 2018-09-26 11:43:33 -03:00 committed by Harrison Healey
parent f7c7e70a37
commit f522df7a2e
16 changed files with 446 additions and 402 deletions

View file

@ -212,6 +212,7 @@ dependencies {
implementation project(':react-native-sentry')
implementation project(':react-native-exception-handler')
implementation project(':rn-fetch-blob')
implementation project(':react-native-recyclerview-list')
// For animated GIF support
implementation 'com.facebook.fresco:animated-base-support:1.3.0'

View file

@ -18,6 +18,7 @@ import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.github.godness84.RNRecyclerViewList.RNRecyclerviewListPackage;
import com.facebook.react.ReactPackage;
import com.facebook.soloader.SoLoader;
@ -75,7 +76,8 @@ public class MainApplication extends NavigationApplication implements INotificat
new ReactNativeDocumentPicker(),
new SharePackage(this),
new KeychainPackage(),
new InitializationPackage(this)
new InitializationPackage(this),
new RNRecyclerviewListPackage()
);
}

View file

@ -39,3 +39,5 @@ include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-recyclerview-list'
project(':react-native-recyclerview-list').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-recyclerview-list/android')

View file

@ -44,7 +44,7 @@ function makeMapStateToProps() {
return {
...getDimensions(state),
config,
googleDeveloperKey: config.GoogleDeveloperKey,
link,
openGraphData: getOpenGraphMetadataForUrl(state, link),
showLinkPreviews: previewsEnabled && config.EnableLinkPreviews === 'true',

View file

@ -37,7 +37,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
}).isRequired,
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
config: PropTypes.object,
googleDeveloperKey: PropTypes.string,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
isReplyPost: PropTypes.bool,
@ -198,7 +198,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
return (
<TouchableOpacity
style={[styles.imageContainer, {height: imgHeight}]}
style={[styles.imageContainer, {height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
{...this.responder}
onPress={this.playYouTubeVideo}
>
@ -225,13 +225,13 @@ export default class PostBodyAdditionalContent extends PureComponent {
return (
<TouchableWithoutFeedback
onPress={this.handlePreviewImage}
style={[styles.imageContainer, {height: imgHeight}]}
style={[styles.imageContainer, {height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
{...this.responder}
>
<View ref='item'>
<ProgressiveImage
ref='image'
style={[styles.image, {width, height: imgHeight}]}
style={[styles.image, {width, height: imgHeight || MAX_YOUTUBE_IMAGE_HEIGHT}]}
defaultSource={{uri}}
resizeMode='contain'
onError={this.handleLinkLoadError}
@ -394,11 +394,11 @@ export default class PostBodyAdditionalContent extends PureComponent {
playVideo(videoId, startTime).
catch(this.playYouTubeVideoError);
} else {
const {config} = this.props;
const {googleDeveloperKey} = this.props;
if (config.GoogleDeveloperKey) {
if (googleDeveloperKey) {
YouTubeStandaloneAndroid.playVideo({
apiKey: config.GoogleDeveloperKey,
apiKey: googleDeveloperKey,
videoId,
autoplay: true,
startTime,

View file

@ -18,15 +18,15 @@ 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;
let initialIndex = postIds.indexOf(START_OF_NEW_MESSAGES);
if (ownProps.highlightPostId) {
initialIndex = postIds.indexOf(ownProps.highlightPostId);
}
return {
deepLinkURL: state.views.root.deepLinkURL,
deviceHeight,
measureCellLayout,
postIds,
initialIndex,
serverURL: getCurrentUrl(state),
siteURL: getConfig(state).SiteURL,
theme: getTheme(state),

View file

@ -0,0 +1,125 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {StyleSheet} from 'react-native';
import RecyclerViewList, {DataSource, RecyclerRefreshControl} from 'react-native-recyclerview-list';
import {ListTypes} from 'app/constants';
import PostListBase from './post_list_base';
const SCROLL_UP_MULTIPLIER = 3.5;
export default class PostList extends PostListBase {
constructor(props) {
super(props);
this.contentOffsetY = 0;
this.state = {
refreshing: false,
managedConfig: {},
dataSource: new DataSource(props.postIds, this.keyExtractor),
};
}
componentWillReceiveProps(nextProps) {
if (this.props.channelId !== nextProps.channelId) {
this.contentOffsetY = 0;
this.contentHeight = 0;
}
if (nextProps.postIds !== this.props.postIds) {
this.setState({
dataSource: new DataSource(nextProps.postIds, this.keyExtractor),
});
}
}
handleLayout = (layoutHeight, contentWidth, contentHeight) => {
if (layoutHeight && contentHeight < layoutHeight) {
// We still have less than 1 screen of posts loaded with more to get, so load more
this.props.onLoadMoreUp();
}
};
handleScroll = (event) => {
const {layoutMeasurement, contentOffset, contentSize} = event.nativeEvent;
if (!this.postListHeight) {
this.handleLayout(layoutMeasurement.height, contentSize.width, contentSize.height);
}
this.postListHeight = layoutMeasurement.height;
if (contentOffset.y >= 0) {
const definedHeight = Math.round(this.postListHeight) * SCROLL_UP_MULTIPLIER;
const pageOffsetY = contentOffset.y;
const direction = (this.contentOffsetY < pageOffsetY) ?
ListTypes.VISIBILITY_SCROLL_DOWN :
ListTypes.VISIBILITY_SCROLL_UP;
this.contentOffsetY = contentOffset.y;
switch (direction) {
case ListTypes.VISIBILITY_SCROLL_UP:
if (Math.round(pageOffsetY) < definedHeight) {
this.props.onLoadMoreUp();
}
break;
}
}
};
render() {
const {
channelId,
initialIndex,
postIds,
} = this.props;
const otherProps = {};
if (postIds.length) {
otherProps.ListFooterComponent = this.props.renderFooter();
} else {
otherProps.ListEmptyComponent = this.props.renderFooter();
}
const hasPostsKey = postIds.length ? 'true' : 'false';
return (
<RecyclerRefreshControl
key={`recyclerFor-${channelId}-${hasPostsKey}`}
colors={['red', 'blue']}
direction='bottom'
enabled={Boolean(channelId)}
onRefresh={this.handleRefresh}
refreshing={this.state.refreshing}
style={styles.flex}
>
<RecyclerViewList
key={`${channelId}-${hasPostsKey}`}
ref={'list'}
style={[styles.flex, styles.postListContent]}
dataSource={this.state.dataSource}
renderItem={this.renderItem}
initialScrollIndex={initialIndex}
initialScrollPosition={RecyclerViewList.Constants.ScrollPosition.CENTER}
inverted={true}
onScroll={this.handleScroll}
onContentSizeChange={this.onContentSizeChange}
windowSize={60}
{...otherProps}
/>
</RecyclerRefreshControl>
);
}
}
const styles = StyleSheet.create({
flex: {
flex: 1,
},
postListContent: {
paddingTop: 5,
},
});

View file

@ -0,0 +1,137 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FlatList, StyleSheet} from 'react-native';
import {ListTypes} from 'app/constants';
import {makeExtraData} from 'app/utils/list_view';
import PostListBase from './post_list_base';
const INITIAL_BATCH_TO_RENDER = 15;
const SCROLL_UP_MULTIPLIER = 3.5;
export default class PostList extends PostListBase {
constructor(props) {
super(props);
this.hasDoneInitialScroll = false;
this.contentOffsetY = 0;
this.makeExtraData = makeExtraData();
this.state = {
managedConfig: {},
postListHeight: 0,
};
}
componentWillReceiveProps(nextProps) {
if (this.props.channelId !== nextProps.channelId) {
this.contentOffsetY = 0;
this.hasDoneInitialScroll = false;
}
}
handleContentSizeChange = (contentWidth, contentHeight) => {
this.scrollToInitialIndexIfNeeded(contentWidth, contentHeight);
if (this.state.postListHeight && contentHeight < this.state.postListHeight && this.props.extraData) {
// We still have less than 1 screen of posts loaded with more to get, so load more
this.props.onLoadMoreUp();
}
};
handleLayout = (event) => {
const {height} = event.nativeEvent.layout;
this.setState({postListHeight: height});
};
handleScroll = (event) => {
const pageOffsetY = event.nativeEvent.contentOffset.y;
if (pageOffsetY > 0) {
const contentHeight = event.nativeEvent.contentSize.height;
const direction = (this.contentOffsetY < pageOffsetY) ?
ListTypes.VISIBILITY_SCROLL_UP :
ListTypes.VISIBILITY_SCROLL_DOWN;
this.contentOffsetY = pageOffsetY;
if (
direction === ListTypes.VISIBILITY_SCROLL_UP &&
(contentHeight - pageOffsetY) < (this.state.postListHeight * SCROLL_UP_MULTIPLIER)
) {
this.props.onLoadMoreUp();
}
}
};
handleScrollToIndexFailed = () => {
requestAnimationFrame(() => {
this.hasDoneInitialScroll = false;
this.scrollToInitialIndexIfNeeded(1, 1);
});
};
scrollToInitialIndexIfNeeded = (width, height) => {
if (
width > 0 &&
height > 0 &&
this.props.initialIndex > 0 &&
!this.hasDoneInitialScroll
) {
this.refs.list.scrollToIndex({
animated: false,
index: this.props.initialIndex,
viewPosition: 0.5,
});
this.hasDoneInitialScroll = true;
}
};
render() {
const {
channelId,
highlightPostId,
postIds,
} = this.props;
const refreshControl = {
refreshing: false,
};
if (channelId) {
refreshControl.onRefresh = this.handleRefresh;
}
const hasPostsKey = postIds.length ? 'true' : 'false';
return (
<FlatList
key={`recyclerFor-${channelId}-${hasPostsKey}`}
ref='list'
contentContainerStyle={styles.postListContent}
data={postIds}
extraData={this.makeExtraData(channelId, highlightPostId, this.props.extraData)}
initialNumToRender={INITIAL_BATCH_TO_RENDER}
inverted={true}
keyExtractor={this.keyExtractor}
ListFooterComponent={this.props.renderFooter}
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
onContentSizeChange={this.handleContentSizeChange}
onLayout={this.handleLayout}
onScroll={this.handleScroll}
onScrollToIndexFailed={this.handleScrollToIndexFailed}
removeClippedSubviews={true}
renderItem={this.renderItem}
scrollEventThrottle={60}
{...refreshControl}
/>
);
}
}
const styles = StyleSheet.create({
postListContent: {
paddingTop: 5,
},
});

View file

@ -3,34 +3,18 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
InteractionManager,
Platform,
StyleSheet,
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import Post from 'app/components/post';
import {START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import mattermostManaged from 'app/mattermost_managed';
import {makeExtraData} from 'app/utils/list_view';
import {changeOpacity} from 'app/utils/theme';
import {matchPermalink} from 'app/utils/url';
import DateHeader from './date_header';
import {isDateLine} from './date_header/utils';
import NewMessagesDivider from './new_messages_divider';
import withLayout from './with_layout';
const PostWithLayout = withLayout(Post);
const INITIAL_BATCH_TO_RENDER = 15;
const NEW_MESSAGES_HEIGHT = 28;
const DATE_HEADER_HEIGHT = 28;
export default class PostList extends PureComponent {
export default class PostListBase extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadChannelsByTeamName: PropTypes.func.isRequired,
@ -39,19 +23,14 @@ export default class PostList extends PureComponent {
setDeepLinkURL: PropTypes.func.isRequired,
}).isRequired,
channelId: PropTypes.string,
currentUserId: PropTypes.string,
deepLinkURL: PropTypes.string,
deviceHeight: PropTypes.number.isRequired,
extraData: PropTypes.any,
highlightPostId: PropTypes.string,
indicateNewMessages: PropTypes.bool,
initialIndex: PropTypes.number,
isSearchResult: PropTypes.bool,
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
measureCellLayout: PropTypes.bool,
navigator: PropTypes.object,
onContentSizeChange: PropTypes.func,
onEndReached: PropTypes.func,
onHashtagPress: PropTypes.func,
onLoadMoreUp: PropTypes.func,
onPermalinkPress: PropTypes.func,
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
@ -65,47 +44,20 @@ export default class PostList extends PureComponent {
};
static defaultProps = {
loadMore: () => true,
onLoadMoreUp: () => true,
renderFooter: () => null,
};
constructor(props) {
super(props);
this.newMessagesIndex = -1;
this.makeExtraData = makeExtraData();
this.itemMeasurements = {};
this.state = {
managedConfig: {},
scrollToMessage: false,
};
}
componentWillMount() {
this.listenerId = mattermostManaged.addEventListener('change', this.setManagedConfig);
}
componentDidMount() {
EventEmitter.on('reset_channel', this.scrollToBottomOffset);
this.mounted = true;
this.setManagedConfig();
}
componentWillReceiveProps(nextProps) {
if (nextProps.postIds !== this.props.postIds) {
this.newMessagesIndex = -1;
}
if (this.props.channelId !== nextProps.channelId) {
this.itemMeasurements = {};
this.newMessageScrolledTo = false;
this.scrollToBottomOffset();
}
}
componentDidUpdate() {
if ((this.props.measureCellLayout || this.props.isSearchResult) && this.state.scrollToMessage) {
this.scrollListToMessageOffset();
}
if (this.props.deepLinkURL) {
this.handleDeepLink(this.props.deepLinkURL);
this.props.actions.setDeepLinkURL('');
@ -113,7 +65,7 @@ export default class PostList extends PureComponent {
}
componentWillUnmount() {
EventEmitter.off('reset_channel', this.scrollToBottomOffset);
this.mounted = false;
mattermostManaged.removeEventListener(this.listenerId);
}
@ -135,6 +87,22 @@ export default class PostList extends PureComponent {
}
};
handleRefresh = () => {
const {
actions,
channelId,
onRefresh,
} = this.props;
if (channelId) {
actions.refreshChannelWithRetry(channelId);
}
if (onRefresh) {
onRefresh();
}
};
handlePermalinkPress = (postId, teamName) => {
const {actions, onPermalinkPress} = this.props;
@ -146,12 +114,87 @@ export default class PostList extends PureComponent {
}
};
showPermalinkView = (postId) => {
keyExtractor = (item) => {
// All keys are strings (either post IDs or special keys)
return item;
};
renderItem = ({item, index}) => {
if (item === START_OF_NEW_MESSAGES) {
// 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.
const moreNewMessages = this.props.postIds.length === index + 2;
return (
<NewMessagesDivider
index={index}
theme={this.props.theme}
moreMessages={moreNewMessages}
/>
);
} else if (isDateLine(item)) {
return (
<DateHeader
dateLineString={item}
index={index}
/>
);
}
const postId = item;
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
// comes after this one in the list
const previousPostId = index < this.props.postIds.length - 1 ? this.props.postIds[index + 1] : null;
const nextPostId = index > 0 ? this.props.postIds[index - 1] : null;
return this.renderPost(postId, previousPostId, nextPostId);
};
renderPost = (postId, previousPostId, nextPostId) => {
const {
actions,
highlightPostId,
isSearchResult,
navigator,
onHashtagPress,
onPostPress,
renderReplies,
shouldRenderReplyButton,
} = this.props;
const {managedConfig} = this.state;
const highlight = highlightPostId === postId;
return (
<Post
postId={postId}
previousPostId={previousPostId}
nextPostId={nextPostId}
onPermalinkPress={this.handlePermalinkPress}
highlight={highlight}
renderReplies={renderReplies}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
onPress={onPostPress}
navigator={navigator}
managedConfig={managedConfig}
/>
);
};
setManagedConfig = async (config) => {
let nextConfig = config;
if (!nextConfig) {
nextConfig = await mattermostManaged.getLocalConfig();
}
if (this.mounted) {
this.setState({
managedConfig: nextConfig,
});
}
};
showPermalinkView = (postId) => {
const {actions, navigator} = this.props;
actions.selectFocusedPostId(postId);
@ -169,7 +212,6 @@ export default class PostList extends PureComponent {
passProps: {
isPermalink: true,
onClose: this.handleClosePermalink,
onHashtagPress,
onPermalinkPress: this.handlePermalinkPress,
},
};
@ -178,229 +220,5 @@ export default class PostList extends PureComponent {
navigator.showModal(options);
}
};
scrollToBottomOffset = () => {
InteractionManager.runAfterInteractions(() => {
if (this.refs.list) {
this.refs.list.scrollToOffset({offset: 0, animated: false});
}
});
};
getMeasurementOffset = (index) => {
const orderedKeys = Object.keys(this.itemMeasurements).sort((a, b) => {
const numA = Number(a);
const numB = Number(b);
if (numA > numB) {
return 1;
} else if (numA < numB) {
return -1;
}
return 0;
}).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 - 1 : this.newMessagesIndex;
if (index !== -1) {
let offset = this.getMeasurementOffset(index) + this.itemMeasurements[index];
const windowHeight = this.state.postListHeight;
if (offset < windowHeight) {
return;
}
const upperBound = offset + windowHeight;
offset = offset - ((upperBound - offset) / 2);
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,
});
}
});
}
};
setManagedConfig = async (config) => {
let nextConfig = config;
if (!nextConfig) {
nextConfig = await mattermostManaged.getLocalConfig();
}
if (Object.keys(nextConfig).length) {
this.setState({
managedConfig: nextConfig,
});
}
};
keyExtractor = (item) => {
// All keys are strings (either post IDs or special keys)
return item;
};
onRefresh = () => {
const {
actions,
channelId,
onRefresh,
} = this.props;
if (channelId) {
actions.refreshChannelWithRetry(channelId);
}
if (onRefresh) {
onRefresh();
}
};
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;
this.itemMeasurements[index] = NEW_MESSAGES_HEIGHT;
return (
<NewMessagesDivider
index={index}
theme={this.props.theme}
moreMessages={this.moreNewMessages}
/>
);
} else if (isDateLine(item)) {
this.itemMeasurements[index] = DATE_HEADER_HEIGHT;
return (
<DateHeader
dateLineString={item}
index={index}
/>
);
}
const postId = item;
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
// comes after this one in the list
const previousPostId = index < this.props.postIds.length - 1 ? this.props.postIds[index + 1] : null;
const nextPostId = index > 0 ? this.props.postIds[index - 1] : null;
return this.renderPost(postId, previousPostId, nextPostId, index);
};
renderPost = (postId, previousPostId, nextPostId, index) => {
const {
highlightPostId,
isSearchResult,
navigator,
onHashtagPress,
onPostPress,
renderReplies,
shouldRenderReplyButton,
} = this.props;
const {managedConfig} = this.state;
const highlight = highlightPostId === postId;
if (highlight) {
this.newMessagesIndex = index;
}
return (
<PostWithLayout
postId={postId}
previousPostId={previousPostId}
nextPostId={nextPostId}
highlight={highlight}
index={index}
renderReplies={renderReplies}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
onHashtagPress={onHashtagPress}
onPermalinkPress={this.handlePermalinkPress}
onPress={onPostPress}
navigator={navigator}
managedConfig={managedConfig}
onLayoutCalled={this.measureItem}
shouldCallOnLayout={this.props.measureCellLayout && !this.newMessageScrolledTo}
/>
);
};
onLayout = (event) => {
const {height} = event.nativeEvent.layout;
this.setState({
postListHeight: height,
});
};
render() {
const {
channelId,
highlightPostId,
onEndReached,
postIds,
} = this.props;
const refreshControl = {
refreshing: false,
};
if (channelId) {
refreshControl.onRefresh = this.onRefresh;
}
return (
<FlatList
onContentSizeChange={this.props.onContentSizeChange}
onLayout={this.onLayout}
ref='list'
data={postIds}
extraData={this.makeExtraData(channelId, highlightPostId, this.props.extraData)}
initialNumToRender={10}
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
inverted={true}
keyExtractor={this.keyExtractor}
ListFooterComponent={this.props.renderFooter}
onEndReached={onEndReached}
onEndReachedThreshold={Platform.OS === 'ios' ? 0 : 1}
removeClippedSubviews={true}
{...refreshControl}
renderItem={this.renderItem}
contentContainerStyle={styles.postListContent}
/>
);
}
}
const styles = StyleSheet.create({
postListContent: {
paddingTop: 5,
},
});

View file

@ -1,41 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {emptyFunction} from 'app/utils/general';
function withLayout(WrappedComponent) {
return class WithLayoutComponent extends PureComponent {
static propTypes = {
index: PropTypes.number.isRequired,
onLayoutCalled: PropTypes.func,
shouldCallOnLayout: PropTypes.bool,
};
static defaultProps = {
onLayoutCalled: emptyFunction,
};
onLayout = (event) => {
const {height} = event.nativeEvent.layout;
const {shouldCallOnLayout} = this.props;
if (shouldCallOnLayout) {
this.props.onLayoutCalled(this.props.index, height);
}
};
render() {
const {index, onLayoutCalled, shouldCallOnLayout, ...otherProps} = this.props; //eslint-disable-line no-unused-vars
return (
<View onLayout={this.onLayout}>
<WrappedComponent {...otherProps}/>
</View>
);
}
};
}
export default withLayout;

View file

@ -7,15 +7,13 @@ import {
Platform,
StyleSheet,
View,
InteractionManager,
} from 'react-native';
import {debounce} from 'mattermost-redux/actions/helpers';
import AnnouncementBanner from 'app/components/announcement_banner';
import PostList from 'app/components/post_list';
import PostListRetry from 'app/components/post_list_retry';
import RetryBarIndicator from 'app/components/retry_bar_indicator';
import {ViewTypes} from 'app/constants';
import tracker from 'app/utils/time_tracker';
let ChannelIntro = null;
@ -34,7 +32,6 @@ export default class ChannelPostList extends PureComponent {
channelId: PropTypes.string.isRequired,
channelRefreshingFailed: PropTypes.bool,
currentUserId: PropTypes.string,
deviceHeight: PropTypes.number.isRequired,
lastViewedAt: PropTypes.number,
loadMorePostsVisible: PropTypes.bool.isRequired,
navigator: PropTypes.object,
@ -44,7 +41,7 @@ export default class ChannelPostList extends PureComponent {
};
static defaultProps = {
postVisibility: 15,
postVisibility: ViewTypes.POST_VISIBILITY_CHUNK_SIZE,
};
constructor(props) {
@ -52,39 +49,20 @@ export default class ChannelPostList extends PureComponent {
this.state = {
visiblePostIds: this.getVisiblePostIds(props),
loading: true,
};
this.contentHeight = 0;
}
componentDidMount() {
this.mounted = true;
InteractionManager.runAfterInteractions(() => {
if (this.mounted === true) {
this.setState({loading: false});
}
});
}
componentWillReceiveProps(nextProps) {
const {postIds: nextPostIds} = nextProps;
let visiblePostIds = this.state.visiblePostIds;
const channelSwitch = nextProps.channelId !== this.props.channelId;
if (nextPostIds !== this.props.postIds || nextProps.postVisibility !== this.props.postVisibility) {
visiblePostIds = this.getVisiblePostIds(nextProps);
}
this.setState({
visiblePostIds,
loading: channelSwitch,
}, () => InteractionManager.runAfterInteractions(() => {
if (channelSwitch) {
this.setState({loading: false});
}
}));
this.setState({visiblePostIds});
}
componentDidUpdate(prevProps) {
@ -93,10 +71,6 @@ export default class ChannelPostList extends PureComponent {
}
}
componentWillUnmount() {
this.mounted = false;
}
getVisiblePostIds = (props) => {
return props.postIds.slice(0, props.postVisibility);
};
@ -131,21 +105,15 @@ export default class ChannelPostList extends PureComponent {
}
};
handleContentSizeChange = (contentWidth, contentHeight) => {
this.contentHeight = contentHeight;
};
loadMorePosts = debounce(() => {
if (this.props.loadMorePostsVisible) {
const {actions, channelId} = this.props;
loadMorePostsTop = async () => {
const {actions, channelId} = this.props;
if (!this.isLoadingMoreTop) {
this.isLoadingMoreTop = true;
actions.increasePostVisibility(channelId).then((hasMore) => {
if (hasMore && this.contentHeight < this.props.deviceHeight) {
// We still have less than 1 screen of posts loaded with more to get, so load more
this.loadMorePosts();
}
this.isLoadingMoreTop = !hasMore;
});
}
}, 100);
};
loadPostsRetry = () => {
const {actions, channelId} = this.props;
@ -165,7 +133,7 @@ export default class ChannelPostList extends PureComponent {
return (
<LoadMorePosts
channelId={this.props.channelId}
loadMore={this.loadMorePosts}
loadMore={this.loadMorePostsTop}
theme={this.props.theme}
/>
);
@ -196,15 +164,7 @@ export default class ChannelPostList extends PureComponent {
theme,
} = this.props;
const {
visiblePostIds,
loading,
} = this.state;
if (loading) {
return null;
}
const {visiblePostIds} = this.state;
let component;
if (!postIds.length && channelRefreshingFailed) {
@ -219,8 +179,7 @@ export default class ChannelPostList extends PureComponent {
<PostList
postIds={visiblePostIds}
extraData={loadMorePostsVisible}
onContentSizeChange={this.handleContentSizeChange}
onEndReached={this.loadMorePosts}
onLoadMoreUp={this.loadMorePostsTop}
onPostPress={this.goToThread}
onRefresh={actions.setChannelRefreshing}
renderReplies={true}

View file

@ -246,8 +246,8 @@ export default class Permalink extends PureComponent {
let focusChannelId = channelId;
const post = await actions.getPostThread(focusedPostId, false);
if (this.mounted && post.error && (!postIds || !postIds.length)) {
if (isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
if (post.error && (!postIds || !postIds.length)) {
if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') {
this.setState({
error: formatMessage({
id: 'permalink.error.access',
@ -258,7 +258,7 @@ export default class Permalink extends PureComponent {
defaultMessage: 'No Results Found',
}),
});
} else {
} else if (this.mounted) {
this.setState({error: post.error.message, retry: true});
}
@ -281,7 +281,9 @@ export default class Permalink extends PureComponent {
actions.getPostsAfter(focusChannelId, focusedPostId, 0, 10),
]);
this.setState({loading: false});
if (this.mounted) {
this.setState({loading: false});
}
};
onNavigatorEvent = (event) => {

7
package-lock.json generated
View file

@ -14382,6 +14382,13 @@
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-1.1.1.tgz",
"integrity": "sha512-t0Ujm177bagjUOSzhpmkSz+LqFW04HnY9TeZFavDCmV521fQvFz82aD+POXqWsAdsJVOK3umJYBNNqCjC3g0hQ=="
},
"react-native-recyclerview-list": {
"version": "github:enahum/react-native-recyclerview-list#a05429439c49c41c73f42bb1d8ddc86eb8af5d35",
"from": "github:enahum/react-native-recyclerview-list#a05429439c49c41c73f42bb1d8ddc86eb8af5d35",
"requires": {
"prop-types": "^15.6.0"
}
},
"react-native-safe-area": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/react-native-safe-area/-/react-native-safe-area-0.5.0.tgz",

View file

@ -45,6 +45,7 @@
"react-native-notifications": "github:enahum/react-native-notifications#e5bf431716613b6abf312698dde194537420f966",
"react-native-passcode-status": "1.1.1",
"react-native-permissions": "1.1.1",
"react-native-recyclerview-list": "enahum/react-native-recyclerview-list.git#a05429439c49c41c73f42bb1d8ddc86eb8af5d35",
"react-native-safe-area": "0.5.0",
"react-native-section-list-get-item-layout": "2.2.3",
"react-native-sentry": "0.38.3",

File diff suppressed because one or more lines are too long

View file

@ -2,6 +2,8 @@
// See LICENSE.txt for license information.
module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/app_icon.js',
'./node_modules/app/components/at_mention/at_mention.js',
'./node_modules/app/components/at_mention/index.js',
'./node_modules/app/components/attachment_button.js',
'./node_modules/app/components/badge.js',
'./node_modules/app/components/channel_icon.js',
@ -25,6 +27,7 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/layout/keyboard_layout/index.js',
'./node_modules/app/components/layout/keyboard_layout/keyboard_layout.js',
'./node_modules/app/components/loading.js',
'./node_modules/app/components/markdown/hashtag.js',
'./node_modules/app/components/markdown/index.js',
'./node_modules/app/components/markdown/markdown.js',
'./node_modules/app/components/markdown/markdown_block_quote.js',
@ -55,8 +58,8 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/post_list/date_header/index.js',
'./node_modules/app/components/post_list/index.js',
'./node_modules/app/components/post_list/new_messages_divider.js',
'./node_modules/app/components/post_list/post_list.js',
'./node_modules/app/components/post_list/with_layout.js',
'./node_modules/app/components/post_list/post_list.android.js',
'./node_modules/app/components/post_list/post_list_base.js',
'./node_modules/app/components/post_list_retry.js',
'./node_modules/app/components/post_profile_picture/index.js',
'./node_modules/app/components/post_profile_picture/post_profile_picture.js',
@ -68,7 +71,6 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/components/profile_picture/profile_picture.js',
'./node_modules/app/components/progressive_image/index.js',
'./node_modules/app/components/progressive_image/progressive_image.js',
'./node_modules/app/components/quick_text_input.js',
'./node_modules/app/components/reply_icon.js',
'./node_modules/app/components/safe_area_view/index.js',
'./node_modules/app/components/safe_area_view/safe_area_view.android.js',
@ -115,6 +117,7 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/reducers/views/i18n.js',
'./node_modules/app/reducers/views/index.js',
'./node_modules/app/reducers/views/login.js',
'./node_modules/app/reducers/views/post.js',
'./node_modules/app/reducers/views/recent_emojis.js',
'./node_modules/app/reducers/views/root.js',
'./node_modules/app/reducers/views/search.js',
@ -145,14 +148,35 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/app/utils/avoid_native_bridge.js',
'./node_modules/app/utils/client_upgrade.js',
'./node_modules/app/utils/general.js',
'./node_modules/app/utils/i18n.js',
'./node_modules/app/utils/image_cache_manager.js',
'./node_modules/app/utils/network.js',
'./node_modules/app/utils/push_notifications.js',
'./node_modules/app/utils/sentry/middleware.js',
'./node_modules/app/utils/i18n.js',
'./node_modules/app/utils/theme.js',
'./node_modules/app/utils/time_tracker.js',
'./node_modules/index.js',
'./node_modules/node_modules/@babel/runtime/helpers/arrayWithHoles.js',
'./node_modules/node_modules/@babel/runtime/helpers/arrayWithoutHoles.js',
'./node_modules/node_modules/@babel/runtime/helpers/assertThisInitialized.js',
'./node_modules/node_modules/@babel/runtime/helpers/classCallCheck.js',
'./node_modules/node_modules/@babel/runtime/helpers/createClass.js',
'./node_modules/node_modules/@babel/runtime/helpers/defineProperty.js',
'./node_modules/node_modules/@babel/runtime/helpers/extends.js',
'./node_modules/node_modules/@babel/runtime/helpers/get.js',
'./node_modules/node_modules/@babel/runtime/helpers/getPrototypeOf.js',
'./node_modules/node_modules/@babel/runtime/helpers/inherits.js',
'./node_modules/node_modules/@babel/runtime/helpers/interopRequireDefault.js',
'./node_modules/node_modules/@babel/runtime/helpers/interopRequireWildcard.js',
'./node_modules/node_modules/@babel/runtime/helpers/objectSpread.js',
'./node_modules/node_modules/@babel/runtime/helpers/objectWithoutProperties.js',
'./node_modules/node_modules/@babel/runtime/helpers/objectWithoutPropertiesLoose.js',
'./node_modules/node_modules/@babel/runtime/helpers/possibleConstructorReturn.js',
'./node_modules/node_modules/@babel/runtime/helpers/setPrototypeOf.js',
'./node_modules/node_modules/@babel/runtime/helpers/slicedToArray.js',
'./node_modules/node_modules/@babel/runtime/helpers/toConsumableArray.js',
'./node_modules/node_modules/@babel/runtime/helpers/typeof.js',
'./node_modules/node_modules/@babel/runtime/regenerator/index.js',
'./node_modules/node_modules/base-64/base64.js',
'./node_modules/node_modules/clamp/index.js',
'./node_modules/node_modules/color-convert/conversions.js',
@ -408,6 +432,11 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/node_modules/react-native-notifications/notification.android.js',
'./node_modules/node_modules/react-native-permissions/index.js',
'./node_modules/node_modules/react-native-permissions/lib/permissions.android.js',
'./node_modules/node_modules/react-native-recyclerview-list/index.js',
'./node_modules/node_modules/react-native-recyclerview-list/src/Constants.js',
'./node_modules/node_modules/react-native-recyclerview-list/src/DataSource.js',
'./node_modules/node_modules/react-native-recyclerview-list/src/RecyclerRefresh.js',
'./node_modules/node_modules/react-native-recyclerview-list/src/RecyclerViewList.js',
'./node_modules/node_modules/react-native-svg/elements/Circle.js',
'./node_modules/node_modules/react-native-svg/elements/ClipPath.js',
'./node_modules/node_modules/react-native-svg/elements/Defs.js',
@ -416,7 +445,9 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/node_modules/react-native-svg/elements/Image.js',
'./node_modules/node_modules/react-native-svg/elements/Line.js',
'./node_modules/node_modules/react-native-svg/elements/LinearGradient.js',
'./node_modules/node_modules/react-native-svg/elements/Mask.js',
'./node_modules/node_modules/react-native-svg/elements/Path.js',
'./node_modules/node_modules/react-native-svg/elements/Pattern.js',
'./node_modules/node_modules/react-native-svg/elements/Polygon.js',
'./node_modules/node_modules/react-native-svg/elements/Polyline.js',
'./node_modules/node_modules/react-native-svg/elements/RadialGradient.js',
@ -668,6 +699,8 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/node_modules/redux-persist/lib/utils/isStatePlainEnough.js',
'./node_modules/node_modules/redux-persist/lib/utils/setImmediate.js',
'./node_modules/node_modules/redux/lib/redux.js',
'./node_modules/node_modules/regenerator-runtime/runtime-module.js',
'./node_modules/node_modules/regenerator-runtime/runtime.js',
'./node_modules/node_modules/reselect/lib/index.js',
'./node_modules/node_modules/rn-fetch-blob/android.js',
'./node_modules/node_modules/rn-fetch-blob/cba/index.js',
@ -713,8 +746,6 @@ module.exports = ['./node_modules/app/app.js',
'./node_modules/node_modules/schedule/tracking.js',
'./node_modules/node_modules/semver/semver.js',
'./node_modules/node_modules/shallow-equals/index.js',
'./node_modules/node_modules/stacktrace-parser/index.js',
'./node_modules/node_modules/stacktrace-parser/lib/stacktrace-parser.js',
'./node_modules/node_modules/symbol-observable/lib/index.js',
'./node_modules/node_modules/symbol-observable/lib/ponyfill.js',
'./node_modules/node_modules/tinycolor2/tinycolor.js',