Improving Performance Part 3 (#1011)

* Update mattermost-redux

* Tap on Deleted post to remove it

* Improvements when rendering post additional content

* Avoid unnecessary re-renders on the post list component

* Avoid unnecessary re-renders on the channel post list component

* Avoid unnecessary re-renders on the channel screen

* Feedback review
This commit is contained in:
enahum 2017-10-11 14:54:13 -03:00 committed by GitHub
parent f8414559b2
commit f50b2b9412
24 changed files with 468 additions and 318 deletions

View file

@ -3,7 +3,9 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, Platform, RefreshControl, ScrollView, StyleSheet, View} from 'react-native';
import {FlatList, Platform, ScrollView, StyleSheet, View} from 'react-native';
import RefreshList from 'app/components/refresh_list';
import VirtualList from './virtual_list';
@ -64,7 +66,7 @@ export default class InvertibleFlatList extends PureComponent {
<ScrollView
{...props}
refreshControl={
<RefreshControl
<RefreshList
refreshing={props.refreshing}
onRefresh={props.onRefresh}
tintColor={theme.centerChannelColor}

View file

@ -12,23 +12,20 @@ import {getTheme} from 'app/selectors/preferences';
import Post from './post';
function makeMapStateToProps() {
return function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.post.id);
function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.postId);
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
return {
...ownProps,
post,
config,
currentUserId: getCurrentUserId(state),
highlight: ownProps.post.highlight,
license,
roles,
theme: getTheme(state)
};
return {
post,
config,
currentUserId: getCurrentUserId(state),
highlight: ownProps.highlight,
license,
roles,
theme: getTheme(state)
};
}
@ -43,4 +40,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(Post);
export default connect(mapStateToProps, mapDispatchToProps)(Post);

View file

@ -6,17 +6,8 @@ import {bindActionCreators} from 'redux';
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
import {getTheme} from 'app/selectors/preferences';
import PostAttachmentOpenGraph from './post_attachment_opengraph';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
@ -25,4 +16,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph);
export default connect(null, mapDispatchToProps)(PostAttachmentOpenGraph);

View file

@ -190,11 +190,20 @@ class PostBody extends PureComponent {
let messageComponent;
if (hasBeenDeleted) {
messageComponent = (
<FormattedText
style={messageStyle}
id='post_body.deleted'
defaultMessage='(message deleted)'
/>
<TouchableHighlight
onHideUnderlay={this.handleHideUnderlay}
onPress={onPress}
onShowUnderlay={this.handleShowUnderlay}
underlayColor='transparent'
>
<View style={{flexDirection: 'row'}}>
<FormattedText
style={messageStyle}
id='post_body.deleted'
defaultMessage='(message deleted)'
/>
</View>
</TouchableHighlight>
);
body = (<View>{messageComponent}</View>);
} else if (message.length) {

View file

@ -41,7 +41,6 @@ function makeMapStateToProps() {
const link = getFirstLink(ownProps.message);
return {
...ownProps,
...getDimensions(state),
config,
link,

View file

@ -98,7 +98,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
return null;
}
const {link, openGraphData, showLinkPreviews} = this.props;
const {link, openGraphData, showLinkPreviews, theme} = this.props;
const attachments = this.getSlackAttachment();
if (attachments) {
return attachments;
@ -109,6 +109,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
<PostAttachmentOpenGraph
link={link}
openGraphData={openGraphData}
theme={theme}
/>
);
}
@ -244,22 +245,22 @@ export default class PostBodyAdditionalContent extends PureComponent {
};
render() {
const {link, openGraphData} = this.props;
const {link, openGraphData, postProps} = this.props;
const {linkLoaded, linkLoadError} = this.state;
let isYouTube = false;
let isImage = false;
let isOpenGraph = false;
const {attachments} = postProps;
if (link) {
isYouTube = isYoutubeLink(link);
isImage = isImageLink(link);
isOpenGraph = Boolean(openGraphData && openGraphData.description);
if (!link && !attachments) {
return null;
}
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
const embed = this.generateToggleableEmbed(isImage, isYouTube);
if (embed && (linkLoaded || isYouTube)) {
return embed;
}
const isYouTube = isYoutubeLink(link);
const isImage = isImageLink(link);
const isOpenGraph = Boolean(openGraphData && openGraphData.description);
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
const embed = this.generateToggleableEmbed(isImage, isYouTube);
if (embed && (linkLoaded || isYouTube)) {
return embed;
}
}

View file

@ -9,9 +9,8 @@ import {getTheme} from 'app/selectors/preferences';
import PostList from './post_list';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
return {
...ownProps,
theme: getTheme(state)
};
}

View file

@ -2,6 +2,7 @@
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {
TouchableOpacity,
@ -13,7 +14,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
export default class LoadMorePosts extends PureComponent {
class LoadMorePosts extends PureComponent {
static propTypes = {
loading: PropTypes.bool.isRequired,
loadMore: PropTypes.func,
@ -72,3 +73,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
}
};
});
function mapStateToProps(state, ownProps) {
return {
loading: state.views.channel.loadingPosts[ownProps.channelId] || false
};
}
export default connect(mapStateToProps)(LoadMorePosts);

View file

@ -28,7 +28,6 @@ export default class PostList extends PureComponent {
channelId: PropTypes.string,
currentUserId: PropTypes.string,
indicateNewMessages: PropTypes.bool,
isLoadingMore: PropTypes.bool,
isSearchResult: PropTypes.bool,
lastViewedAt: PropTypes.number,
loadMore: PropTypes.func,
@ -36,17 +35,26 @@ export default class PostList extends PureComponent {
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
posts: PropTypes.array.isRequired,
refreshing: PropTypes.bool,
renderReplies: PropTypes.bool,
showLoadMore: PropTypes.bool,
shouldRenderReplyButton: PropTypes.bool,
theme: PropTypes.object.isRequired
};
static defaultProps = {
loadMore: () => true
};
componentDidUpdate(prevProps) {
if (prevProps.channelId !== this.props.channelId && this.refs.list) {
// When switching channels make sure we start from the bottom
this.refs.list.scrollToOffset({y: 0, animated: false});
}
}
getPostsWithDates = () => {
const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props;
const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
if (showLoadMore) {
return [...list, LOAD_MORE_POSTS];
}
@ -58,18 +66,8 @@ export default class PostList extends PureComponent {
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();
}
return item.id || item;
};
onRefresh = () => {
@ -89,9 +87,10 @@ export default class PostList extends PureComponent {
};
renderChannelIntro = () => {
const {channelId, navigator, refreshing, showLoadMore} = this.props;
const {channelId, navigator, showLoadMore} = this.props;
if (channelId && !showLoadMore && !refreshing) {
// FIXME: Only show the channel intro when we are at the very start of the channel
if (channelId && !showLoadMore) {
return (
<View>
<ChannelIntro navigator={navigator}/>
@ -124,10 +123,7 @@ export default class PostList extends PureComponent {
}
if (item === LOAD_MORE_POSTS && this.props.showLoadMore) {
return (
<LoadMorePosts
loading={this.props.isLoadingMore}
theme={this.props.theme}
/>
<LoadMorePosts theme={this.props.theme}/>
);
}
@ -149,7 +145,8 @@ export default class PostList extends PureComponent {
return (
<Post
post={post}
highlight={post.highlight}
postId={post.id}
renderReplies={renderReplies}
isFirstReply={post.isFirstReply}
isLastReply={post.isLastReply}
@ -163,24 +160,26 @@ export default class PostList extends PureComponent {
};
render() {
const {channelId, refreshing, theme} = this.props;
const {channelId, loadMore, theme} = this.props;
const refreshControl = {
refreshing
refreshing: false
};
if (channelId) {
refreshControl.onRefresh = this.onRefresh;
}
const data = this.getPostsWithDates();
return (
<FlatList
data={this.getPostsWithDates()}
initialNumToRender={20}
ref='list'
data={data}
initialNumToRender={15}
inverted={true}
keyExtractor={this.keyExtractor}
ListFooterComponent={this.renderChannelIntro}
onEndReached={this.loadMorePosts}
onEndReached={loadMore}
onEndReachedThreshold={0}
{...refreshControl}
renderItem={this.renderItem}

View file

@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import {createPost} from 'mattermost-redux/actions/posts';
import {userTyping} from 'mattermost-redux/actions/websocket';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
@ -22,6 +23,7 @@ function mapStateToProps(state, ownProps) {
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
return {
channelId: getCurrentChannelId(state),
canUploadFiles: canUploadFilesOnMobile(state),
channelIsLoading: state.views.channel.loading,
currentUserId: getCurrentUserId(state),

View file

@ -0,0 +1,23 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getConnection} from 'app/selectors/device';
import RefreshList from './refresh_list';
function mapStateToProps(state) {
const networkOnline = getConnection(state);
let {refreshing} = state.views.channel;
if (!networkOnline) {
refreshing = false;
}
return {
refreshing
};
}
export default connect(mapStateToProps)(RefreshList);

View file

@ -0,0 +1,19 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {RefreshControl} from 'react-native';
export default class RefreshList extends PureComponent {
static propTypes = {
...RefreshControl.propTypes
};
render() {
return (
<RefreshControl
{...this.props}
/>
);
}
}

View file

@ -0,0 +1,25 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {RequestStatus} from 'mattermost-redux/constants';
import {getConnection} from 'app/selectors/device';
import RetryBarIndicator from './retry_bar_indicator';
function mapStateToProps(state) {
const {websocket: websocketRequest} = state.requests.general;
const networkOnline = getConnection(state);
const webSocketOnline = websocketRequest.status === RequestStatus.SUCCESS;
let failed = state.views.channel.retryFailed && webSocketOnline;
if (!networkOnline) {
failed = false;
}
return {
failed
};
}
export default connect(mapStateToProps)(RetryBarIndicator);

View file

@ -0,0 +1,65 @@
// 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 {
Animated,
StyleSheet
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
const {View: AnimatedView} = Animated;
export default class RetryBarIndicator extends PureComponent {
static propTypes = {
failed: PropTypes.bool
};
state = {
retryMessageHeight: new Animated.Value(0)
};
componentWillReceiveProps(nextProps) {
if (this.props.failed !== nextProps.failed) {
this.toggleRetryMessage(nextProps.failed);
}
}
toggleRetryMessage = (show = true) => {
const value = show ? 38 : 0;
Animated.timing(this.state.retryMessageHeight, {
toValue: value,
duration: 350
}).start();
};
render() {
const {retryMessageHeight} = this.state;
const refreshIndicatorDimensions = {
height: retryMessageHeight
};
return (
<AnimatedView style={[style.refreshIndicator, refreshIndicatorDimensions]}>
<FormattedText
id='mobile.retry_message'
defaultMessage='Refreshing messages failed. Pull up to try again.'
style={{color: 'white', flex: 1, fontSize: 12}}
/>
</AnimatedView>
);
}
}
const style = StyleSheet.create({
refreshIndicator: {
alignItems: 'center',
backgroundColor: '#fb8000',
flexDirection: 'row',
paddingHorizontal: 10,
position: 'absolute',
top: 0,
overflow: 'hidden',
width: '100%'
}
});

View file

@ -10,7 +10,6 @@ import {
View
} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ChannelDrawer from 'app/components/channel_drawer';
@ -49,8 +48,7 @@ class Channel extends PureComponent {
currentTeamId: PropTypes.string,
currentChannelId: PropTypes.string,
theme: PropTypes.object.isRequired,
webSocketRequest: PropTypes.object,
channelsRequestStatus: PropTypes.string
channelsRequestFailed: PropTypes.bool
};
componentWillMount() {
@ -64,14 +62,6 @@ class Channel extends PureComponent {
}
componentDidMount() {
const {startPeriodicStatusUpdates} = this.props.actions;
try {
startPeriodicStatusUpdates();
} catch (error) {
// We don't care about the error
}
// Mark current channel as read when opening app while logged in
if (this.props.currentChannelId) {
this.props.actions.markChannelAsRead(this.props.currentChannelId);
@ -126,19 +116,21 @@ class Channel extends PureComponent {
});
handleConnectionChange = (isConnected) => {
const {actions, webSocketRequest} = this.props;
const {closeWebSocket, connection, initWebSocket} = actions;
const {actions} = this.props;
const {
closeWebSocket,
connection,
initWebSocket,
startPeriodicStatusUpdates,
stopPeriodicStatusUpdates
} = actions;
if (isConnected) {
if (!webSocketRequest || webSocketRequest.status === RequestStatus.NOT_STARTED) {
try {
initWebSocket(Platform.OS);
} catch (error) {
// We don't care if it fails
}
}
initWebSocket(Platform.OS);
startPeriodicStatusUpdates();
} else {
closeWebSocket(true);
stopPeriodicStatusUpdates();
}
connection(isConnected);
};
@ -168,7 +160,7 @@ class Channel extends PureComponent {
render() {
const {
channelsRequestStatus,
channelsRequestFailed,
currentChannelId,
intl,
navigator,
@ -178,7 +170,7 @@ class Channel extends PureComponent {
const style = getStyleFromTheme(theme);
if (!currentChannelId) {
if (channelsRequestStatus === RequestStatus.FAILURE) {
if (channelsRequestFailed) {
return (
<PostListRetry
retry={this.retryLoadChannels}
@ -204,10 +196,11 @@ class Channel extends PureComponent {
<OfflineIndicator/>
<View style={style.header}>
<ChannelDrawerButton/>
<ChannelTitle
onPress={this.goToChannelInfo}
<ChannelTitle onPress={this.goToChannelInfo}/>
<ChannelSearchButton
navigator={navigator}
theme={theme}
/>
<ChannelSearchButton navigator={navigator}/>
</View>
</View>
<KeyboardLayout
@ -215,16 +208,11 @@ class Channel extends PureComponent {
style={style.keyboardLayout}
>
<View style={style.postList}>
<ChannelPostList
channelId={currentChannelId}
navigator={navigator}
/>
<ChannelPostList navigator={navigator}/>
</View>
<ChannelLoader theme={theme}/>
<PostTextbox
ref={this.attachPostTextbox}
onChangeText={this.handleDraftChanged}
channelId={currentChannelId}
navigator={navigator}
/>
</KeyboardLayout>

View file

@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Animated,
Platform,
StyleSheet,
View
@ -13,11 +12,9 @@ import {
import {General} from 'mattermost-redux/constants';
import FormattedText from 'app/components/formatted_text';
import PostList from 'app/components/post_list';
import PostListRetry from 'app/components/post_list_retry';
const {View: AnimatedView} = Animated;
import RetryBarIndicator from 'app/components/retry_bar_indicator';
class ChannelPostList extends PureComponent {
static propTypes = {
@ -30,13 +27,11 @@ class ChannelPostList extends PureComponent {
}).isRequired,
channelDisplayName: PropTypes.string,
channelId: PropTypes.string.isRequired,
channelIsRefreshing: PropTypes.bool,
channelRefreshingFailed: PropTypes.bool,
channelType: PropTypes.string,
currentUserId: PropTypes.string,
intl: intlShape.isRequired,
lastViewedAt: PropTypes.number,
loadingPosts: PropTypes.bool,
navigator: PropTypes.object,
posts: PropTypes.array.isRequired,
postVisibility: PropTypes.number,
@ -45,8 +40,6 @@ class ChannelPostList extends PureComponent {
};
static defaultProps = {
posts: [],
loadingPosts: false,
postVisibility: 15
};
@ -54,29 +47,18 @@ class ChannelPostList extends PureComponent {
super(props);
this.state = {
retryMessageHeight: new Animated.Value(0),
visiblePosts: this.getVisiblePosts(props),
showLoadMore: props.posts.length >= props.postVisibility
};
}
componentDidMount() {
this.mounted = true;
}
componentWillReceiveProps(nextProps) {
const {channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps;
const {posts: nextPosts} = nextProps;
if (nextChannelRefreshingFailed && nextPosts.length) {
this.toggleRetryMessage();
} else if (!nextChannelRefreshingFailed || !nextPosts.length) {
this.toggleRetryMessage(false);
}
const showLoadMore = nextProps.posts.length >= nextProps.postVisibility;
const showLoadMore = nextPosts.length >= nextProps.postVisibility;
let visiblePosts = this.state.visiblePosts;
if (nextProps.posts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) {
if (nextPosts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) {
visiblePosts = this.getVisiblePosts(nextProps);
}
@ -86,22 +68,10 @@ class ChannelPostList extends PureComponent {
});
}
componentWillUnmount() {
this.mounted = false;
}
getVisiblePosts = (props) => {
return props.posts.slice(0, props.postVisibility);
};
toggleRetryMessage = (show = true) => {
const value = show ? 38 : 0;
Animated.timing(this.state.retryMessageHeight, {
toValue: value,
duration: 350
}).start();
};
goToThread = (post) => {
const {actions, channelId, channelDisplayName, channelType, intl, navigator, theme} = this.props;
const rootId = (post.root_id || post.id);
@ -156,18 +126,15 @@ class ChannelPostList extends PureComponent {
const {
actions,
channelId,
channelIsRefreshing,
channelRefreshingFailed,
currentUserId,
lastViewedAt,
loadingPosts,
navigator,
posts,
theme
} = this.props;
const {
retryMessageHeight,
showLoadMore,
visiblePosts
} = this.state;
@ -185,7 +152,6 @@ class ChannelPostList extends PureComponent {
<PostList
posts={visiblePosts}
loadMore={this.loadMorePosts}
isLoadingMore={loadingPosts}
showLoadMore={showLoadMore}
onPostPress={this.goToThread}
onRefresh={actions.setChannelRefreshing}
@ -195,25 +161,14 @@ class ChannelPostList extends PureComponent {
lastViewedAt={lastViewedAt}
channelId={channelId}
navigator={navigator}
refreshing={channelIsRefreshing}
/>
);
}
const refreshIndicatorDimensions = {
height: retryMessageHeight
};
return (
<View style={style.container}>
{component}
<AnimatedView style={[style.refreshIndicator, refreshIndicatorDimensions]}>
<FormattedText
id='mobile.retry_message'
defaultMessage='Refreshing messages failed. Pull up to try again.'
style={{color: 'white', flex: 1, fontSize: 12}}
/>
</AnimatedView>
<RetryBarIndicator/>
</View>
);
}
@ -222,16 +177,6 @@ class ChannelPostList extends PureComponent {
const style = StyleSheet.create({
container: {
flex: 1
},
refreshIndicator: {
alignItems: 'center',
backgroundColor: '#fb8000',
flexDirection: 'row',
paddingHorizontal: 10,
position: 'absolute',
top: 0,
overflow: 'hidden',
width: '100%'
}
});

View file

@ -7,10 +7,9 @@ import {connect} from 'react-redux';
import {selectPost} from 'mattermost-redux/actions/posts';
import {RequestStatus} from 'mattermost-redux/constants';
import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts';
import {getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentChannelId, getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
import {getConnection} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import ChannelPostList from './channel_post_list';
@ -19,45 +18,23 @@ function makeMapStateToProps() {
const getChannel = makeGetChannel();
const getPostsInChannel = makeGetPostsInChannel();
return function mapStateToProps(state, ownProps) {
const channelId = ownProps.channelId;
const {getPosts, getPostsRetryAttempts, getPostsSince, getPostsSinceRetryAttempts} = state.requests.posts;
return function mapStateToProps(state) {
const channelId = getCurrentChannelId(state);
const posts = getPostsInChannel(state, channelId) || [];
const {websocket: websocketRequest} = state.requests.general;
const networkOnline = getConnection(state);
const webSocketOnline = websocketRequest.status === RequestStatus.SUCCESS;
let getPostsStatus;
if (getPostsRetryAttempts > 0) {
getPostsStatus = getPosts.status;
} else if (getPostsSinceRetryAttempts > 1) {
getPostsStatus = getPostsSince.status;
}
let channelIsRefreshing = state.views.channel.refreshing;
let channelRefreshingFailed = getPostsStatus === RequestStatus.FAILURE && webSocketOnline;
if (!networkOnline) {
channelIsRefreshing = false;
channelRefreshingFailed = false;
}
const channelRefreshingFailed = state.views.channel.retryFailed;
const channel = getChannel(state, {id: channelId}) || {};
return {
channelId,
channelIsRefreshing,
channelRefreshingFailed,
currentUserId: getCurrentUserId(state),
channelType: channel.type,
channelDisplayName: channel.display_name,
posts,
postVisibility: state.views.channel.postVisibility[channelId],
loadingPosts: state.views.channel.loadingPosts[channelId],
lastViewedAt: getMyCurrentChannelMembership(state).last_viewed_at,
networkOnline,
totalMessageCount: channel.total_msg_count,
theme: getTheme(state),
...ownProps
theme: getTheme(state)
};
};
}
@ -74,4 +51,54 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(ChannelPostList);
function areStatesEqual(next, prev) {
const nextChannelId = next.entities.channels.currentChannelId;
const prevChannelId = prev.entities.channels.currentChannelId;
if (prevChannelId !== nextChannelId) {
return false;
}
const nextVisible = next.views.channel.postVisibility[nextChannelId];
const prevVisible = prev.views.channel.postVisibility[nextChannelId];
const nextPostsIds = next.entities.posts.postsInChannel[nextChannelId];
const prevPostsIds = prev.entities.posts.postsInChannel[nextChannelId];
// When we don't have posts and we failed to get the post after tha max retry attempts
const prevRetryFailed = prev.views.channel.retryFailed;
const nextRetryFailed = next.views.channel.retryFailed;
if (prevRetryFailed !== nextRetryFailed && nextRetryFailed && !nextPostsIds) {
return false;
}
// if we don't have post and visibility is not set we don't need to re-render
if (!nextPostsIds || !nextVisible || !prevVisible) {
return true;
}
const nextVisiblePostsIds = nextPostsIds.slice(0, nextVisible);
const prevVisiblePostsIds = prevPostsIds ? prevPostsIds.slice(0, prevVisible) : [];
// if we have a different amount of post we should re-render
if (nextVisiblePostsIds.length !== prevVisiblePostsIds.length) {
return false;
}
const {status: nextStatus} = next.requests.posts.getPosts;
const {status: prevStatus} = prev.requests.posts.getPosts;
// if we are requesting post for the first time we should re-render
if (prevStatus === RequestStatus.STARTED && nextStatus === RequestStatus.SUCCESS &&
nextPostsIds.length !== prevPostsIds.length) {
return false;
}
// if at least one post id changed we need to re-render
for (let i = 0; i <= nextVisiblePostsIds.length; i++) {
if (nextVisiblePostsIds[i] && nextVisiblePostsIds[i] !== prevVisiblePostsIds[i]) {
return false;
}
}
return true;
}
export default connect(makeMapStateToProps, mapDispatchToProps, null, {pure: true, areStatesEqual})(ChannelPostList);

View file

@ -6,8 +6,6 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
StyleSheet,
TouchableOpacity,
@ -15,15 +13,11 @@ import {
} from 'react-native';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {clearSearch} from 'mattermost-redux/actions/search';
import {handlePostDraftChanged} from 'app/actions/views/channel';
import {getTheme} from 'app/selectors/preferences';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
const SEARCH = 'search';
class ChannelSearchButton extends PureComponent {
export default class ChannelSearchButton extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearSearch: PropTypes.func.isRequired,
@ -33,7 +27,7 @@ class ChannelSearchButton extends PureComponent {
theme: PropTypes.object
};
handlePress = async () => {
handlePress = wrapWithPreventDoubleTap(async () => {
const {actions, navigator, theme} = this.props;
await actions.clearSearch();
@ -52,7 +46,7 @@ class ChannelSearchButton extends PureComponent {
theme
}
});
};
});
render() {
const {
@ -61,7 +55,7 @@ class ChannelSearchButton extends PureComponent {
return (
<TouchableOpacity
onPress={() => preventDoubleTap(this.handlePress, this)}
onPress={this.handlePress}
style={style.container}
>
<View style={style.wrapper}>
@ -89,21 +83,3 @@ const style = StyleSheet.create({
zIndex: 30
}
});
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
...ownProps
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearSearch,
handlePostDraftChanged
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelSearchButton);

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {clearSearch} from 'mattermost-redux/actions/search';
import {handlePostDraftChanged} from 'app/actions/views/channel';
import ChannelSearchButton from './channel_search_button';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearSearch,
handlePostDraftChanged
}, dispatch)
};
}
export default connect(null, mapDispatchToProps)(ChannelSearchButton);

View file

@ -1,75 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
function ChannelTitle(props) {
const channelName = props.displayName || props.currentChannel.display_name;
let icon;
if (channelName) {
icon = (
<Icon
style={{marginHorizontal: 6}}
size={12}
name='chevron-down'
color={props.theme.sidebarHeaderTextColor}
/>
);
}
return (
<TouchableOpacity
style={{flexDirection: 'row', flex: 1}}
onPress={props.onPress}
>
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginHorizontal: 15}}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={{
color: props.theme.sidebarHeaderTextColor,
fontSize: 15,
fontWeight: 'bold'}}
>
{channelName}
</Text>
{icon}
</View>
</TouchableOpacity>
);
}
ChannelTitle.propTypes = {
currentChannel: PropTypes.object,
displayName: PropTypes.string,
onPress: PropTypes.func,
theme: PropTypes.object
};
ChannelTitle.defaultProps = {
currentChannel: {},
displayName: null,
theme: {}
};
function mapStateToProps(state) {
return {
currentChannel: getCurrentChannel(state) || {},
displayName: state.views.channel.displayName,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelTitle);

View file

@ -0,0 +1,88 @@
// 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 {
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelTitle extends PureComponent {
static propTypes = {
currentChannelName: PropTypes.string,
displayName: PropTypes.string,
onPress: PropTypes.func,
theme: PropTypes.object
};
static defaultProps = {
currentChannel: {},
displayName: null,
theme: {}
};
render() {
const {currentChannelName, displayName, onPress, theme} = this.props;
const channelName = displayName || currentChannelName;
const style = getStyle(theme);
let icon;
if (channelName) {
icon = (
<Icon
style={style.icon}
size={12}
name='chevron-down'
/>
);
}
return (
<TouchableOpacity
style={style.container}
onPress={onPress}
>
<View style={style.wrapper}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.text}
>
{channelName}
</Text>
{icon}
</View>
</TouchableOpacity>
);
}
}
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row'
},
wrapper: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
marginHorizontal: 15
},
icon: {
color: theme.sidebarHeaderTextColor,
marginHorizontal: 6
},
text: {
color: theme.sidebarHeaderTextColor,
fontSize: 17,
fontWeight: 'bold'
}
};
});

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
import ChannelTitle from './channel_title';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
return {
currentChannelName: currentChannel ? currentChannel.display_name : '',
displayName: state.views.channel.displayName,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelTitle);

View file

@ -4,6 +4,13 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users';
import {init as initWebSocket, close as closeWebSocket} from 'mattermost-redux/actions/websocket';
import {RequestStatus} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar,
@ -13,29 +20,16 @@ import {connection} from 'app/actions/device';
import {selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {getTheme} from 'app/selectors/preferences';
import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
init as initWebSocket,
close as closeWebSocket
} from 'mattermost-redux/actions/websocket';
import Channel from './channel';
function mapStateToProps(state, ownProps) {
const {websocket} = state.requests.general;
function mapStateToProps(state) {
const {myChannels: channelsRequest} = state.requests.channels;
return {
...ownProps,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
theme: getTheme(state),
webSocketRequest: websocket,
channelsRequestStatus: channelsRequest.status
channelsRequestFailed: channelsRequest.status === RequestStatus.FAILURE
};
}
@ -57,4 +51,27 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Channel);
function areStatesEqual(next, prev) {
// When switching teams
if (next.entities.teams.currentTeamId !== prev.entities.teams.currentTeamId) {
return false;
}
// When we have a new channel after switching teams
const prevChannelId = prev.entities.channels.currentChannelId;
const nextChannelId = next.entities.channels.currentChannelId;
if (nextChannelId !== prevChannelId) {
return false;
}
// When getting the channels for a team and the request fails
const prevStatus = prev.requests.channels.myChannels.status;
const nextStatus = next.requests.channels.myChannels.status;
if (!nextChannelId && prevStatus === RequestStatus.STARTED && nextStatus === RequestStatus.FAILURE) {
return false;
}
return true;
}
export default connect(mapStateToProps, mapDispatchToProps, null, {pure: true, areStatesEqual})(Channel);

View file

@ -3878,7 +3878,7 @@ makeerror@1.0.x:
mattermost-redux@mattermost/mattermost-redux#master:
version "0.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/85db14af9a6d8c402213b0eb3c372f1ac6adad34"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/6932c3fd4064434c5b30cff9898d724cdc687648"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"