RN-382 Refactor at_mention & channel_mention autocomplete (#988)

* RN-382 Refactor at_mention & channel_mention autocomplete

* Feedback review

* If the term changes always trigger a request
This commit is contained in:
enahum
2017-10-04 17:36:51 -03:00
committed by Jarred Witt
parent ac0ac22f39
commit 031876fb77
19 changed files with 876 additions and 489 deletions

View File

@@ -1,47 +1,39 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
ListView,
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {sortByUsername} from 'mattermost-redux/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {SectionList} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i;
const FROM_REGEX = /\bfrom:\s*(\S*)$/i;
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AtMention extends Component {
export default class AtMention extends PureComponent {
static propTypes = {
currentUserId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
defaultChannel: PropTypes.object.isRequired,
autocompleteUsers: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
actions: PropTypes.shape({
autocompleteUsers: PropTypes.func.isRequired
})
}).isRequired,
currentChannelId: PropTypes.string,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
defaultChannel: PropTypes.object,
inChannel: PropTypes.array,
isSearch: PropTypes.bool,
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
outChannel: PropTypes.array,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired
};
static defaultProps = {
autocompleteUsers: {},
defaultChannel: {},
postDraft: '',
isSearch: false
@@ -50,74 +42,81 @@ export default class AtMention extends Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
rowHasChanged: (r1, r2) => r1 !== r2
});
const data = {};
this.state = {
active: false,
dataSource: ds.cloneWithRowsAndSections(data)
sections: []
};
}
componentWillReceiveProps(nextProps) {
const {isSearch} = nextProps;
const regex = isSearch ? FROM_REGEX : AT_MENTION_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
if (!match || this.state.mentionComplete) {
const {inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
active: false,
matchTerm: null,
mentionComplete: false
mentionComplete: false,
sections: []
});
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
return;
}
const matchTerm = isSearch ? match[1] : match[2];
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
});
if (matchTerm !== this.props.matchTerm) {
// if the term changed and we haven't made the request do that first
const {currentTeamId, currentChannelId} = this.props;
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, currentChannelId);
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, isSearch ? currentChannelId : '');
return;
}
if (nextProps.requestStatus !== RequestStatus.STARTED) {
const membersInChannel = this.filter(nextProps.autocompleteUsers.inChannel, matchTerm) || [];
const membersOutOfChannel = this.filter(nextProps.autocompleteUsers.outChannel, matchTerm) || [];
let data = {};
if (requestStatus !== RequestStatus.STARTED &&
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)) {
// if the request is complete and the term is not null we show the autocomplete
const sections = [];
if (isSearch) {
data = {members: membersInChannel.concat(membersOutOfChannel).sort(sortByUsername)};
sections.push({
id: 'mobile.suggestion.members',
defaultMessage: 'Members',
data: teamMembers,
key: 'teamMembers'
});
} else {
if (membersInChannel.length > 0) {
data = Object.assign({}, data, {inChannel: membersInChannel});
if (inChannel.length) {
sections.push({
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members',
data: inChannel,
key: 'inChannel'
});
}
if (this.checkSpecialMentions(matchTerm) && !isSearch) {
data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()});
if (this.checkSpecialMentions(matchTerm)) {
sections.push({
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions',
data: this.getSpecialMentions(),
key: 'special',
renderItem: this.renderSpecialMentions
});
}
if (membersOutOfChannel.length > 0) {
data = Object.assign({}, data, {notInChannel: membersOutOfChannel});
if (outChannel.length) {
sections.push({
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel',
data: outChannel,
key: 'outChannel'
});
}
}
this.setState({
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel') || data.hasOwnProperty('members'),
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
sections
});
}
}
filter = (profiles, matchTerm) => {
const {isSearch} = this.props;
return profiles.filter((p) => {
return ((p.id !== this.props.currentUserId || isSearch) && (
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
});
keyExtractor = (item) => {
return item.id || item;
};
getSpecialMentions = () => {
@@ -149,7 +148,7 @@ export default class AtMention extends Component {
let completedDraft;
if (isSearch) {
completedDraft = mentionPart.replace(FROM_REGEX, `from: ${mention} `);
completedDraft = mentionPart.replace(AT_MENTION_SEARCH_REGEX, `from: ${mention} `);
} else {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
@@ -159,140 +158,62 @@ export default class AtMention extends Component {
}
onChangeText(completedDraft, true);
this.setState({
active: false,
mentionComplete: true
});
this.setState({mentionComplete: true});
};
renderSectionHeader = (sectionData, sectionId) => {
const style = getStyleFromTheme(this.props.theme);
const localization = {
inChannel: {
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members'
},
notInChannel: {
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel'
},
specialMentions: {
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions'
},
members: {
id: 'mobile.suggestion.members',
defaultMessage: 'Members'
}
};
renderSectionHeader = ({section}) => {
return (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
<AutocompleteSectionHeader
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
/>
);
};
renderRow = (data, sectionId) => {
if (sectionId === 'specialMentions') {
return this.renderSpecialMentions(data);
}
const style = getStyleFromTheme(this.props.theme);
const hasFullName = data.first_name.length > 0 && data.last_name.length > 0;
renderItem = ({item}) => {
return (
<TouchableOpacity
onPress={() => this.completeMention(data.username)}
style={style.row}
>
<View style={style.rowPicture}>
<ProfilePicture
user={data}
theme={this.props.theme}
size={20}
status={null}
/>
</View>
<Text style={style.rowUsername}>{`@${data.username}`}</Text>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${data.first_name} ${data.last_name}`}</Text>}
</TouchableOpacity>
<AtMentionItem
onPress={this.completeMention}
userId={item}
/>
);
};
renderSpecialMentions = (data) => {
const style = getStyleFromTheme(this.props.theme);
renderSpecialMentions = ({item}) => {
return (
<TouchableOpacity
onPress={() => this.completeMention(data.completeHandle)}
style={style.row}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${data.completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={data.id}
defaultMessage={data.defaultMessage}
values={data.values}
style={[style.rowFullname, {flex: 1}]}
/>
</Text>
</TouchableOpacity>
<SpecialMentionItem
completeHandle={item.completeHandle}
defaultMessage={item.defaultMessage}
id={item.id}
onPress={this.completeMention}
theme={this.props.theme}
values={item.values}
/>
);
};
render() {
const {autocompleteUsers, requestStatus} = this.props;
if ((!this.state.active && (requestStatus !== RequestStatus.STARTED || requestStatus !== RequestStatus.SUCCESS)) || this.state.mentionComplete) {
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(this.props.theme);
if (
!autocompleteUsers.inChannel &&
!autocompleteUsers.outChannel &&
requestStatus === RequestStatus.STARTED
) {
return (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
const style = getStyleFromTheme(theme);
return (
<ListView
<SectionList
keyboardShouldPersistTaps='always'
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
renderFooter={this.renderFooter}
pageSize={10}
initialListSize={10}
initialNumToRender={10}
/>
);
}
@@ -300,72 +221,11 @@ export default class AtMention extends Component {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
section: {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg
},
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
loading: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 20,
backgroundColor: theme.centerChannelBg,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0
},
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center'
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
opacity: 0.6
},
textWrapper: {
flex: 1,
flexWrap: 'wrap',
paddingRight: 8
search: {
height: 250
}
};
});

View File

@@ -4,21 +4,29 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import {autocompleteUsers} from 'mattermost-redux/actions/users';
import {getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
import {getProfilesInCurrentChannel, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {getCurrentChannelId, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
filterMembersInChannel,
filterMembersNotInChannel,
filterMembersInCurrentTeam,
getMatchTermForAtMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';
function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
let postDraft;
if (ownProps.isSearch) {
if (isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
@@ -29,16 +37,28 @@ function mapStateToProps(state, ownProps) {
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForAtMention(value, isSearch);
let teamMembers;
let inChannel;
let outChannel;
if (isSearch) {
teamMembers = filterMembersInCurrentTeam(state, matchTerm);
} else {
inChannel = filterMembersInChannel(state, matchTerm);
outChannel = filterMembersNotInChannel(state, matchTerm);
}
return {
currentUserId: state.entities.users.currentUserId,
currentChannelId,
currentTeamId: state.entities.teams.currentTeamId,
currentTeamId: getCurrentTeamId(state),
defaultChannel: getDefaultChannel(state),
postDraft,
autocompleteUsers: {
inChannel: getProfilesInCurrentChannel(state),
outChannel: getProfilesNotInCurrentChannel(state)
},
matchTerm,
teamMembers,
inChannel,
outChannel,
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state),
...ownProps

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
static propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string,
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
theme: PropTypes.object.isRequired
};
completeMention = () => {
const {onPress, username} = this.props;
onPress(username);
};
render() {
const {
firstName,
lastName,
userId,
username,
theme
} = this.props;
const style = getStyleFromTheme(theme);
const hasFullName = firstName.length > 0 && lastName.length > 0;
return (
<TouchableOpacity
key={userId}
onPress={this.completeMention}
style={style.row}
>
<View style={style.rowPicture}>
<ProfilePicture
userId={userId}
theme={theme}
size={20}
status={null}
/>
</View>
<Text style={style.rowUsername}>{`@${username}`}</Text>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center'
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
opacity: 0.6
}
};
});

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'app/selectors/preferences';
import AtMentionItem from './at_mention_item';
function mapStateToProps(state, ownProps) {
const user = getUser(state, ownProps.userId);
return {
firstName: user.first_name,
lastName: user.last_name,
username: user.username,
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(AtMentionItem);

View File

@@ -0,0 +1,58 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AutocompleteSectionHeader extends PureComponent {
static propTypes = {
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
render() {
const {defaultMessage, id, theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
section: {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg
}
};
});

View File

@@ -1,37 +1,34 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
ListView,
Text,
TouchableOpacity,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {SectionList} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
const CHANNEL_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelMention extends Component {
export default class ChannelMention extends PureComponent {
static propTypes = {
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
autocompleteChannels: PropTypes.object.isRequired,
postDraft: PropTypes.string,
isSearch: PropTypes.bool,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
actions: PropTypes.shape({
searchChannels: PropTypes.func.isRequired
})
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
isSearch: PropTypes.bool,
matchTerm: PropTypes.string,
myChannels: PropTypes.array,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
postDraft: PropTypes.string,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
@@ -42,97 +39,82 @@ export default class ChannelMention extends Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
active: false,
dataSource: ds.cloneWithRowsAndSections(props.autocompleteChannels)
sections: []
};
}
componentWillReceiveProps(nextProps) {
const {isSearch} = nextProps;
const regex = isSearch ? CHANNEL_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, requestStatus} = nextProps;
// If not match or if user clicked on a channel
if (!match || this.state.mentionComplete) {
const nextState = {
active: false,
mentionComplete: false
};
// Handle the case where the user typed a ~ first and then backspaced
if (nextProps.postDraft.length < this.props.postDraft.length) {
nextState.matchTerm = null;
}
this.setState(nextState);
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: []
});
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
return;
}
const matchTerm = isSearch ? match[1] : match[2];
const myChannels = this.filter(nextProps.autocompleteChannels.myChannels, matchTerm);
const otherChannels = this.filter(nextProps.autocompleteChannels.otherChannels, matchTerm);
// Show loading indicator on first pull for channels
if (nextProps.requestStatus === RequestStatus.STARTED && ((myChannels.length === 0 && otherChannels.length === 0) || matchTerm === '')) {
this.setState({
active: true,
loading: true
});
return;
}
// Still matching the same term that didn't return any results
let startsWith;
if (isSearch) {
startsWith = match[0].startsWith(`in:${this.state.matchTerm}`) || match[0].startsWith(`channel:${this.state.matchTerm}`);
} else {
startsWith = match[0].startsWith(`~${this.state.matchTerm}`);
}
if (startsWith && (myChannels.length === 0 && otherChannels.length === 0)) {
this.setState({
active: false
});
return;
}
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
});
if (matchTerm !== this.props.matchTerm) {
// if the term changed and we haven't made the request do that first
const {currentTeamId} = this.props;
this.props.actions.searchChannels(currentTeamId, matchTerm);
return;
}
if (nextProps.requestStatus !== RequestStatus.STARTED && this.props.autocompleteChannels !== nextProps.autocompleteChannels) {
let data = {};
if (myChannels.length > 0) {
data = Object.assign({}, data, {myChannels});
}
if (otherChannels.length > 0) {
data = Object.assign({}, data, {otherChannels});
if (requestStatus !== RequestStatus.STARTED &&
(myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels)) {
// if the request is complete and the term is not null we show the autocomplete
const sections = [];
if (isSearch) {
if (publicChannels.length) {
sections.push({
id: 'suggestion.search.public',
defaultMessage: 'Public Channels',
data: publicChannels,
key: 'publicChannels'
});
}
if (privateChannels.length) {
sections.push({
id: 'suggestion.search.private',
defaultMessage: 'Private Channels',
data: privateChannels,
key: 'privateChannels'
});
}
} else {
if (myChannels.length) {
sections.push({
id: 'suggestion.mention.channels',
defaultMessage: 'My Channels',
data: myChannels,
key: 'myChannels'
});
}
if (otherChannels.length) {
sections.push({
id: 'suggestion.mention.morechannels',
defaultMessage: 'Other Channels',
data: otherChannels,
key: 'otherChannels'
});
}
}
this.setState({
active: true,
loading: false,
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
sections
});
}
}
filter = (channels, matchTerm) => {
return channels.filter((c) => c.name.includes(matchTerm) || c.display_name.includes(matchTerm));
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
@@ -140,7 +122,7 @@ export default class ChannelMention extends Component {
let completedDraft;
if (isSearch) {
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
completedDraft = mentionPart.replace(CHANNEL_SEARCH_REGEX, `${channelOrIn} ${mention} `);
completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `);
} else {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
@@ -150,87 +132,53 @@ export default class ChannelMention extends Component {
}
onChangeText(completedDraft, true);
this.setState({
active: false,
mentionComplete: true,
matchTerm: `${mention} `
});
this.setState({mentionComplete: true});
};
renderSectionHeader = (sectionData, sectionId) => {
const style = getStyleFromTheme(this.props.theme);
const localization = {
myChannels: {
id: 'suggestion.mention.channels',
defaultMessage: 'My Channels'
},
otherChannels: {
id: 'suggestion.mention.morechannels',
defaultMessage: 'Other Channels'
}
};
keyExtractor = (item) => {
return item.id || item;
};
renderSectionHeader = ({section}) => {
return (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
<AutocompleteSectionHeader
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
/>
);
};
renderRow = (data) => {
const style = getStyleFromTheme(this.props.theme);
renderItem = ({item}) => {
return (
<TouchableOpacity
onPress={() => this.completeMention(data.name)}
style={style.row}
>
<Text style={style.rowDisplayName}>{data.display_name}</Text>
<Text style={style.rowName}>{` (~${data.name})`}</Text>
</TouchableOpacity>
<ChannelMentionItem
channelId={item}
onPress={this.completeMention}
/>
);
};
render() {
if (!this.state.active || this.state.mentionComplete) {
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
// other components are not blocked.
return null;
}
const {requestStatus, theme} = this.props;
const style = getStyleFromTheme(theme);
if (this.state.loading && requestStatus === RequestStatus.STARTED) {
return (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<ListView
<SectionList
keyboardShouldPersistTaps='always'
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
renderRow={this.renderRow}
pageSize={10}
initialListSize={10}
initialNumToRender={10}
/>
);
}
@@ -238,57 +186,11 @@ export default class ChannelMention extends Component {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
section: {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg
},
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
loading: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 20,
backgroundColor: theme.centerChannelBg,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0
},
row: {
padding: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6
search: {
height: 250
}
};
});

View File

@@ -5,21 +5,29 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {General} from 'mattermost-redux/constants';
import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
filterMyChannels,
filterOtherChannels,
filterPublicChannels,
filterPrivateChannels,
getMatchTermForChannelMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'app/selectors/preferences';
import ChannelMention from './channel_mention';
function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
let postDraft;
if (ownProps.isSearch) {
if (isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
} else if (rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
@@ -30,17 +38,30 @@ function mapStateToProps(state, ownProps) {
}
}
const autocompleteChannels = {
myChannels: getMyChannels(state).filter((c) => c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL),
otherChannels: getOtherChannels(state)
};
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForChannelMention(value, isSearch);
let myChannels;
let otherChannels;
let publicChannels;
let privateChannels;
if (isSearch) {
publicChannels = filterPublicChannels(state, matchTerm);
privateChannels = filterPrivateChannels(state, matchTerm);
} else {
myChannels = filterMyChannels(state, matchTerm);
otherChannels = filterOtherChannels(state, matchTerm);
}
return {
...ownProps,
currentChannelId,
currentTeamId: state.entities.teams.currentTeamId,
myChannels,
otherChannels,
publicChannels,
privateChannels,
currentTeamId: getCurrentTeamId(state),
matchTerm,
postDraft,
autocompleteChannels,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state)
};

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity
} from 'react-native';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
displayName: PropTypes.string,
name: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
completeMention = () => {
const {onPress, name} = this.props;
onPress(name);
};
render() {
const {
channelId,
displayName,
name,
theme
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
key={channelId}
onPress={this.completeMention}
style={style.row}
>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` (~${name})`}</Text>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
padding: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6
}
};
});

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
import ChannelMentionItem from './channel_mention_item';
function mapStateToProps(state, ownProps) {
const channel = getChannel(state, ownProps.channelId);
return {
displayName: channel.display_name,
name: channel.name,
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(ChannelMentionItem);

View File

@@ -68,10 +68,14 @@ const style = StyleSheet.create({
overflow: 'hidden'
},
searchContainer: {
backgroundColor: 'white',
elevation: 5,
flex: 1,
left: 0,
maxHeight: 250,
overflow: 'hidden',
position: 'absolute',
right: 0,
zIndex: 5,
...Platform.select({
android: {
top: 47
@@ -79,11 +83,6 @@ const style = StyleSheet.create({
ios: {
top: 64
}
}),
left: 0,
right: 0,
maxHeight: 250,
overflow: 'hidden',
zIndex: 5
})
}
});

View File

@@ -0,0 +1,106 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class SpecialMentionItem extends PureComponent {
static propTypes = {
completeHandle: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
values: PropTypes.object
};
completeMention = () => {
const {onPress, completeHandle} = this.props;
onPress(completeHandle);
};
render() {
const {
defaultMessage,
id,
completeHandle,
theme,
values
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
onPress={this.completeMention}
style={style.row}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={id}
defaultMessage={defaultMessage}
values={values}
style={style.rowFullname}
/>
</Text>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center'
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
flex: 1,
opacity: 0.6
},
textWrapper: {
flex: 1,
flexWrap: 'wrap',
paddingRight: 8
}
};
});

View File

@@ -71,7 +71,7 @@ class ChannelIntro extends PureComponent {
style={style.profile}
>
<ProfilePicture
user={member}
userId={member.id}
size={64}
statusBorderWidth={2}
statusSize={25}

View File

@@ -40,7 +40,7 @@ export default class UserListRow extends React.PureComponent {
selected={this.props.selected}
>
<ProfilePicture
user={this.props.user}
userId={this.props.user.id}
size={32}
/>
<View style={style.textContainer}>

View File

@@ -51,7 +51,7 @@ function PostProfilePicture(props) {
return (
<ProfilePicture
user={user}
userId={user.id}
size={PROFILE_PICTURE_SIZE}
/>
);
@@ -60,7 +60,7 @@ function PostProfilePicture(props) {
return (
<TouchableOpacity onPress={onViewUserProfile}>
<ProfilePicture
user={user}
userId={user.id}
size={PROFILE_PICTURE_SIZE}
/>
</TouchableOpacity>

View File

@@ -6,19 +6,21 @@ import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import {getStatusesByIdsBatchedDebounced} from 'mattermost-redux/actions/users';
import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
import ProfilePicture from './profile_picture';
function mapStateToProps(state, ownProps) {
let status = ownProps.status;
if (!status && ownProps.user) {
status = ownProps.user.status || getStatusForUserId(state, ownProps.user.id);
const user = getUser(state, ownProps.userId);
if (!status && ownProps.userId) {
status = getStatusForUserId(state, ownProps.userId);
}
return {
theme: ownProps.theme || getTheme(state),
status,
user,
...ownProps
};
}

View File

@@ -0,0 +1,10 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
export const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i;
export const AT_MENTION_SEARCH_REGEX = /\bfrom:\s*(\S*)$/i;
export const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
export const CHANNEL_MENTION_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;

View File

@@ -80,7 +80,7 @@ export default class Notification extends PureComponent {
} else if (user) {
icon = (
<ProfilePicture
user={user}
userId={user.id}
size={IMAGE_SIZE}
/>
);

View File

@@ -115,7 +115,7 @@ class UserProfile extends PureComponent {
>
<View style={style.top}>
<ProfilePicture
user={user}
userId={user.id}
size={150}
statusBorderWidth={6}
statusSize={40}

View File

@@ -0,0 +1,198 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {createSelector} from 'reselect';
import {General} from 'mattermost-redux/constants';
import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
import {
getCurrentUser, getCurrentUserId, getProfilesInCurrentChannel,
getProfilesNotInCurrentChannel, getProfilesInCurrentTeam
} from 'mattermost-redux/selectors/entities/users';
import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
import {sortByUsername} from 'mattermost-redux/utils/user_utils';
import * as Autocomplete from 'app/constants/autocomplete';
import {getCurrentLocale} from 'app/selectors/i18n';
export const getMatchTermForAtMention = (() => {
let lastMatchTerm = null;
let lastValue;
let lastIsSearch;
return (value, isSearch) => {
if (value !== lastValue || isSearch !== lastIsSearch) {
const regex = isSearch ? Autocomplete.AT_MENTION_SEARCH_REGEX : Autocomplete.AT_MENTION_REGEX;
const match = value.match(regex);
lastValue = value;
lastIsSearch = isSearch;
if (match) {
lastMatchTerm = isSearch ? match[1] : match[2];
} else {
lastMatchTerm = null;
}
}
return lastMatchTerm;
};
})();
export const getMatchTermForChannelMention = (() => {
let lastMatchTerm = null;
let lastValue;
let lastIsSearch;
return (value, isSearch) => {
if (value !== lastValue || isSearch !== lastIsSearch) {
const regex = isSearch ? Autocomplete.CHANNEL_MENTION_SEARCH_REGEX : Autocomplete.CHANNEL_MENTION_REGEX;
const match = value.match(regex);
lastValue = value;
lastIsSearch = isSearch;
if (match) {
lastMatchTerm = isSearch ? match[1] : match[2];
} else {
lastMatchTerm = null;
}
}
return lastMatchTerm;
};
})();
export const filterMembersInChannel = createSelector(
getProfilesInCurrentChannel,
getCurrentUserId,
(state, matchTerm) => matchTerm,
(profilesInChannel, currentUserId, matchTerm) => {
let profiles;
if (matchTerm) {
profiles = profilesInChannel.filter((p) => {
return ((p.id !== currentUserId) && (
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
});
} else {
profiles = profilesInChannel.filter((p) => p.id !== currentUserId);
}
// already sorted
return profiles.map((p) => p.id);
}
);
export const filterMembersNotInChannel = createSelector(
getProfilesNotInCurrentChannel,
getCurrentUserId,
(state, matchTerm) => matchTerm,
(profilesNotInChannel, currentUserId, matchTerm) => {
let profiles;
if (matchTerm) {
profiles = profilesNotInChannel.filter((p) => {
return ((p.id !== currentUserId) && (
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
});
} else {
profiles = profilesNotInChannel;
}
return profiles.map((p) => p.id);
}
);
export const filterMembersInCurrentTeam = createSelector(
getProfilesInCurrentTeam,
getCurrentUser,
(state, matchTerm) => matchTerm,
(profilesInTeam, currentUser, matchTerm) => {
// FIXME: We need to include the currentUser here as is not in profilesInTeam on the redux store
let profiles;
if (matchTerm) {
profiles = [...profilesInTeam, currentUser].filter((p) => {
return (p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm));
});
} else {
profiles = [...profilesInTeam, currentUser];
}
return profiles.sort(sortByUsername).map((p) => p.id);
}
);
export const filterMyChannels = createSelector(
getMyChannels,
(state, opts) => opts,
(myChannels, matchTerm) => {
let channels;
if (matchTerm) {
channels = myChannels.filter((c) => {
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL) &&
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
});
} else {
channels = myChannels.filter((c) => {
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL);
});
}
return channels.map((c) => c.id);
}
);
export const filterOtherChannels = createSelector(
getOtherChannels,
(state, matchTerm) => matchTerm,
(otherChannels, matchTerm) => {
let channels;
if (matchTerm) {
channels = otherChannels.filter((c) => {
return (c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
});
} else {
channels = otherChannels;
}
return channels.map((c) => c.id);
}
);
export const filterPublicChannels = createSelector(
getMyChannels,
getOtherChannels,
getCurrentLocale,
(state, matchTerm) => matchTerm,
(myChannels, otherChannels, locale, matchTerm) => {
let channels;
if (matchTerm) {
channels = myChannels.filter((c) => {
return c.type === General.OPEN_CHANNEL &&
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
}).concat(
otherChannels.filter((c) => c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm))
);
} else {
channels = myChannels.filter((c) => {
return (c.type === General.OPEN_CHANNEL || c.type === General.PRIVATE_CHANNEL);
}).concat(otherChannels);
}
return channels.sort(sortChannelsByDisplayName.bind(null, locale)).map((c) => c.id);
}
);
export const filterPrivateChannels = createSelector(
getMyChannels,
(state, matchTerm) => matchTerm,
(myChannels, matchTerm) => {
let channels;
if (matchTerm) {
channels = myChannels.filter((c) => {
return c.type === General.PRIVATE_CHANNEL &&
(c.name.startsWith(matchTerm) || c.display_name.startsWith(matchTerm));
});
} else {
channels = myChannels.filter((c) => {
return c.type === General.PRIVATE_CHANNEL;
});
}
return channels.map((c) => c.id);
}
);