More Channels (#276)

* More Channels

* Fix cleaning search

* feeback review
This commit is contained in:
enahum 2017-02-20 16:51:02 -03:00 committed by GitHub
parent 6c527300d7
commit 3eb7e377cf
30 changed files with 864 additions and 222 deletions

View file

@ -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({

View file

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

View file

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

View file

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

View file

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

View 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ function mapStateToProps(state, ownProps) {
return {
...ownProps,
currentTeamId: state.entities.teams.currentId,
currentChannel,
currentChannelCreatorName,
currentChannelMemberCount,

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = '';