diff --git a/app/components/inverted_flat_list/index.js b/app/components/inverted_flat_list/index.js index d6e379efd..f07f8fc7d 100644 --- a/app/components/inverted_flat_list/index.js +++ b/app/components/inverted_flat_list/index.js @@ -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 { + + + + + ); body = ({messageComponent}); } else if (message.length) { diff --git a/app/components/post_body_additional_content/index.js b/app/components/post_body_additional_content/index.js index 4229087a3..1921d14c0 100644 --- a/app/components/post_body_additional_content/index.js +++ b/app/components/post_body_additional_content/index.js @@ -41,7 +41,6 @@ function makeMapStateToProps() { const link = getFirstLink(ownProps.message); return { - ...ownProps, ...getDimensions(state), config, link, diff --git a/app/components/post_body_additional_content/post_body_additional_content.js b/app/components/post_body_additional_content/post_body_additional_content.js index b92e1c0cf..79e228a79 100644 --- a/app/components/post_body_additional_content/post_body_additional_content.js +++ b/app/components/post_body_additional_content/post_body_additional_content.js @@ -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 { ); } @@ -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; } } diff --git a/app/components/post_list/index.js b/app/components/post_list/index.js index 52601c54c..d9b3d664f 100644 --- a/app/components/post_list/index.js +++ b/app/components/post_list/index.js @@ -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) }; } diff --git a/app/components/post_list/load_more_posts.js b/app/components/post_list/load_more_posts.js index 354194fe7..693904d60 100644 --- a/app/components/post_list/load_more_posts.js +++ b/app/components/post_list/load_more_posts.js @@ -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); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index 7cd51c661..e068db993 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -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 ( @@ -124,10 +123,7 @@ export default class PostList extends PureComponent { } if (item === LOAD_MORE_POSTS && this.props.showLoadMore) { return ( - + ); } @@ -149,7 +145,8 @@ export default class PostList extends PureComponent { return ( + ); + } +} diff --git a/app/components/retry_bar_indicator/index.js b/app/components/retry_bar_indicator/index.js new file mode 100644 index 000000000..b6c0c247f --- /dev/null +++ b/app/components/retry_bar_indicator/index.js @@ -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); diff --git a/app/components/retry_bar_indicator/retry_bar_indicator.js b/app/components/retry_bar_indicator/retry_bar_indicator.js new file mode 100644 index 000000000..31f632d94 --- /dev/null +++ b/app/components/retry_bar_indicator/retry_bar_indicator.js @@ -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 ( + + + + ); + } +} + +const style = StyleSheet.create({ + refreshIndicator: { + alignItems: 'center', + backgroundColor: '#fb8000', + flexDirection: 'row', + paddingHorizontal: 10, + position: 'absolute', + top: 0, + overflow: 'hidden', + width: '100%' + } +}); diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index 34d126317..1c238e7c3 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -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 ( - + - - + diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 4bda2925e..a156315db 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -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 { ); } - const refreshIndicatorDimensions = { - height: retryMessageHeight - }; - return ( {component} - - - + ); } @@ -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%' } }); diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index f8a5d4f7f..811536d2a 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -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); diff --git a/app/screens/channel/channel_search_button.js b/app/screens/channel/channel_search_button/channel_search_button.js similarity index 70% rename from app/screens/channel/channel_search_button.js rename to app/screens/channel/channel_search_button/channel_search_button.js index ec9615eaa..6a1cb3506 100644 --- a/app/screens/channel/channel_search_button.js +++ b/app/screens/channel/channel_search_button/channel_search_button.js @@ -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 ( preventDoubleTap(this.handlePress, this)} + onPress={this.handlePress} style={style.container} > @@ -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); diff --git a/app/screens/channel/channel_search_button/index.js b/app/screens/channel/channel_search_button/index.js new file mode 100644 index 000000000..7e8c1033e --- /dev/null +++ b/app/screens/channel/channel_search_button/index.js @@ -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); diff --git a/app/screens/channel/channel_title.js b/app/screens/channel/channel_title.js deleted file mode 100644 index 6a35fca16..000000000 --- a/app/screens/channel/channel_title.js +++ /dev/null @@ -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 = ( - - ); - } - - return ( - - - - {channelName} - - {icon} - - - ); -} - -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); diff --git a/app/screens/channel/channel_title/channel_title.js b/app/screens/channel/channel_title/channel_title.js new file mode 100644 index 000000000..c2c2526cd --- /dev/null +++ b/app/screens/channel/channel_title/channel_title.js @@ -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 = ( + + ); + } + + return ( + + + + {channelName} + + {icon} + + + ); + } +} + +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' + } + }; +}); diff --git a/app/screens/channel/channel_title/index.js b/app/screens/channel/channel_title/index.js new file mode 100644 index 000000000..beed6c218 --- /dev/null +++ b/app/screens/channel/channel_title/index.js @@ -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); diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 39eed76b2..3a14c106e 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -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); diff --git a/yarn.lock b/yarn.lock index 78a6c62ab..c6fb464d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"