More Channels (#276)
* More Channels * Fix cleaning search * feeback review
This commit is contained in:
parent
6c527300d7
commit
3eb7e377cf
30 changed files with 864 additions and 222 deletions
|
|
@ -130,6 +130,15 @@ export function showOptionsModal(title, options) {
|
|||
};
|
||||
}
|
||||
|
||||
export function showMoreChannelsModal() {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: NavigationTypes.NAVIGATION_MODAL,
|
||||
route: Routes.MoreChannels
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function showDirectMessagesModal() {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import {changeOpacity} from 'app/utils/theme';
|
|||
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
import Badge from './badge';
|
||||
import Badge from 'app/components/badge';
|
||||
|
||||
export default class ChannelItem extends React.Component {
|
||||
export default class ChannelDrawerItem extends React.Component {
|
||||
static propTypes = {
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
onSelectChannel: React.PropTypes.func.isRequired,
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {Constants} from 'service/constants';
|
||||
import LineDivider from 'app/components/line_divider';
|
||||
import ChannelItem from './channel_item';
|
||||
import ChannelDrawerItem from './channel_drawer_item';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import UnreadIndicator from './unread_indicator';
|
||||
import deepEqual from 'deep-equal';
|
||||
|
|
@ -64,10 +64,19 @@ const Styles = StyleSheet.create({
|
|||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center'
|
||||
},
|
||||
more: {
|
||||
position: 'absolute',
|
||||
opacity: 0.6,
|
||||
right: 0,
|
||||
width: 50,
|
||||
height: 30,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
class ChannelList extends Component {
|
||||
class ChannelDrawerList extends Component {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
currentTeam: PropTypes.object.isRequired,
|
||||
|
|
@ -85,6 +94,7 @@ class ChannelList extends Component {
|
|||
unmarkFavorite: PropTypes.func.isRequired,
|
||||
showOptionsModal: PropTypes.func.isRequired,
|
||||
showDirectMessagesModal: PropTypes.func.isRequired,
|
||||
showMoreChannelsModal: PropTypes.func.isRequired,
|
||||
closeOptionsModal: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
|
@ -337,7 +347,7 @@ class ChannelList extends Component {
|
|||
const unread = msgCount > 0;
|
||||
|
||||
return (
|
||||
<ChannelItem
|
||||
<ChannelDrawerItem
|
||||
ref={channel.id}
|
||||
channel={channel}
|
||||
hasUnread={unread}
|
||||
|
|
@ -381,12 +391,28 @@ class ChannelList extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
const moreChannels = (
|
||||
<TouchableHighlight
|
||||
style={Styles.more}
|
||||
onPress={this.props.actions.showMoreChannelsModal}
|
||||
>
|
||||
<Icon
|
||||
name='plus-circle'
|
||||
size={18}
|
||||
color={theme.sidebarText}
|
||||
/>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
|
||||
data.push(
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.channels'
|
||||
defaultMessage='CHANNELS'
|
||||
/>,
|
||||
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
|
||||
<FormattedText
|
||||
style={[Styles.title, {color: theme.sidebarText}]}
|
||||
id='sidebar.channels'
|
||||
defaultMessage='CHANNELS'
|
||||
/>
|
||||
{moreChannels}
|
||||
</View>,
|
||||
...publicChannels
|
||||
);
|
||||
data.push(
|
||||
|
|
@ -398,19 +424,9 @@ class ChannelList extends Component {
|
|||
...privateChannels
|
||||
);
|
||||
|
||||
const moreDmStyle = {
|
||||
position: 'absolute',
|
||||
opacity: 0.6,
|
||||
right: 0,
|
||||
width: 50,
|
||||
height: 30,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
};
|
||||
|
||||
const moreDms = (
|
||||
<TouchableHighlight
|
||||
style={moreDmStyle}
|
||||
style={Styles.more}
|
||||
onPress={this.props.actions.showDirectMessagesModal}
|
||||
>
|
||||
<Icon
|
||||
|
|
@ -530,4 +546,4 @@ class ChannelList extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ChannelList);
|
||||
export default injectIntl(ChannelDrawerList);
|
||||
|
|
@ -4,11 +4,22 @@
|
|||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {showDirectMessagesModal, showOptionsModal, closeModal} from 'app/actions/navigation';
|
||||
import {closeDMChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel';
|
||||
import {
|
||||
closeModal,
|
||||
showMoreChannelsModal,
|
||||
showDirectMessagesModal,
|
||||
showOptionsModal
|
||||
} from 'app/actions/navigation';
|
||||
|
||||
import {
|
||||
closeDMChannel,
|
||||
leaveChannel,
|
||||
markFavorite,
|
||||
unmarkFavorite
|
||||
} from 'app/actions/views/channel';
|
||||
|
||||
import {viewChannel, markChannelAsRead} from 'service/actions/channels';
|
||||
import ChannelList from './channel_list';
|
||||
import ChannelDrawerList from './channel_drawer_list';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
|
|
@ -27,9 +38,10 @@ function mapDispatchToProps(dispatch) {
|
|||
unmarkFavorite,
|
||||
showOptionsModal,
|
||||
showDirectMessagesModal,
|
||||
showMoreChannelsModal,
|
||||
closeOptionsModal: closeModal
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelList);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawerList);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelListContainer from './channel_list_container';
|
||||
import ChannelListContainer from './channel_drawer_list_container';
|
||||
|
||||
export default ChannelListContainer;
|
||||
139
app/components/custom_list/channel_list_row.js
Normal file
139
app/components/custom_list/channel_list_row.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
icon: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5
|
||||
},
|
||||
purpose: {
|
||||
fontSize: 13,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function createTouchableComponent(children, action) {
|
||||
return (
|
||||
<TouchableOpacity onPress={action}>
|
||||
{children}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function ChannelListRow(props) {
|
||||
const {id, displayName, purpose, onPress, theme} = props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const RowComponent = (
|
||||
<View style={style.container}>
|
||||
<View style={style.titleContainer}>
|
||||
{props.selectable &&
|
||||
<TouchableWithoutFeedback onPress={props.onRowSelect}>
|
||||
<View style={style.selectorContainer}>
|
||||
<View style={[style.selector, (props.selected && style.selectorFilled)]}>
|
||||
{props.selected &&
|
||||
<Icon
|
||||
name='check'
|
||||
size={16}
|
||||
color='#fff'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
}
|
||||
<Icon
|
||||
name='globe'
|
||||
style={style.icon}
|
||||
/>
|
||||
<View style={style.textContainer}>
|
||||
<View style={{flexGrow: 1, flexDirection: 'column'}}>
|
||||
<Text style={style.displayName}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
|
||||
<Text
|
||||
style={style.purpose}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{purpose}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (typeof onPress === 'function') {
|
||||
return createTouchableComponent(RowComponent, () => onPress(id));
|
||||
}
|
||||
|
||||
return RowComponent;
|
||||
}
|
||||
|
||||
ChannelListRow.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
displayName: PropTypes.string.isRequired,
|
||||
purpose: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
selected: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ChannelListRow;
|
||||
|
|
@ -1,58 +1,65 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import {
|
||||
ListView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {displayUsername} from 'service/utils/user_utils';
|
||||
|
||||
import {ListView, StyleSheet, Text, View} from 'react-native';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import MemberListRow from './member_list_row';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
listView: {
|
||||
flex: 1
|
||||
},
|
||||
loadingText: {
|
||||
opacity: 0.6
|
||||
},
|
||||
sectionContainer: {
|
||||
backgroundColor: '#eaeaea',
|
||||
paddingLeft: 10,
|
||||
paddingVertical: 2
|
||||
},
|
||||
sectionText: {
|
||||
fontWeight: '600'
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 1,
|
||||
backgroundColor: '#eaeaea'
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
listView: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
loadingText: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6)
|
||||
},
|
||||
sectionContainer: {
|
||||
backgroundColor: theme.sidebarHeaderBg,
|
||||
paddingLeft: 10,
|
||||
paddingVertical: 2
|
||||
},
|
||||
sectionText: {
|
||||
fontWeight: '600',
|
||||
color: theme.sidebarHeaderTextColor
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1)
|
||||
},
|
||||
noResultContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
noResultText: {
|
||||
fontSize: 26,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default class MemberList extends PureComponent {
|
||||
export default class CustomList extends PureComponent {
|
||||
static propTypes = {
|
||||
members: PropTypes.array.isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
searching: PropTypes.bool,
|
||||
onRowPress: PropTypes.func,
|
||||
onListEndReached: PropTypes.func,
|
||||
onListEndReachedThreshold: PropTypes.number,
|
||||
preferences: PropTypes.object,
|
||||
loadingMembers: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
loadingText: PropTypes.object,
|
||||
listPageSize: PropTypes.number,
|
||||
listInitialSize: PropTypes.number,
|
||||
listScrollRenderAheadDistance: PropTypes.number,
|
||||
showSections: PropTypes.bool,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
renderRow: PropTypes.func
|
||||
renderRow: PropTypes.func.isRequired,
|
||||
createSections: PropTypes.func
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -63,7 +70,9 @@ export default class MemberList extends PureComponent {
|
|||
listInitialSize: 10,
|
||||
listScrollRenderAheadDistance: 200,
|
||||
selectable: false,
|
||||
loadingText: null,
|
||||
onRowSelect: () => true,
|
||||
createSections: () => true,
|
||||
showSections: true
|
||||
};
|
||||
|
||||
|
|
@ -74,17 +83,18 @@ export default class MemberList extends PureComponent {
|
|||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {members, showSections, searching} = nextProps;
|
||||
const {data, showSections, searching} = nextProps;
|
||||
this.showNoResults = true;
|
||||
|
||||
if (searching || searching !== this.props.searching) {
|
||||
this.setState(this.buildDataSource(nextProps));
|
||||
} else if (members !== this.props.members || showSections !== this.props.showSections) {
|
||||
let data = members;
|
||||
} else if (data !== this.props.data || showSections !== this.props.showSections) {
|
||||
let newData = data;
|
||||
if (showSections) {
|
||||
data = this.createSections(members);
|
||||
newData = this.props.createSections(data);
|
||||
}
|
||||
|
||||
const mergedData = Object.assign({}, data, this.state.data);
|
||||
const mergedData = Object.assign({}, newData, this.state.data);
|
||||
const dataSource = showSections ? this.state.dataSource.cloneWithRowsAndSections(mergedData) : this.state.dataSource.cloneWithRows(mergedData);
|
||||
this.setState({
|
||||
data: mergedData,
|
||||
|
|
@ -98,33 +108,17 @@ export default class MemberList extends PureComponent {
|
|||
rowHasChanged: (r1, r2) => r1 !== r2,
|
||||
sectionHeaderHasChanged: (s1, s2) => s1 !== s2
|
||||
});
|
||||
let data = props.members;
|
||||
let newData = props.data;
|
||||
if (props.showSections) {
|
||||
data = this.createSections(props.members);
|
||||
newData = this.props.createSections(props.data);
|
||||
}
|
||||
const dataSource = ds.cloneWithRowsAndSections(data);
|
||||
const dataSource = props.showSections ? ds.cloneWithRowsAndSections(newData) : ds.cloneWithRows(newData);
|
||||
return {
|
||||
data,
|
||||
data: newData,
|
||||
dataSource
|
||||
};
|
||||
};
|
||||
|
||||
createSections = (data) => {
|
||||
const sections = {};
|
||||
data.forEach((d) => {
|
||||
const name = d.username;
|
||||
const sectionKey = name.substring(0, 1).toUpperCase();
|
||||
|
||||
if (!sections[sectionKey]) {
|
||||
sections[sectionKey] = [];
|
||||
}
|
||||
|
||||
sections[sectionKey].push(d);
|
||||
});
|
||||
|
||||
return sections;
|
||||
};
|
||||
|
||||
handleRowSelect = (sectionId, rowId) => {
|
||||
const data = this.state.data;
|
||||
const section = [...data[sectionId]];
|
||||
|
|
@ -142,6 +136,7 @@ export default class MemberList extends PureComponent {
|
|||
};
|
||||
|
||||
renderSectionHeader = (sectionData, sectionId) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
return (
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{sectionId}</Text>
|
||||
|
|
@ -149,29 +144,21 @@ export default class MemberList extends PureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
renderRow = (user, sectionId, rowId) => {
|
||||
const {id, username} = user;
|
||||
const displayName = displayUsername(user, this.props.preferences);
|
||||
let onRowSelect = null;
|
||||
if (this.props.selectable) {
|
||||
onRowSelect = () => this.handleRowSelect(sectionId, rowId);
|
||||
}
|
||||
|
||||
return (
|
||||
<MemberListRow
|
||||
id={id}
|
||||
user={user}
|
||||
displayName={displayName}
|
||||
username={username}
|
||||
onPress={this.props.onRowPress}
|
||||
selectable={this.props.selectable}
|
||||
selected={user.selected}
|
||||
onRowSelect={onRowSelect}
|
||||
/>
|
||||
renderRow = (rowData, sectionId, rowId) => {
|
||||
return this.props.renderRow(
|
||||
rowData,
|
||||
sectionId,
|
||||
rowId,
|
||||
this.props.preferences,
|
||||
this.props.theme,
|
||||
this.props.selectable,
|
||||
this.props.onRowPress,
|
||||
this.handleRowSelect
|
||||
);
|
||||
};
|
||||
|
||||
renderSeparator = (sectionId, rowId) => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
return (
|
||||
<View
|
||||
key={`${sectionId}-${rowId}`}
|
||||
|
|
@ -181,17 +168,18 @@ export default class MemberList extends PureComponent {
|
|||
};
|
||||
|
||||
renderFooter = () => {
|
||||
if (!this.props.loadingMembers) {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
if (!this.props.loading || !this.props.loadingText) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backgroundColor = this.props.members.length > 0 ? '#fff' : '#0000';
|
||||
const backgroundColor = this.props.data.length > 0 ? theme.centerChannelBg : '#0000';
|
||||
|
||||
return (
|
||||
<View style={{height: 70, backgroundColor, alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FormattedText
|
||||
id='mobile.components.member_list.loading_members'
|
||||
defaultMessage='Loading Members...'
|
||||
{...this.props.loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -199,14 +187,32 @@ export default class MemberList extends PureComponent {
|
|||
};
|
||||
|
||||
render() {
|
||||
const renderRow = this.props.renderRow || this.renderRow;
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
let noResults = false;
|
||||
if (typeof this.props.data === 'object') {
|
||||
noResults = Object.keys(this.props.data).length === 0;
|
||||
} else {
|
||||
noResults = this.props.data.length === 0;
|
||||
}
|
||||
|
||||
if (this.showNoResults && !this.props.loading && noResults) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListView
|
||||
style={style.listView}
|
||||
dataSource={this.state.dataSource}
|
||||
renderRow={renderRow}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
renderRow={this.renderRow}
|
||||
renderSectionHeader={this.props.showSections ? this.renderSectionHeader : null}
|
||||
renderSeparator={this.renderSeparator}
|
||||
renderFooter={this.renderFooter}
|
||||
enableEmptySections={true}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes} from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
|
|
@ -7,49 +10,56 @@ import {
|
|||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome';
|
||||
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 16
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 10
|
||||
},
|
||||
username: {
|
||||
marginLeft: 5,
|
||||
fontSize: 16,
|
||||
opacity: 0.7
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
height: 65,
|
||||
paddingHorizontal: 15,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.centerChannelBg
|
||||
},
|
||||
displayName: {
|
||||
fontSize: 16,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
icon: {
|
||||
fontSize: 20,
|
||||
color: theme.centerChannelColor
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 5
|
||||
},
|
||||
username: {
|
||||
marginLeft: 5,
|
||||
fontSize: 16,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5)
|
||||
},
|
||||
selector: {
|
||||
height: 28,
|
||||
width: 28,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: '#888',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorContainer: {
|
||||
height: 50,
|
||||
paddingRight: 15,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
selectorFilled: {
|
||||
backgroundColor: '#378FD2',
|
||||
borderWidth: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function createTouchableComponent(children, action) {
|
||||
|
|
@ -61,7 +71,8 @@ function createTouchableComponent(children, action) {
|
|||
}
|
||||
|
||||
function MemberListRow(props) {
|
||||
const {id, displayName, username, onPress, user} = props;
|
||||
const {id, displayName, username, onPress, theme, user} = props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const RowComponent = (
|
||||
<View style={style.container}>
|
||||
|
|
@ -115,6 +126,7 @@ MemberListRow.propTypes = {
|
|||
displayName: PropTypes.string.isRequired,
|
||||
pictureURL: PropTypes.string,
|
||||
username: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
LoadTeam,
|
||||
Login,
|
||||
Mfa,
|
||||
MoreChannels,
|
||||
MoreDirectMessages,
|
||||
OptionsModal,
|
||||
RightMenuDrawer,
|
||||
|
|
@ -80,6 +81,13 @@ export const Routes = {
|
|||
title: {id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}
|
||||
}
|
||||
},
|
||||
MoreChannels: {
|
||||
key: 'MoreChannels',
|
||||
component: MoreChannels,
|
||||
navigationProps: {
|
||||
title: {id: 'more_channels.title', defaultMessage: 'More Channels'}
|
||||
}
|
||||
},
|
||||
MoreDirectMessages: {
|
||||
key: 'MoreDirectMessages',
|
||||
component: MoreDirectMessages,
|
||||
|
|
|
|||
|
|
@ -9,18 +9,24 @@ import {
|
|||
View
|
||||
} from 'react-native';
|
||||
|
||||
import MemberList from 'app/components/member_list';
|
||||
import MemberList from 'app/components/custom_list';
|
||||
import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list';
|
||||
|
||||
import AddMemberButton from './add_member_button';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default class ChannelAddMembers extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
currentChannel: PropTypes.object,
|
||||
currentChannelMemberCount: PropTypes.number,
|
||||
membersNotInChannel: PropTypes.array.isRequired,
|
||||
|
|
@ -36,17 +42,17 @@ export default class ChannelAddMembers extends PureComponent {
|
|||
goBack: PropTypes.func.isRequired,
|
||||
handleAddChannelMembers: PropTypes.func.isRequired
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
static navigationProps = {
|
||||
renderRightComponent: (props, emitter) => {
|
||||
return <AddMemberButton emitter={emitter}/>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedMembers: {}
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.subscribeToHeaderEvent('add_members', this.handleAddMembersPress);
|
||||
|
|
@ -83,25 +89,31 @@ export default class ChannelAddMembers extends PureComponent {
|
|||
if (loadMoreRequestStatus !== 'started' && membersNotInChannel.length < (currentTeamMemberCount - currentChannelMemberCount - 1)) {
|
||||
this.props.actions.getProfilesNotInChannel(this.props.currentTeam.id, this.props.currentChannel.id, this.props.membersNotInChannel.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleRowSelect = (id) => {
|
||||
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
|
||||
this.setState({
|
||||
selectedMembers
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<MemberList
|
||||
members={this.props.membersNotInChannel}
|
||||
data={this.props.membersNotInChannel}
|
||||
theme={this.props.theme}
|
||||
onListEndReached={this.loadMoreMembers}
|
||||
preferences={this.props.preferences}
|
||||
loadingMembers={this.props.loadMoreRequestStatus === 'started'}
|
||||
loading={this.props.loadMoreRequestStatus === 'started'}
|
||||
loadingText={loadingText}
|
||||
selectable={true}
|
||||
onRowSelect={this.handleRowSelect}
|
||||
renderRow={renderMemberRow}
|
||||
createSections={createMembersSections}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import navigationSceneConnect from '../navigationSceneConnect';
|
|||
import {handleAddChannelMembers} from 'app/actions/views/channel_add_members';
|
||||
import {goBack} from 'app/actions/navigation';
|
||||
import {getCurrentChannel, getCurrentChannelStats} from 'service/selectors/entities/channels';
|
||||
import {getMyPreferences} from 'service/selectors/entities/preferences';
|
||||
import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getCurrentTeam, getCurrentTeamStats} from 'service/selectors/entities/teams';
|
||||
import {getProfilesNotInCurrentChannel} from 'service/selectors/entities/users';
|
||||
import {getTeamStats} from 'service/actions/teams';
|
||||
|
|
@ -21,6 +21,7 @@ function mapStateToProps(state) {
|
|||
const currentChannelMemberCount = getCurrentChannelStats(state) && getCurrentChannelStats(state).member_count;
|
||||
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
currentChannel: getCurrentChannel(state),
|
||||
membersNotInChannel: getProfilesNotInCurrentChannel(state),
|
||||
currentTeam: getCurrentTeam(state),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import ChannelList from 'app/components/channel_list';
|
||||
import ChannelDrawerList from 'app/components/channel_drawer_list';
|
||||
|
||||
export default class ChannelDrawer extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
|
@ -37,7 +37,7 @@ export default class ChannelDrawer extends React.PureComponent {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<ChannelList
|
||||
<ChannelDrawerList
|
||||
currentTeam={currentTeam}
|
||||
currentChannel={currentChannel}
|
||||
channels={channels}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const style = StyleSheet.create({
|
|||
class ChannelInfo extends PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
currentChannel: PropTypes.object.isRequired,
|
||||
currentChannelCreatorName: PropTypes.string,
|
||||
currentChannelMemberCount: PropTypes.number,
|
||||
|
|
@ -65,7 +66,7 @@ class ChannelInfo extends PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.actions.getChannelStats(this.props.currentChannel.team_id, this.props.currentChannel.id);
|
||||
this.props.actions.getChannelStats(this.props.currentTeamId, this.props.currentChannel.id);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
|
@ -179,15 +180,15 @@ class ChannelInfo extends PureComponent {
|
|||
</View>
|
||||
<ChannelInfoRow
|
||||
action={this.props.actions.goToChannelMembers}
|
||||
defaultMessage={isAdmin ? 'Manage Members' : 'View Members'}
|
||||
defaultMessage={(isAdmin && currentChannel.type !== Constants.DM_CHANNEL) ? 'Manage Members' : 'View Members'}
|
||||
detail={currentChannelMemberCount}
|
||||
icon='users'
|
||||
textId={isAdmin ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
|
||||
textId={(isAdmin && currentChannel.type !== Constants.DM_CHANNEL) ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
|
||||
/>
|
||||
<View style={style.separatorContainer}>
|
||||
<View style={style.separator}/>
|
||||
</View>
|
||||
{isAdmin &&
|
||||
{isAdmin && this.renderLeaveOrDeleteChannelRow() &&
|
||||
<View>
|
||||
<ChannelInfoRow
|
||||
action={this.props.actions.goToChannelAddMembers}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ function mapStateToProps(state, ownProps) {
|
|||
|
||||
return {
|
||||
...ownProps,
|
||||
currentTeamId: state.entities.teams.currentId,
|
||||
currentChannel,
|
||||
currentChannelCreatorName,
|
||||
currentChannelMemberCount,
|
||||
|
|
|
|||
|
|
@ -10,20 +10,27 @@ import {
|
|||
} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
|
||||
import MemberList from 'app/components/member_list';
|
||||
import MemberList from 'app/components/custom_list';
|
||||
import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list';
|
||||
|
||||
import ChannelMembersTitle from './channel_members_title';
|
||||
import RemoveMemberButton from './remove_member_button';
|
||||
import {Constants} from 'service/constants';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
class ChannelMembers extends PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
currentChannel: PropTypes.object,
|
||||
currentChannelMembers: PropTypes.array.isRequired,
|
||||
currentChannelMemberCount: PropTypes.number.isRequired,
|
||||
|
|
@ -38,7 +45,7 @@ class ChannelMembers extends PureComponent {
|
|||
goBack: PropTypes.func.isRequired,
|
||||
handleRemoveChannelMembers: PropTypes.func.isRequired
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
static navigationProps = {
|
||||
renderTitleComponent: () => {
|
||||
|
|
@ -47,11 +54,11 @@ class ChannelMembers extends PureComponent {
|
|||
renderRightComponent: (props, emitter) => {
|
||||
return <RemoveMemberButton emitter={emitter}/>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedMembers: {}
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.subscribeToHeaderEvent('remove_members', this.handleRemoveMembersPress);
|
||||
|
|
@ -112,38 +119,46 @@ class ChannelMembers extends PureComponent {
|
|||
onPress: () => this.removeMembers(membersToRemove)
|
||||
}]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
removeMembers = (membersToRemove) => {
|
||||
const {actions, currentTeam, currentChannel} = this.props;
|
||||
actions.handleRemoveChannelMembers(currentTeam.id, currentChannel.id, membersToRemove).then(() => {
|
||||
actions.goBack();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadMoreMembers = () => {
|
||||
if (this.props.requestStatus !== 'started' && this.props.currentChannelMembers.length < this.props.currentChannelMemberCount) {
|
||||
this.props.actions.getProfilesInChannel(this.props.currentTeam.id, this.props.currentChannel.id, this.props.currentChannelMembers.length);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleRowSelect = (id) => {
|
||||
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
|
||||
this.setState({
|
||||
selectedMembers
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {currentChannel, isAdmin, theme} = this.props;
|
||||
const canManage = (isAdmin && currentChannel.type !== Constants.DM_CHANNEL && currentChannel.name !== Constants.DEFAULT_CHANNEL);
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<MemberList
|
||||
members={this.props.currentChannelMembers}
|
||||
data={this.props.currentChannelMembers}
|
||||
theme={this.props.theme}
|
||||
onListEndReached={this.loadMoreMembers}
|
||||
preferences={this.props.preferences}
|
||||
loadingMembers={this.props.requestStatus === 'started'}
|
||||
selectable={this.props.isAdmin}
|
||||
loading={this.props.requestStatus === 'started'}
|
||||
loadingText={loadingText}
|
||||
selectable={canManage}
|
||||
onRowSelect={this.handleRowSelect}
|
||||
renderRow={renderMemberRow}
|
||||
createSections={createMembersSections}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import navigationSceneConnect from '../navigationSceneConnect';
|
|||
import {goBack} from 'app/actions/navigation';
|
||||
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
|
||||
import {getCurrentChannel, getCurrentChannelStats} from 'service/selectors/entities/channels';
|
||||
import {getMyPreferences} from 'service/selectors/entities/preferences';
|
||||
import {getMyPreferences, getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getCurrentTeam} from 'service/selectors/entities/teams';
|
||||
import {getProfilesInCurrentChannel, getCurrentUserRoles} from 'service/selectors/entities/users';
|
||||
import {getProfilesInChannel} from 'service/actions/users';
|
||||
|
|
@ -22,6 +22,7 @@ function mapStateToProps(state) {
|
|||
const isAdmin = currentUserRoles.includes('_admin');
|
||||
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
currentChannel: getCurrentChannel(state),
|
||||
currentChannelMembers: getProfilesInCurrentChannel(state),
|
||||
currentChannelMemberCount,
|
||||
|
|
|
|||
|
|
@ -8,16 +8,20 @@ import {
|
|||
} from 'react-native';
|
||||
|
||||
import {getCurrentUserRoles} from 'service/selectors/entities/users';
|
||||
import {getCurrentChannel} from 'service/selectors/entities/channels';
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
function ChannelMemberTitle(props) {
|
||||
const {currentChannel, isAdmin} = props;
|
||||
const manage = (isAdmin && currentChannel.type !== Constants.DM_CHANNEL && currentChannel.name !== Constants.DEFAULT_CHANNEL);
|
||||
return (
|
||||
<View style={{alignItems: 'center', justifyContent: 'center', flex: 1, marginHorizontal: 50}}>
|
||||
<FormattedText
|
||||
id={props.isAdmin ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
|
||||
defaultMessage={props.isAdmin ? 'Manage Members' : 'View Members'}
|
||||
id={manage ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
|
||||
defaultMessage={manage ? 'Manage Members' : 'View Members'}
|
||||
style={{color: props.theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold', textAlign: 'center'}}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -26,6 +30,7 @@ function ChannelMemberTitle(props) {
|
|||
|
||||
ChannelMemberTitle.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
currentChannel: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
|
|
@ -40,6 +45,7 @@ function mapStateToProps(state) {
|
|||
|
||||
return {
|
||||
isAdmin,
|
||||
currentChannel: getCurrentChannel(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,10 +11,13 @@ import {
|
|||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
import {getCurrentUserRoles} from 'service/selectors/entities/users';
|
||||
import {getCurrentChannel} from 'service/selectors/entities/channels';
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
function RemoveMemberButton(props) {
|
||||
if (!props.isAdmin) {
|
||||
const {currentChannel, isAdmin} = props;
|
||||
if (!isAdmin || currentChannel.type === Constants.DM_CHANNEL || currentChannel.name === Constants.DEFAULT_CHANNEL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +40,7 @@ function RemoveMemberButton(props) {
|
|||
RemoveMemberButton.propTypes = {
|
||||
emitter: PropTypes.func.isRequired,
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
currentChannel: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object
|
||||
};
|
||||
|
||||
|
|
@ -50,6 +54,7 @@ function mapStateToProps(state) {
|
|||
const isAdmin = currentUserRoles.includes('_admin');
|
||||
return {
|
||||
isAdmin,
|
||||
currentChannel: getCurrentChannel(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import ChannelAddMembers from './channel_add_members';
|
|||
import LoadTeam from './load_team';
|
||||
import Login from './login/login_container.js';
|
||||
import Mfa from './mfa';
|
||||
import MoreChannels from './more_channels';
|
||||
import MoreDirectMessages from './more_dms';
|
||||
import OptionsModal from './options_modal';
|
||||
import RightMenuDrawer from './right_menu_drawer';
|
||||
|
|
@ -27,6 +28,7 @@ module.exports = {
|
|||
LoadTeam,
|
||||
Login,
|
||||
Mfa,
|
||||
MoreChannels,
|
||||
MoreDirectMessages,
|
||||
OptionsModal,
|
||||
RightMenuDrawer,
|
||||
|
|
|
|||
42
app/scenes/more_channels/index.js
Normal file
42
app/scenes/more_channels/index.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import navigationSceneConnect from '../navigationSceneConnect';
|
||||
|
||||
import {goBack} from 'app/actions/navigation';
|
||||
import {getTheme} from 'service/selectors/entities/preferences';
|
||||
import {getMoreChannels as getMoreChannelsSelector} from 'service/selectors/entities/channels';
|
||||
import {handleSelectChannel} from 'app/actions/views/channel';
|
||||
import {getMoreChannels, joinChannel, searchMoreChannels} from 'service/actions/channels';
|
||||
|
||||
import MoreChannels from './more_channels';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const {currentId: currentUserId} = state.entities.users;
|
||||
const {currentId: currentTeamId} = state.entities.teams;
|
||||
const {getMoreChannels: requestStatus} = state.requests.channels;
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
currentTeamId,
|
||||
channels: getMoreChannelsSelector(state),
|
||||
theme: getTheme(state),
|
||||
requestStatus
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
goBack,
|
||||
handleSelectChannel,
|
||||
joinChannel,
|
||||
getMoreChannels,
|
||||
searchMoreChannels
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(MoreChannels);
|
||||
242
app/scenes/more_channels/more_channels.js
Normal file
242
app/scenes/more_channels/more_channels.js
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
InteractionManager,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ChannelList from 'app/components/custom_list';
|
||||
import ChannelListRow from 'app/components/custom_list/channel_list_row';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
|
||||
import {Constants, RequestStatus} from 'service/constants';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
class MoreChannels extends PureComponent {
|
||||
static propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
channels: PropTypes.array,
|
||||
requestStatus: PropTypes.object.isRequired,
|
||||
subscribeToHeaderEvent: React.PropTypes.func.isRequired,
|
||||
unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
goBack: PropTypes.func.isRequired,
|
||||
handleSelectChannel: PropTypes.func.isRequired,
|
||||
joinChannel: PropTypes.func.isRequired,
|
||||
getMoreChannels: PropTypes.func.isRequired,
|
||||
searchMoreChannels: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
static navigationProps = {
|
||||
renderLeftComponent: (props, emitter, theme) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{flex: 1, paddingHorizontal: 15, justifyContent: 'center'}}
|
||||
onPress={() => emitter('close')}
|
||||
>
|
||||
<FormattedText
|
||||
id='more_direct_channels.close'
|
||||
defaultMessage='Close'
|
||||
style={{color: theme.sidebarHeaderTextColor}}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.searchTimeoutId = 0;
|
||||
|
||||
this.state = {
|
||||
channels: [],
|
||||
page: 0,
|
||||
next: true,
|
||||
searching: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.subscribeToHeaderEvent('close', this.props.actions.goBack);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {requestStatus} = this.props;
|
||||
if (this.state.searching &&
|
||||
nextProps.requestStatus.status === RequestStatus.SUCCESS) {
|
||||
const channels = this.filterChannels(nextProps.channels, this.state.term);
|
||||
this.setState({channels});
|
||||
} else if (requestStatus.status === RequestStatus.STARTED &&
|
||||
nextProps.requestStatus.status === RequestStatus.SUCCESS) {
|
||||
const {page} = this.state;
|
||||
const channels = nextProps.channels.splice(0, (page + 1) * Constants.CHANNELS_CHUNK_SIZE);
|
||||
this.setState({channels});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.props.actions.getMoreChannels(this.props.currentTeamId, 0);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.unsubscribeFromHeaderEvent('close');
|
||||
}
|
||||
|
||||
filterChannels = (channels, term) => {
|
||||
return channels.filter((c) => {
|
||||
return (c.name.toLowerCase().indexOf(term) !== -1 || c.display_name.toLowerCase().indexOf(term) !== -1);
|
||||
});
|
||||
};
|
||||
|
||||
searchBarRef = (ref) => {
|
||||
this.searchBar = ref;
|
||||
};
|
||||
|
||||
onSearchButtonPress = () => {
|
||||
this.searchBar.blur();
|
||||
};
|
||||
|
||||
searchProfiles = (event) => {
|
||||
const term = event.nativeEvent.text.toLowerCase();
|
||||
|
||||
if (term) {
|
||||
const channels = this.filterChannels(this.state.channels, term);
|
||||
this.setState({channels, term, searching: true});
|
||||
clearTimeout(this.searchTimeoutId);
|
||||
|
||||
this.searchTimeoutId = setTimeout(() => {
|
||||
this.props.actions.searchMoreChannels(this.props.currentTeamId, term);
|
||||
}, Constants.SEARCH_TIMEOUT_MILLISECONDS);
|
||||
} else {
|
||||
this.cancelSearch();
|
||||
}
|
||||
};
|
||||
|
||||
cancelSearch = () => {
|
||||
this.props.actions.getMoreChannels(this.props.currentTeamId, 0);
|
||||
this.setState({
|
||||
term: null,
|
||||
searching: false,
|
||||
page: 0
|
||||
});
|
||||
};
|
||||
|
||||
loadMoreChannels = () => {
|
||||
let {page} = this.state;
|
||||
if (this.props.requestStatus.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) {
|
||||
page = page + 1;
|
||||
this.props.actions.getMoreChannels(
|
||||
this.props.currentTeamId,
|
||||
page * Constants.CHANNELS_CHUNK_SIZE,
|
||||
Constants.CHANNELS_CHUNK_SIZE).
|
||||
then((data) => {
|
||||
if (Object.keys(data).length) {
|
||||
this.setState({
|
||||
page
|
||||
});
|
||||
} else {
|
||||
this.setState({next: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderChannelRow = (channel, sectionId, rowId, preferences, theme, selectable, onPress, onSelect) => {
|
||||
const {id, display_name: displayName, purpose} = channel;
|
||||
let onRowSelect = null;
|
||||
if (selectable) {
|
||||
onRowSelect = () => onSelect(sectionId, rowId);
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelListRow
|
||||
id={id}
|
||||
displayName={displayName}
|
||||
purpose={purpose}
|
||||
theme={theme}
|
||||
onPress={onPress}
|
||||
selectable={selectable}
|
||||
selected={channel.selected}
|
||||
onRowSelect={onRowSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
onSelectChannel = async (id) => {
|
||||
this.searchBar.blur();
|
||||
await this.props.actions.joinChannel(
|
||||
this.props.currentUserId,
|
||||
this.props.currentTeamId,
|
||||
id);
|
||||
await this.props.actions.handleSelectChannel(id);
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.props.actions.goBack();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const isLoading = this.props.requestStatus.status === RequestStatus.STARTED;
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
const more = this.state.searching ? () => true : this.loadMoreChannels;
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View
|
||||
style={{marginVertical: 5}}
|
||||
>
|
||||
<SearchBar
|
||||
ref={this.searchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})}
|
||||
height={27}
|
||||
fontSize={14}
|
||||
textColor={this.props.theme.centerChannelColor}
|
||||
hideBackground={true}
|
||||
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.07)}
|
||||
onChange={this.searchProfiles}
|
||||
onSearchButtonPress={this.onSearchButtonPress}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
/>
|
||||
</View>
|
||||
<ChannelList
|
||||
data={this.state.channels}
|
||||
theme={this.props.theme}
|
||||
searching={this.state.searching}
|
||||
onListEndReached={more}
|
||||
loading={isLoading}
|
||||
selectable={false}
|
||||
listScrollRenderAheadDistance={50}
|
||||
showSections={false}
|
||||
renderRow={this.renderChannelRow}
|
||||
onRowPress={this.onSelectChannel}
|
||||
loadingText={{id: 'mobile.loading_channels', defaultMessage: 'Loading Channels...'}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(MoreChannels);
|
||||
|
|
@ -11,17 +11,19 @@ import {
|
|||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import MemberList from 'app/components/member_list';
|
||||
import MemberList from 'app/components/custom_list';
|
||||
import SearchBar from 'app/components/search_bar';
|
||||
|
||||
import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list';
|
||||
import {Constants, RequestStatus} from 'service/constants';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#fff'
|
||||
}
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.centerChannelBg
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
class MoreDirectMessages extends PureComponent {
|
||||
|
|
@ -100,6 +102,14 @@ class MoreDirectMessages extends PureComponent {
|
|||
this.props.unsubscribeFromHeaderEvent('close');
|
||||
}
|
||||
|
||||
searchBarRef = (ref) => {
|
||||
this.searchBar = ref;
|
||||
};
|
||||
|
||||
onSearchButtonPress = () => {
|
||||
this.searchBar.blur();
|
||||
};
|
||||
|
||||
searchProfiles = (event) => {
|
||||
const term = event.nativeEvent.text;
|
||||
|
||||
|
|
@ -116,10 +126,10 @@ class MoreDirectMessages extends PureComponent {
|
|||
};
|
||||
|
||||
cancelSearch = () => {
|
||||
this.props.actions.getProfiles(0);
|
||||
this.setState({
|
||||
searching: false,
|
||||
page: 0,
|
||||
profiles: []
|
||||
page: 0
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -140,13 +150,12 @@ class MoreDirectMessages extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
onSelectMember = (id) => {
|
||||
onSelectMember = async (id) => {
|
||||
this.searchBar.blur();
|
||||
this.props.actions.makeDirectChannel(id).
|
||||
then(() => {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.props.actions.goBack();
|
||||
});
|
||||
await this.props.actions.makeDirectChannel(id);
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
this.props.actions.goBack();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -154,7 +163,7 @@ class MoreDirectMessages extends PureComponent {
|
|||
const {formatMessage} = this.props.intl;
|
||||
const isLoading = (this.props.requestStatus.status === RequestStatus.STARTED) ||
|
||||
(this.props.searchRequest.status === RequestStatus.STARTED);
|
||||
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
const more = this.state.searching ? () => true : this.loadMoreProfiles;
|
||||
|
||||
return (
|
||||
|
|
@ -163,30 +172,31 @@ class MoreDirectMessages extends PureComponent {
|
|||
style={{marginVertical: 5}}
|
||||
>
|
||||
<SearchBar
|
||||
ref={(ref) => {
|
||||
this.searchBar = ref;
|
||||
}}
|
||||
ref={this.searchBarRef}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMesage: 'Search'})}
|
||||
height={27}
|
||||
fontSize={14}
|
||||
textColor={this.props.theme.centerChannelColor}
|
||||
hideBackground={true}
|
||||
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.4)}
|
||||
textFieldBackgroundColor={changeOpacity(this.props.theme.centerChannelColor, 0.07)}
|
||||
onChange={this.searchProfiles}
|
||||
onSearchButtonPress={() => {
|
||||
this.searchBar.blur();
|
||||
}}
|
||||
onSearchButtonPress={this.onSearchButtonPress}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
/>
|
||||
</View>
|
||||
<MemberList
|
||||
members={this.state.profiles}
|
||||
data={this.state.profiles}
|
||||
theme={this.props.theme}
|
||||
searching={this.state.searching}
|
||||
onListEndReached={more}
|
||||
preferences={this.props.preferences}
|
||||
loadingMembers={isLoading}
|
||||
loading={isLoading}
|
||||
selectable={false}
|
||||
listScrollRenderAheadDistance={50}
|
||||
createSections={createMembersSections}
|
||||
renderRow={renderMemberRow}
|
||||
onRowPress={this.onSelectMember}
|
||||
loadingText={loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
50
app/utils/member_list.js
Normal file
50
app/utils/member_list.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import MemberListRow from 'app/components/custom_list/member_list_row';
|
||||
import {displayUsername} from 'service/utils/user_utils';
|
||||
|
||||
export const loadingText = {
|
||||
id: 'mobile.loading_members',
|
||||
defaultMessage: 'Loading Members...'
|
||||
};
|
||||
|
||||
export function createMembersSections(data) {
|
||||
const sections = {};
|
||||
data.forEach((d) => {
|
||||
const name = d.username;
|
||||
const sectionKey = name.substring(0, 1).toUpperCase();
|
||||
|
||||
if (!sections[sectionKey]) {
|
||||
sections[sectionKey] = [];
|
||||
}
|
||||
|
||||
sections[sectionKey].push(d);
|
||||
});
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
export function renderMemberRow(user, sectionId, rowId, preferences, theme, selectable, onPress, onSelect) {
|
||||
const {id, username} = user;
|
||||
const displayName = displayUsername(user, preferences);
|
||||
let onRowSelect = null;
|
||||
if (selectable) {
|
||||
onRowSelect = () => onSelect(sectionId, rowId);
|
||||
}
|
||||
|
||||
return (
|
||||
<MemberListRow
|
||||
id={id}
|
||||
user={user}
|
||||
displayName={displayName}
|
||||
username={username}
|
||||
theme={theme}
|
||||
onPress={onPress}
|
||||
selectable={selectable}
|
||||
selected={user.selected}
|
||||
onRowSelect={onRowSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1500,10 +1500,11 @@
|
|||
"mobile.channel_list.privateChannel": "Private Channel",
|
||||
"mobile.channel_list.publicChannel": "Public Channel",
|
||||
"mobile.components.channels_list_view.yourChannels": "Your channels:",
|
||||
"mobile.components.member_list.loading_members": "Loading Members...",
|
||||
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
|
||||
"mobile.components.select_server_view.continue": "Continue",
|
||||
"mobile.components.select_server_view.siteUrlPlaceholder": "https://mattermost.example.com",
|
||||
"mobile.loading_channels": "Loading Channels...",
|
||||
"mobile.loading_members": "Loading Members...",
|
||||
"mobile.routes.channels": "Channels",
|
||||
"mobile.routes.enterServerUrl": "Enter Server URL",
|
||||
"mobile.routes.login": "Login",
|
||||
|
|
|
|||
|
|
@ -439,6 +439,33 @@ export function getMoreChannels(teamId, offset, limit = Constants.CHANNELS_CHUNK
|
|||
let channels;
|
||||
try {
|
||||
channels = await Client.getMoreChannels(teamId, offset, limit);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: ChannelTypes.MORE_CHANNELS_FAILURE, error}, getState);
|
||||
return null;
|
||||
}
|
||||
|
||||
dispatch(batchActions([
|
||||
{
|
||||
type: ChannelTypes.RECEIVED_MORE_CHANNELS,
|
||||
data: await channels
|
||||
},
|
||||
{
|
||||
type: ChannelTypes.MORE_CHANNELS_SUCCESS
|
||||
}
|
||||
]), getState);
|
||||
|
||||
return channels;
|
||||
};
|
||||
}
|
||||
|
||||
export function searchMoreChannels(teamId, term) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({type: ChannelTypes.MORE_CHANNELS_REQUEST}, getState);
|
||||
|
||||
let channels;
|
||||
try {
|
||||
channels = await Client.searchMoreChannels(teamId, term);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch);
|
||||
dispatch({type: ChannelTypes.MORE_CHANNELS_FAILURE, error}, getState);
|
||||
|
|
@ -640,6 +667,7 @@ export default {
|
|||
deleteChannel,
|
||||
viewChannel,
|
||||
getMoreChannels,
|
||||
searchMoreChannels,
|
||||
getChannelStats,
|
||||
addChannelMember,
|
||||
removeChannelMember,
|
||||
|
|
|
|||
|
|
@ -539,6 +539,13 @@ export default class Client {
|
|||
);
|
||||
};
|
||||
|
||||
searchMoreChannels = async (teamId, term) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute(teamId)}/more/search`,
|
||||
{method: 'post', body: JSON.stringify({term})}
|
||||
);
|
||||
};
|
||||
|
||||
getChannelStats = async (teamId, channelId) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelNeededRoute(teamId, channelId)}/stats`,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {createSelector} from 'reselect';
|
||||
import {getCurrentTeamId} from 'service/selectors/entities/teams';
|
||||
import {buildDisplayableChannelList, completeDirectChannelInfo} from 'service/utils/channel_utils';
|
||||
import {buildDisplayableChannelList, getNotMemberChannels, completeDirectChannelInfo} from 'service/utils/channel_utils';
|
||||
import {Constants} from 'service/constants';
|
||||
|
||||
function getAllChannels(state) {
|
||||
|
|
@ -93,3 +93,11 @@ export const getDefaultChannel = createSelector(
|
|||
return Object.values(channels).find((c) => c.team_id === teamId && c.name === Constants.DEFAULT_CHANNEL);
|
||||
}
|
||||
);
|
||||
|
||||
export const getMoreChannels = createSelector(
|
||||
getAllChannels,
|
||||
getChannelMemberships,
|
||||
(allChannels, myMembers) => {
|
||||
return getNotMemberChannels(Object.values(allChannels), myMembers);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ export function buildDisplayableChannelList(usersState, teamsState, allChannels,
|
|||
};
|
||||
}
|
||||
|
||||
export function getNotMemberChannels(allChannels, myMembers) {
|
||||
return allChannels.filter(not(isNotMemberOf.bind(this, myMembers)));
|
||||
}
|
||||
|
||||
export function getDirectChannelName(id, otherId) {
|
||||
let handle;
|
||||
|
||||
|
|
@ -117,6 +121,10 @@ function isDirectChannelForUser(userId, otherUserId, channel) {
|
|||
return channel.type === Constants.DM_CHANNEL && getUserIdFromChannelName(userId, channel.name) === otherUserId;
|
||||
}
|
||||
|
||||
function isNotMemberOf(myMembers, channel) {
|
||||
return myMembers[channel.id];
|
||||
}
|
||||
|
||||
export function getUserIdFromChannelName(userId, channelName) {
|
||||
const ids = channelName.split('__');
|
||||
let otherUserId = '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue