diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index 38ef25953..4b67a273f 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -7,6 +7,7 @@ import { BackHandler, InteractionManager, Keyboard, + StyleSheet, View } from 'react-native'; @@ -35,23 +36,15 @@ export default class ChannelDrawer extends PureComponent { }).isRequired, blurPostTextBox: PropTypes.func.isRequired, children: PropTypes.node, - channels: PropTypes.object, - currentChannel: PropTypes.object, - currentDisplayName: PropTypes.string, - channelMembers: PropTypes.object, - currentTeam: PropTypes.object, + currentChannelId: PropTypes.string.isRequired, + currentTeamId: PropTypes.string.isRequired, currentUserId: PropTypes.string.isRequired, intl: PropTypes.object.isRequired, - myTeamMembers: PropTypes.object.isRequired, navigator: PropTypes.object, + teamsCount: PropTypes.number.isRequired, theme: PropTypes.object.isRequired }; - static defaultProps = { - currentTeam: {}, - currentChannel: {} - }; - state = { openDrawer: false, openDrawerOffset: DRAWER_INITIAL_OFFSET @@ -91,37 +84,26 @@ export default class ChannelDrawer extends PureComponent { handleDrawerClose = () => { this.resetDrawer(); - if (this.closeLeftHandle) { - InteractionManager.clearInteractionHandle(this.closeLeftHandle); - this.closeLeftHandle = null; - } - if (this.state.openDrawer) { - this.setState({openDrawer: false}); - } - }; - - handleDrawerCloseStart = () => { - if (!this.closeLeftHandle) { - this.closeLeftHandle = InteractionManager.createInteractionHandle(); + // The state doesn't get updated if you swipe to close + this.setState({ + openDrawer: false + }); } }; handleDrawerOpen = () => { - this.setState({openDrawer: true}); if (this.state.openDrawerOffset === DRAWER_INITIAL_OFFSET) { Keyboard.dismiss(); } - - if (this.openLeftHandle) { - InteractionManager.clearInteractionHandle(this.openLeftHandle); - this.openLeftHandle = null; - } }; handleDrawerOpenStart = () => { - if (!this.openLeftHandle) { - this.openLeftHandle = InteractionManager.createInteractionHandle(); + if (!this.state.openDrawer) { + // The state doesn't get updated if you swipe to open + this.setState({ + openDrawer: true + }); } }; @@ -145,13 +127,16 @@ export default class ChannelDrawer extends PureComponent { openChannelDrawer = () => { this.props.blurPostTextBox(); - this.setState({openDrawer: true}); + + this.setState({ + openDrawer: true + }); }; selectChannel = (channel) => { const { actions, - currentChannel + currentChannelId } = this.props; const { @@ -162,72 +147,61 @@ export default class ChannelDrawer extends PureComponent { viewChannel } = actions; - markChannelAsRead(channel.id, currentChannel.id); setChannelLoading(); - viewChannel(currentChannel.id); setChannelDisplayName(channel.display_name); + handleSelectChannel(channel.id); + this.closeChannelDrawer(); + InteractionManager.runAfterInteractions(() => { - handleSelectChannel(channel.id); + markChannelAsRead(channel.id, currentChannelId); + viewChannel(currentChannelId); }); }; joinChannel = async (channel) => { const { actions, - currentChannel, - currentDisplayName, - currentTeam, + currentTeamId, currentUserId, intl } = this.props; const { - handleSelectChannel, joinChannel, - makeDirectChannel, - markChannelAsRead, - setChannelDisplayName, - setChannelLoading, - viewChannel + makeDirectChannel } = actions; - markChannelAsRead(currentChannel.id); - setChannelLoading(); - viewChannel(currentChannel.id); - setChannelDisplayName(channel.display_name); - const displayValue = {displayName: channel.display_name}; + let result; if (channel.type === General.DM_CHANNEL) { - const result = await makeDirectChannel(channel.id); + result = await makeDirectChannel(channel.id); + if (result.error) { const dmFailedMessage = { id: 'mobile.open_dm.error', defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again." }; - setChannelDisplayName(currentDisplayName); alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue); - } else { - this.closeChannelDrawer(); } } else { - const result = await joinChannel(currentUserId, currentTeam.id, channel.id); + result = await joinChannel(currentUserId, currentTeamId, channel.id); if (result.error) { const joinFailedMessage = { id: 'mobile.join_channel.error', defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again." }; - setChannelDisplayName(currentDisplayName); alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue); - } else { - this.closeChannelDrawer(); - InteractionManager.runAfterInteractions(() => { - handleSelectChannel(channel.id); - }); } } + + if (result.error) { + return; + } + + this.selectChannel(result.data); }; onPageSelected = (index) => { @@ -246,8 +220,7 @@ export default class ChannelDrawer extends PureComponent { }; showTeams = () => { - const teamsCount = Object.keys(this.props.myTeamMembers).length; - if (this.swiperIndex === 1 && teamsCount > 1) { + if (this.swiperIndex === 1 && this.props.teamsCount > 1) { this.refs.swiper.showTeamsPage(); } }; @@ -260,39 +233,32 @@ export default class ChannelDrawer extends PureComponent { renderContent = () => { const { - currentChannel, - currentTeam, - channels, - channelMembers, navigator, - myTeamMembers, + teamsCount, theme } = this.props; - const {openDrawerOffset} = this.state; - const showTeams = openDrawerOffset === DRAWER_INITIAL_OFFSET && Object.keys(myTeamMembers).length > 1; + + const { + openDrawerOffset + } = this.state; + + const showTeams = openDrawerOffset === DRAWER_INITIAL_OFFSET && teamsCount > 1; const teams = ( - + ); const channelsList = ( - + ); - }; + } render() { const {children} = this.props; @@ -324,7 +290,6 @@ export default class ChannelDrawer extends PureComponent { open={openDrawer} onOpenStart={this.handleDrawerOpenStart} onOpen={this.handleDrawerOpen} - onCloseStart={this.handleDrawerCloseStart} onClose={this.handleDrawerClose} captureGestures='open' type='static' @@ -340,7 +305,7 @@ export default class ChannelDrawer extends PureComponent { panThreshold={0.25} acceptPan={true} negotiatePan={true} - useInteractionManager={false} + useInteractionManager={true} tweenDuration={100} tweenHandler={this.handleDrawerTween} elevation={-5} @@ -361,3 +326,10 @@ export default class ChannelDrawer extends PureComponent { ); } } + +const style = StyleSheet.create({ + swiperContent: { + flex: 1, + marginBottom: 10 + } +}); diff --git a/app/components/channel_drawer/channels_list/channels_list.js b/app/components/channel_drawer/channels_list/channels_list.js new file mode 100644 index 000000000..bab3d24ce --- /dev/null +++ b/app/components/channel_drawer/channels_list/channels_list.js @@ -0,0 +1,402 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Platform, + Text, + TouchableHighlight, + View +} from 'react-native'; +import {injectIntl, intlShape} from 'react-intl'; +import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; + +import Badge from 'app/components/badge'; +import SearchBar from 'app/components/search_bar'; +import {preventDoubleTap} from 'app/utils/tap'; +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +import FilteredList from './filtered_list'; +import List from './list'; + +class ChannelsList extends React.PureComponent { + static propTypes = { + channels: PropTypes.object.isRequired, + channelMembers: PropTypes.object, + currentChannel: PropTypes.object, + currentTeam: PropTypes.object.isRequired, + intl: intlShape.isRequired, + myTeamMembers: PropTypes.object.isRequired, + navigator: PropTypes.object, + onJoinChannel: PropTypes.func.isRequired, + onSearchEnds: PropTypes.func.isRequired, + onSearchStart: PropTypes.func.isRequired, + onSelectChannel: PropTypes.func.isRequired, + onShowTeams: PropTypes.func.isRequired, + theme: PropTypes.object.isRequired + }; + + static defaultProps = { + currentTeam: {}, + currentChannel: {} + }; + + constructor(props) { + super(props); + this.firstUnreadChannel = null; + this.state = { + searching: false, + term: '' + }; + + MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => { + this.closeButton = source; + }); + } + + onSelectChannel = (channel) => { + if (channel.fake) { + this.props.onJoinChannel(channel); + } else { + this.props.onSelectChannel(channel); + } + + this.refs.search_bar.cancel(); + }; + + openSettingsModal = () => { + const {intl, navigator, theme} = this.props; + + navigator.showModal({ + screen: 'Settings', + title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}), + animationType: 'slide-up', + animated: true, + backButtonTitle: '', + navigatorStyle: { + navBarTextColor: theme.sidebarHeaderTextColor, + navBarBackgroundColor: theme.sidebarHeaderBg, + navBarButtonColor: theme.sidebarHeaderTextColor, + screenBackgroundColor: theme.centerChannelBg + }, + navigatorButtons: { + leftButtons: [{ + id: 'close-settings', + icon: this.closeButton + }] + } + }); + }; + + onSearch = (term) => { + this.setState({term}); + }; + + onSearchFocused = () => { + this.setState({searching: true}); + this.props.onSearchStart(); + }; + + cancelSearch = () => { + this.props.onSearchEnds(); + this.setState({searching: false}); + this.onSearch(''); + }; + + render() { + const { + currentChannel, + currentTeam, + intl, + myTeamMembers, + onShowTeams, + theme + } = this.props; + + if (!currentChannel) { + return {'Loading'}; + } + + const {searching, term} = this.state; + const teamMembers = Object.values(myTeamMembers); + const showMembers = teamMembers.length > 1; + const styles = getStyleSheet(theme); + + let settings; + let list; + if (searching) { + const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles, term}; + list = ; + } else { + settings = ( + preventDoubleTap(this.openSettingsModal)} + underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} + > + + + ); + + const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles}; + list = ; + } + + const title = ( + + + + ); + + let badge; + let switcher; + if (showMembers && !searching) { + let mentionCount = 0; + let messageCount = 0; + teamMembers.forEach((m) => { + if (m.team_id !== currentTeam.id) { + mentionCount = mentionCount + (m.mention_count || 0); + messageCount = messageCount + (m.msg_count || 0); + } + }); + + let badgeCount = 0; + if (mentionCount) { + badgeCount = mentionCount; + } else if (messageCount) { + badgeCount = -1; + } + + if (badgeCount) { + badge = ( + + ); + } + + switcher = ( + preventDoubleTap(onShowTeams)} + underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} + > + + + + + {currentTeam.display_name.substr(0, 2).toUpperCase()} + + + + ); + } + + return ( + + + + {switcher} + {title} + {settings} + {badge} + + + {list} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + backgroundColor: theme.sidebarBg, + flex: 1 + }, + extraPadding: { + paddingBottom: 5 + }, + statusBar: { + backgroundColor: theme.sidebarHeaderBg, + ...Platform.select({ + ios: { + paddingTop: 20 + } + }) + }, + headerContainer: { + alignItems: 'center', + paddingLeft: 10, + backgroundColor: theme.sidebarHeaderBg, + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10), + ...Platform.select({ + android: { + height: 46 + }, + ios: { + height: 44 + } + }) + }, + header: { + color: theme.sidebarHeaderTextColor, + flex: 1, + fontSize: 17, + fontWeight: 'normal', + paddingLeft: 16 + }, + settingsContainer: { + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 10, + ...Platform.select({ + android: { + height: 46, + marginRight: 6 + }, + ios: { + height: 44, + marginRight: 8 + } + }) + }, + settings: { + color: theme.sidebarHeaderTextColor, + fontSize: 18, + fontWeight: '300' + }, + titleContainer: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + height: 48, + marginLeft: 16 + }, + title: { + flex: 1, + color: theme.sidebarText, + opacity: 1, + fontSize: 15, + fontWeight: '400', + letterSpacing: 0.8, + lineHeight: 18 + }, + searchContainer: { + flex: 1, + ...Platform.select({ + android: { + marginBottom: 1 + }, + ios: { + marginBottom: 3 + } + }) + }, + switcherContainer: { + alignItems: 'center', + backgroundColor: theme.sidebarHeaderTextColor, + borderRadius: 2, + flexDirection: 'row', + height: 32, + justifyContent: 'center', + marginLeft: 6, + marginRight: 10, + paddingHorizontal: 6 + }, + switcherDivider: { + backgroundColor: theme.sidebarHeaderBg, + height: 15, + marginHorizontal: 6, + width: 1 + }, + switcherTeam: { + color: theme.sidebarHeaderBg, + fontFamily: 'OpenSans', + fontSize: 14 + }, + badge: { + backgroundColor: theme.mentionBj, + borderColor: theme.sidebarHeaderBg, + borderRadius: 10, + borderWidth: 1, + flexDirection: 'row', + padding: 3, + position: 'absolute', + left: 5, + top: 0 + }, + mention: { + color: theme.mentionColor, + fontSize: 10 + }, + divider: { + backgroundColor: changeOpacity(theme.sidebarText, 0.1), + height: 1 + }, + actionContainer: { + alignItems: 'center', + height: 48, + justifyContent: 'center', + width: 50 + }, + action: { + color: theme.sidebarText, + fontSize: 20, + fontWeight: '500', + lineHeight: 18 + }, + above: { + backgroundColor: theme.mentionBj, + top: 9 + }, + indicatorText: { + backgroundColor: 'transparent', + color: theme.mentionColor, + fontSize: 14, + paddingVertical: 2, + paddingHorizontal: 4, + textAlign: 'center', + textAlignVertical: 'center' + } + }; +}); + +export default injectIntl(ChannelsList); diff --git a/app/components/channel_drawer/channels_list/index.js b/app/components/channel_drawer/channels_list/index.js index 46926104a..12eec8483 100644 --- a/app/components/channel_drawer/channels_list/index.js +++ b/app/components/channel_drawer/channels_list/index.js @@ -1,402 +1,25 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import { - Platform, - Text, - TouchableHighlight, - View -} from 'react-native'; -import {injectIntl, intlShape} from 'react-intl'; -import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; -import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import {connect} from 'react-redux'; -import Badge from 'app/components/badge'; -import SearchBar from 'app/components/search_bar'; -import {preventDoubleTap} from 'app/utils/tap'; -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {getTheme} from 'app/selectors/preferences'; -import FilteredList from './filtered_list'; -import List from './list'; +import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; -class ChannelsList extends Component { - static propTypes = { - channels: PropTypes.object.isRequired, - channelMembers: PropTypes.object, - currentTeam: PropTypes.object.isRequired, - currentChannel: PropTypes.object, - intl: intlShape.isRequired, - myTeamMembers: PropTypes.object.isRequired, - navigator: PropTypes.object, - onJoinChannel: PropTypes.func.isRequired, - onSearchEnds: PropTypes.func.isRequired, - onSearchStart: PropTypes.func.isRequired, - onSelectChannel: PropTypes.func.isRequired, - onShowTeams: PropTypes.func.isRequired, - theme: PropTypes.object.isRequired +import ChannelsList from './channels_list'; + +function mapStateToProps(state, ownProps) { + return { + ...ownProps, + channels: getChannelsWithUnreadSection(state), + channelMembers: getMyChannelMemberships(state), + currentChannel: getCurrentChannel(state), + currentTeam: getCurrentTeam(state), + myTeamMembers: getTeamMemberships(state), + theme: getTheme(state) }; - - static defaultProps = { - currentTeam: {}, - currentChannel: {} - }; - - constructor(props) { - super(props); - this.firstUnreadChannel = null; - this.state = { - searching: false, - term: '' - }; - - MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => { - this.closeButton = source; - }); - } - - onSelectChannel = (channel) => { - if (channel.fake) { - this.props.onJoinChannel(channel); - } else { - this.props.onSelectChannel(channel); - } - - this.refs.search_bar.cancel(); - }; - - openSettingsModal = () => { - const {intl, navigator, theme} = this.props; - - navigator.showModal({ - screen: 'Settings', - title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}), - animationType: 'slide-up', - animated: true, - backButtonTitle: '', - navigatorStyle: { - navBarTextColor: theme.sidebarHeaderTextColor, - navBarBackgroundColor: theme.sidebarHeaderBg, - navBarButtonColor: theme.sidebarHeaderTextColor, - screenBackgroundColor: theme.centerChannelBg - }, - navigatorButtons: { - leftButtons: [{ - id: 'close-settings', - icon: this.closeButton - }] - } - }); - }; - - onSearch = (term) => { - this.setState({term}); - }; - - onSearchFocused = () => { - this.setState({searching: true}); - this.props.onSearchStart(); - }; - - cancelSearch = () => { - this.props.onSearchEnds(); - this.setState({searching: false}); - this.onSearch(''); - }; - - render() { - const { - currentChannel, - currentTeam, - intl, - myTeamMembers, - onShowTeams, - theme - } = this.props; - - if (!currentChannel) { - return {'Loading'}; - } - - const {searching, term} = this.state; - const teamMembers = Object.values(myTeamMembers); - const showMembers = teamMembers.length > 1; - const styles = getStyleSheet(theme); - - let settings; - let list; - if (searching) { - const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles, term}; - list = ; - } else { - settings = ( - preventDoubleTap(this.openSettingsModal)} - underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} - > - - - ); - - const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles}; - list = ; - } - - const title = ( - - - - ); - - let badge; - let switcher; - if (showMembers && !searching) { - let mentionCount = 0; - let messageCount = 0; - teamMembers.forEach((m) => { - if (m.team_id !== currentTeam.id) { - mentionCount = mentionCount + (m.mention_count || 0); - messageCount = messageCount + (m.msg_count || 0); - } - }); - - let badgeCount = 0; - if (mentionCount) { - badgeCount = mentionCount; - } else if (messageCount) { - badgeCount = -1; - } - - if (badgeCount) { - badge = ( - - ); - } - - switcher = ( - preventDoubleTap(onShowTeams)} - underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} - > - - - - - {currentTeam.display_name.substr(0, 2).toUpperCase()} - - - - ); - } - - return ( - - - - {switcher} - {title} - {settings} - {badge} - - - {list} - - ); - } } -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - container: { - backgroundColor: theme.sidebarBg, - flex: 1 - }, - extraPadding: { - paddingBottom: 5 - }, - statusBar: { - backgroundColor: theme.sidebarHeaderBg, - ...Platform.select({ - ios: { - paddingTop: 20 - } - }) - }, - headerContainer: { - alignItems: 'center', - paddingLeft: 10, - backgroundColor: theme.sidebarHeaderBg, - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10), - ...Platform.select({ - android: { - height: 46 - }, - ios: { - height: 44 - } - }) - }, - header: { - color: theme.sidebarHeaderTextColor, - flex: 1, - fontSize: 17, - fontWeight: 'normal', - paddingLeft: 16 - }, - settingsContainer: { - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 10, - ...Platform.select({ - android: { - height: 46, - marginRight: 6 - }, - ios: { - height: 44, - marginRight: 8 - } - }) - }, - settings: { - color: theme.sidebarHeaderTextColor, - fontSize: 18, - fontWeight: '300' - }, - titleContainer: { - alignItems: 'center', - flex: 1, - flexDirection: 'row', - height: 48, - marginLeft: 16 - }, - title: { - flex: 1, - color: theme.sidebarText, - opacity: 1, - fontSize: 15, - fontWeight: '400', - letterSpacing: 0.8, - lineHeight: 18 - }, - searchContainer: { - flex: 1, - ...Platform.select({ - android: { - marginBottom: 1 - }, - ios: { - marginBottom: 3 - } - }) - }, - switcherContainer: { - alignItems: 'center', - backgroundColor: theme.sidebarHeaderTextColor, - borderRadius: 2, - flexDirection: 'row', - height: 32, - justifyContent: 'center', - marginLeft: 6, - marginRight: 10, - paddingHorizontal: 6 - }, - switcherDivider: { - backgroundColor: theme.sidebarHeaderBg, - height: 15, - marginHorizontal: 6, - width: 1 - }, - switcherTeam: { - color: theme.sidebarHeaderBg, - fontFamily: 'OpenSans', - fontSize: 14 - }, - badge: { - backgroundColor: theme.mentionBj, - borderColor: theme.sidebarHeaderBg, - borderRadius: 10, - borderWidth: 1, - flexDirection: 'row', - padding: 3, - position: 'absolute', - left: 5, - top: 0 - }, - mention: { - color: theme.mentionColor, - fontSize: 10 - }, - divider: { - backgroundColor: changeOpacity(theme.sidebarText, 0.1), - height: 1 - }, - actionContainer: { - alignItems: 'center', - height: 48, - justifyContent: 'center', - width: 50 - }, - action: { - color: theme.sidebarText, - fontSize: 20, - fontWeight: '500', - lineHeight: 18 - }, - above: { - backgroundColor: theme.mentionBj, - top: 9 - }, - indicatorText: { - backgroundColor: 'transparent', - color: theme.mentionColor, - fontSize: 14, - paddingVertical: 2, - paddingHorizontal: 4, - textAlign: 'center', - textAlignVertical: 'center' - } - }; -}); - -export default injectIntl(ChannelsList); +export default connect(mapStateToProps)(ChannelsList); diff --git a/app/components/channel_drawer/index.js b/app/components/channel_drawer/index.js index dfaa39a4b..97da0ed02 100644 --- a/app/components/channel_drawer/index.js +++ b/app/components/channel_drawer/index.js @@ -6,8 +6,8 @@ import {connect} from 'react-redux'; import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels'; import {getTeams} from 'mattermost-redux/actions/teams'; -import {getChannelsWithUnreadSection, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel'; import {makeDirectChannel} from 'app/actions/views/more_dms'; @@ -20,13 +20,10 @@ function mapStateToProps(state, ownProps) { return { ...ownProps, - currentTeam: getCurrentTeam(state) || {}, - currentChannel: getCurrentChannel(state) || {}, - currentDisplayName: state.views.channel.displayName, + currentTeamId: getCurrentTeamId(state), + currentChannelId: getCurrentChannelId(state), currentUserId, - channels: getChannelsWithUnreadSection(state), - channelMembers: state.entities.channels.myMembers, - myTeamMembers: getTeamMemberships(state), + teamsCount: Object.keys(getTeamMemberships(state)).length, theme: getTheme(state) }; } diff --git a/app/components/channel_drawer/teams_list/index.js b/app/components/channel_drawer/teams_list/index.js index 7f75b0eae..890cff28d 100644 --- a/app/components/channel_drawer/teams_list/index.js +++ b/app/components/channel_drawer/teams_list/index.js @@ -7,7 +7,7 @@ import {connect} from 'react-redux'; import {markChannelAsRead} from 'mattermost-redux/actions/channels'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; -import {getCurrentTeamId, getJoinableTeams, getMyTeams} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentTeamId, getJoinableTeams, getMyTeams, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; import {handleTeamChange} from 'app/actions/views/select_team'; @@ -35,6 +35,7 @@ function mapStateToProps(state, ownProps) { currentUrl: removeProtocol(getCurrentUrl(state)), teams: getMyTeams(state).sort(sortTeams.bind(null, (user.locale))), theme: getTheme(state), + myTeamMembers: getTeamMemberships(state), ...ownProps }; } diff --git a/app/components/channel_drawer/teams_list/teams_list.js b/app/components/channel_drawer/teams_list/teams_list.js index b45731aad..8a8ab233d 100644 --- a/app/components/channel_drawer/teams_list/teams_list.js +++ b/app/components/channel_drawer/teams_list/teams_list.js @@ -52,12 +52,12 @@ class TeamsList extends PureComponent { if (team.id === currentTeamId) { closeChannelDrawer(); } else { - if (currentChannelId) { - actions.markChannelAsRead(currentChannelId); - } + actions.handleTeamChange(team); + closeChannelDrawer(); + InteractionManager.runAfterInteractions(() => { - actions.handleTeamChange(team); + actions.markChannelAsRead(currentChannelId); }); } }; diff --git a/app/components/formatted_date.js b/app/components/formatted_date.js index 3e8bce8ea..0e9c58b1a 100644 --- a/app/components/formatted_date.js +++ b/app/components/formatted_date.js @@ -1,12 +1,12 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import {Text} from 'react-native'; -class FormattedDate extends Component { +class FormattedDate extends React.PureComponent { static propTypes = { intl: intlShape.isRequired, value: PropTypes.any.isRequired, diff --git a/app/components/formatted_text.js b/app/components/formatted_text.js index 5f21c07ce..37f6e53c1 100644 --- a/app/components/formatted_text.js +++ b/app/components/formatted_text.js @@ -1,12 +1,12 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {Component, createElement, isValidElement} from 'react'; +import React, {createElement, isValidElement} from 'react'; import PropTypes from 'prop-types'; import {Text} from 'react-native'; import {injectIntl, intlShape} from 'react-intl'; -class FormattedText extends Component { +class FormattedText extends React.PureComponent { static propTypes = { intl: intlShape.isRequired, id: PropTypes.string.isRequired, diff --git a/app/components/formatted_time.js b/app/components/formatted_time.js index 6fa9f1d35..664241e96 100644 --- a/app/components/formatted_time.js +++ b/app/components/formatted_time.js @@ -1,12 +1,12 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import {Text} from 'react-native'; -class FormattedTime extends Component { +class FormattedTime extends React.PureComponent { static propTypes = { intl: intlShape.isRequired, value: PropTypes.any.isRequired, diff --git a/app/components/layout/keyboard_layout.js b/app/components/layout/keyboard_layout.js index 8a83b78a6..52cd737ba 100644 --- a/app/components/layout/keyboard_layout.js +++ b/app/components/layout/keyboard_layout.js @@ -1,11 +1,11 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import {KeyboardAvoidingView, Platform, View} from 'react-native'; -export default class KeyboardLayout extends Component { +export default class KeyboardLayout extends React.PureComponent { static propTypes = { behaviour: PropTypes.string, children: PropTypes.node, diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index f743e9d95..29bcf1253 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -18,16 +18,16 @@ import ChannelDrawer from 'app/components/channel_drawer'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; import Loading from 'app/components/loading'; import OfflineIndicator from 'app/components/offline_indicator'; -import PostTextbox from 'app/components/post_textbox'; import PostListRetry from 'app/components/post_list_retry'; import StatusBar from 'app/components/status_bar'; -import {preventDoubleTap} from 'app/utils/tap'; +import {wrapWithPreventDoubleTap} from 'app/utils/tap'; import {makeStyleSheetFromTheme} from 'app/utils/theme'; import ChannelDrawerButton from './channel_drawer_button'; -import ChannelTitle from './channel_title'; import ChannelPostList from './channel_post_list'; +import ChannelPostTextbox from './channel_post_textbox'; import ChannelSearchButton from './channel_search_button'; +import ChannelTitle from './channel_title'; class Channel extends PureComponent { static propTypes = { @@ -37,7 +37,6 @@ class Channel extends PureComponent { loadProfilesAndTeamMembersForDMSidebar: PropTypes.func.isRequired, selectFirstAvailableTeam: PropTypes.func.isRequired, selectInitialChannel: PropTypes.func.isRequired, - handlePostDraftChanged: PropTypes.func.isRequired, initWebSocket: PropTypes.func.isRequired, closeWebSocket: PropTypes.func.isRequired, startPeriodicStatusUpdates: PropTypes.func.isRequired, @@ -45,13 +44,9 @@ class Channel extends PureComponent { }).isRequired, intl: intlShape.isRequired, navigator: PropTypes.object, - appState: PropTypes.bool, - currentTeam: PropTypes.object, - currentChannel: PropTypes.object, - drafts: PropTypes.object.isRequired, + currentTeamId: PropTypes.string, + currentChannelId: PropTypes.string, theme: PropTypes.object.isRequired, - subscribeToHeaderEvent: PropTypes.func, - unsubscribeFromHeaderEvent: PropTypes.func, webSocketRequest: PropTypes.object, statusBarHeight: PropTypes.number, channelsRequestStatus: PropTypes.string @@ -62,9 +57,8 @@ class Channel extends PureComponent { NetInfo.isConnected.addEventListener('connectionChange', this.handleConnectionChange); NetInfo.isConnected.fetch().then(this.handleConnectionChange); - if (this.props.currentTeam) { - const teamId = this.props.currentTeam.id; - this.loadChannels(teamId); + if (this.props.currentTeamId) { + this.loadChannels(this.props.currentTeamId); } } @@ -79,9 +73,8 @@ class Channel extends PureComponent { } componentWillReceiveProps(nextProps) { - if (nextProps.currentTeam.id && this.props.currentTeam.id !== nextProps.currentTeam.id) { - const teamId = nextProps.currentTeam.id; - this.loadChannels(teamId); + if (nextProps.currentTeamId && this.props.currentTeamId !== nextProps.currentTeamId) { + this.loadChannels(nextProps.currentTeamId); } } @@ -100,10 +93,10 @@ class Channel extends PureComponent { }; blurPostTextBox = () => { - this.postTextbox.getWrappedInstance().getWrappedInstance().blur(); + this.postTextbox.getWrappedInstance().blur(); }; - goToChannelInfo = () => { + goToChannelInfo = wrapWithPreventDoubleTap(() => { const {intl, navigator, theme} = this.props; const options = { screen: 'ChannelInfo', @@ -123,7 +116,7 @@ class Channel extends PureComponent { } else { navigator.push(options); } - }; + }); handleConnectionChange = (isConnected) => { const {actions, webSocketRequest} = this.props; @@ -143,10 +136,6 @@ class Channel extends PureComponent { connection(isConnected); }; - handleDraftChanged = (value) => { - this.props.actions.handlePostDraftChanged(this.props.currentChannel.id, value); - }; - handleLeaveTeam = () => { this.props.actions.selectFirstAvailableTeam(); }; @@ -167,14 +156,13 @@ class Channel extends PureComponent { }; retryLoadChannels = () => { - this.loadChannels(this.props.currentTeam.id); + this.loadChannels(this.props.currentTeamId); }; render() { const { channelsRequestStatus, - currentChannel, - drafts, + currentChannelId, intl, navigator, statusBarHeight, @@ -183,7 +171,7 @@ class Channel extends PureComponent { const style = getStyleFromTheme(theme); - if (!currentChannel.hasOwnProperty('id')) { + if (!currentChannelId) { if (channelsRequestStatus === RequestStatus.FAILURE) { return ( 20) { height = statusBarHeight - 20; @@ -220,16 +206,13 @@ class Channel extends PureComponent { > - @@ -237,7 +220,7 @@ class Channel extends PureComponent { preventDoubleTap(this.goToChannelInfo, this)} + onPress={this.goToChannelInfo} /> 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 80babd10d..ce29c4306 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -20,7 +20,6 @@ import PostList from 'app/components/post_list'; import PostListRetry from 'app/components/post_list_retry'; const {View: AnimatedView} = Animated; -const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window'); class ChannelPostList extends PureComponent { static propTypes = { @@ -35,15 +34,13 @@ class ChannelPostList extends PureComponent { channelIsLoading: PropTypes.bool, channelIsRefreshing: PropTypes.bool, channelRefreshingFailed: PropTypes.bool, - currentChannelId: PropTypes.string, intl: intlShape.isRequired, loadingPosts: PropTypes.bool, myMember: PropTypes.object.isRequired, navigator: PropTypes.object, posts: PropTypes.array.isRequired, postVisibility: PropTypes.number, - theme: PropTypes.object.isRequired, - networkOnline: PropTypes.bool.isRequired + theme: PropTypes.object.isRequired }; static defaultProps = { @@ -55,7 +52,6 @@ class ChannelPostList extends PureComponent { super(props); this.state = { - loaderOpacity: new Animated.Value(1), retryMessageHeight: new Animated.Value(0) }; } @@ -68,17 +64,8 @@ class ChannelPostList extends PureComponent { } componentWillReceiveProps(nextProps) { - const {currentChannelId, channel: currentChannel} = this.props; - const {currentChannelId: nextChannelId, channel: nextChannel, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps; - - // Show the loader if the channel id change - if (currentChannelId !== nextChannelId) { - this.setState({ - loaderOpacity: new Animated.Value(1) - }, () => { - this.shouldMarkChannelAsLoaded(nextPosts.length, nextChannel.total_msg_count === 0, nextChannelRefreshingFailed); - }); - } + const {channel: currentChannel} = this.props; + const {channel: nextChannel, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps; if (currentChannel.id !== nextChannel.id) { // Load the posts when the channel actually changes @@ -110,14 +97,7 @@ class ChannelPostList extends PureComponent { }; channelLoaded = () => { - Animated.timing(this.state.loaderOpacity, { - toValue: 0, - duration: 500 - }).start(() => { - if (this.mounted) { - this.setState({channelLoaded: true}); - } - }); + this.setState({channelLoaded: true}); }; toggleRetryMessage = (show = true) => { @@ -194,7 +174,7 @@ class ChannelPostList extends PureComponent { theme } = this.props; - const {loaderOpacity, retryMessageHeight} = this.state; + const {retryMessageHeight} = this.state; let component; if (!posts.length && channelRefreshingFailed) { @@ -226,23 +206,15 @@ class ChannelPostList extends PureComponent { ); } + const refreshIndicatorDimensions = { + width: Dimensions.get('window').width, + height: retryMessageHeight + }; + return ( {component} - - - - + { + this.props.actions.handlePostDraftChanged(this.props.channelId, value); + }; + + blur = () => { + this.refs.postTextbox.getWrappedInstance().getWrappedInstance().blur(); + }; + + render() { + return ( + + ); + } +} diff --git a/app/screens/channel/channel_post_textbox/index.js b/app/screens/channel/channel_post_textbox/index.js new file mode 100644 index 000000000..1f2b6af2f --- /dev/null +++ b/app/screens/channel/channel_post_textbox/index.js @@ -0,0 +1,28 @@ +// 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 { + handlePostDraftChanged +} from 'app/actions/views/channel'; + +import ChannelPostTextbox from './channel_post_textbox'; + +function mapStateToProps(state, ownProps) { + return { + draft: state.views.channel.drafts[ownProps.channelId] || {}, + ...ownProps + }; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ + handlePostDraftChanged + }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelPostTextbox); diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 1fd49b2c6..34907a99e 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -7,16 +7,15 @@ import {connect} from 'react-redux'; import { loadChannelsIfNecessary, loadProfilesAndTeamMembersForDMSidebar, - selectInitialChannel, - handlePostDraftChanged + selectInitialChannel } from 'app/actions/views/channel'; import {connection} from 'app/actions/views/connection'; import {selectFirstAvailableTeam} from 'app/actions/views/select_team'; import {getTheme} from 'app/selectors/preferences'; import {startPeriodicStatusUpdates, stopPeriodicStatusUpdates} from 'mattermost-redux/actions/users'; -import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; -import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import { init as initWebSocket, @@ -32,9 +31,8 @@ function mapStateToProps(state, ownProps) { return { ...ownProps, - ...state.views.channel, - currentTeam: getCurrentTeam(state) || {}, - currentChannel: getCurrentChannel(state) || {}, + currentTeamId: getCurrentTeamId(state), + currentChannelId: getCurrentChannelId(state), theme: getTheme(state), webSocketRequest: websocket, statusBarHeight, @@ -50,7 +48,6 @@ function mapDispatchToProps(dispatch) { loadProfilesAndTeamMembersForDMSidebar, selectFirstAvailableTeam, selectInitialChannel, - handlePostDraftChanged, initWebSocket, closeWebSocket, startPeriodicStatusUpdates,