RN-345 Optimizing channel switching and general improvements (#914)

* Reduced re-renders of Channel screen

* Switched some components to be PureComponents

* Stopped waiting for drawer to close to switch channels

* Fixed post list being stuck loading

* Fixed incorrectly removed ref

* Removed loader from channel post list

* Removed unused prop

* Re-added ChannelDrawer.swiperIndex

* Changed ChannelPostTextbox to only require a single draft

* Re-enable InteractionManager on ChannelDrawer

* Fixed ChannelDrawer.joinChannel and removed duplicated code

* Moved getting device dimensions into render method

* Switched ChannelsList to be a PureComponent
This commit is contained in:
Harrison Healey 2017-09-18 16:39:21 -04:00 committed by enahum
parent b2cddad13a
commit 96da38ed14
16 changed files with 599 additions and 585 deletions

View file

@ -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 = (
<View style={{flex: 1, marginBottom: 10}}>
<View style={style.swiperContent}>
<TeamsList
closeChannelDrawer={this.closeChannelDrawer}
myTeamMembers={myTeamMembers}
navigator={navigator}
/>
</View>
);
const channelsList = (
<View style={{flex: 1, marginBottom: 10}}>
<View style={style.swiperContent}>
<ChannelsList
currentTeam={currentTeam}
currentChannel={currentChannel}
channels={channels}
channelMembers={channelMembers}
myTeamMembers={myTeamMembers}
theme={theme}
navigator={navigator}
onSelectChannel={this.selectChannel}
onJoinChannel={this.joinChannel}
navigator={navigator}
onShowTeams={this.showTeams}
onSearchStart={this.onSearchStart}
onSearchEnds={this.onSearchEnds}
@ -312,7 +278,7 @@ export default class ChannelDrawer extends PureComponent {
{channelsList}
</DrawerSwiper>
);
};
}
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
}
});

View file

@ -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 <Text>{'Loading'}</Text>;
}
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 = <FilteredList {...listProps}/>;
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => preventDoubleTap(this.openSettingsModal)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles};
list = <List {...listProps}/>;
}
const title = (
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
onChangeText={this.onSearch}
onFocus={this.onSearchFocused}
value={term}
/>
</View>
);
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 = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
switcher = (
<TouchableHighlight
onPress={() => preventDoubleTap(onShowTeams)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<View style={styles.switcherContainer}>
<AwesomeIcon
name='chevron-left'
size={12}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
);
}
return (
<View
style={[styles.container, showMembers ? styles.extraPadding : {}]}
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
{switcher}
{title}
{settings}
{badge}
</View>
</View>
{list}
</View>
);
}
}
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);

View file

@ -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 <Text>{'Loading'}</Text>;
}
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 = <FilteredList {...listProps}/>;
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => preventDoubleTap(this.openSettingsModal)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles};
list = <List {...listProps}/>;
}
const title = (
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
onChangeText={this.onSearch}
onFocus={this.onSearchFocused}
value={term}
/>
</View>
);
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 = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
switcher = (
<TouchableHighlight
onPress={() => preventDoubleTap(onShowTeams)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<View style={styles.switcherContainer}>
<AwesomeIcon
name='chevron-left'
size={12}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
);
}
return (
<View
style={[styles.container, showMembers ? styles.extraPadding : {}]}
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
{switcher}
{title}
{settings}
{badge}
</View>
</View>
{list}
</View>
);
}
}
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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 (
<PostListRetry
@ -199,8 +187,6 @@ class Channel extends PureComponent {
);
}
const channelDraft = drafts[currentChannel.id] || {};
let height = 0;
if (statusBarHeight > 20) {
height = statusBarHeight - 20;
@ -220,16 +206,13 @@ class Channel extends PureComponent {
>
<View style={style.postList}>
<ChannelPostList
channel={currentChannel}
channelId={currentChannelId}
navigator={navigator}
/>
</View>
<PostTextbox
<ChannelPostTextbox
ref={this.attachPostTextbox}
files={channelDraft.files}
value={channelDraft.draft}
channelId={currentChannel.id}
onChangeText={this.handleDraftChanged}
channelId={currentChannelId}
navigator={navigator}
/>
</KeyboardLayout>
@ -237,7 +220,7 @@ class Channel extends PureComponent {
<View style={style.header}>
<ChannelDrawerButton/>
<ChannelTitle
onPress={() => preventDoubleTap(this.goToChannelInfo, this)}
onPress={this.goToChannelInfo}
/>
<ChannelSearchButton navigator={navigator}/>
</View>

View file

@ -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 (
<View style={{flex: 1}}>
{component}
<AnimatedView
pointerEvents='none'
style={{
position: 'absolute',
height: deviceHeight,
width: deviceWidth,
top: 0,
left: 0,
opacity: loaderOpacity
}}
>
<ChannelLoader theme={theme}/>
</AnimatedView>
<AnimatedView style={[style.refreshIndicator, {height: retryMessageHeight}]}>
<AnimatedView style={[style.refreshIndicator, refreshIndicatorDimensions]}>
<FormattedText
id='mobile.retry_message'
defaultMessage='Refreshing messages failed. Pull up to try again.'
@ -262,7 +234,6 @@ const style = StyleSheet.create({
paddingHorizontal: 10,
position: 'absolute',
top: 0,
width: deviceWidth,
overflow: 'hidden'
}
});

View file

@ -7,17 +7,18 @@ 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 {getCurrentChannelId, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel';
import {getTheme} from 'app/selectors/preferences';
import ChannelPostList from './channel_post_list';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
const getPostsInChannel = makeGetPostsInChannel();
return function mapStateToProps(state, ownProps) {
const channelId = ownProps.channel.id;
const channelId = ownProps.channelId;
const {getPosts, getPostsRetryAttempts, getPostsSince, getPostsSinceRetryAttempts} = state.requests.posts;
const posts = getPostsInChannel(state, channelId) || [];
const {websocket: websocketRequest} = state.requests.general;
@ -39,10 +40,10 @@ function makeMapStateToProps() {
}
return {
channel: getChannel(state, {id: channelId}),
channelIsLoading: state.views.channel.loading,
channelIsRefreshing,
channelRefreshingFailed,
currentChannelId: getCurrentChannelId(state),
posts,
postVisibility: state.views.channel.postVisibility[channelId],
loadingPosts: state.views.channel.loadingPosts[channelId],

View file

@ -0,0 +1,39 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import PostTextbox from 'app/components/post_textbox';
export default class ChannelPostTextbox extends React.PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
draft: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
actions: PropTypes.shape({
handlePostDraftChanged: PropTypes.func.isRequired
}).isRequired
};
handleDraftChanged = (value) => {
this.props.actions.handlePostDraftChanged(this.props.channelId, value);
};
blur = () => {
this.refs.postTextbox.getWrappedInstance().getWrappedInstance().blur();
};
render() {
return (
<PostTextbox
ref='postTextbox'
files={this.props.draft.files}
value={this.props.draft.draft}
channelId={this.props.channelId}
onChangeText={this.handleDraftChanged}
navigator={this.props.navigator}
/>
);
}
}

View file

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

View file

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