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:
parent
b2cddad13a
commit
96da38ed14
16 changed files with 599 additions and 585 deletions
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
|
|
|||
402
app/components/channel_drawer/channels_list/channels_list.js
Normal file
402
app/components/channel_drawer/channels_list/channels_list.js
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
28
app/screens/channel/channel_post_textbox/index.js
Normal file
28
app/screens/channel/channel_post_textbox/index.js
Normal 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);
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue