diff --git a/app/components/autocomplete/at_mention/at_mention.js b/app/components/autocomplete/at_mention/at_mention.js
index f14c3d888..d37f26db3 100644
--- a/app/components/autocomplete/at_mention/at_mention.js
+++ b/app/components/autocomplete/at_mention/at_mention.js
@@ -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 (
-
-
-
-
-
+
);
};
- 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 (
- this.completeMention(data.username)}
- style={style.row}
- >
-
-
-
- {`@${data.username}`}
- {hasFullName && {' - '}}
- {hasFullName && {`${data.first_name} ${data.last_name}`}}
-
+
);
};
- renderSpecialMentions = (data) => {
- const style = getStyleFromTheme(this.props.theme);
-
+ renderSpecialMentions = ({item}) => {
return (
- this.completeMention(data.completeHandle)}
- style={style.row}
- >
-
-
-
-
- {`@${data.completeHandle}`}
- {' - '}
-
-
-
+
);
};
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 (
-
-
-
- );
- }
+ const style = getStyleFromTheme(theme);
return (
-
);
}
@@ -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
}
};
});
diff --git a/app/components/autocomplete/at_mention/index.js b/app/components/autocomplete/at_mention/index.js
index 030be6fe6..d6efb75a0 100644
--- a/app/components/autocomplete/at_mention/index.js
+++ b/app/components/autocomplete/at_mention/index.js
@@ -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
diff --git a/app/components/autocomplete/at_mention_item/at_mention_item.js b/app/components/autocomplete/at_mention_item/at_mention_item.js
new file mode 100644
index 000000000..5aa6b2659
--- /dev/null
+++ b/app/components/autocomplete/at_mention_item/at_mention_item.js
@@ -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 (
+
+
+
+
+ {`@${username}`}
+ {hasFullName && {' - '}}
+ {hasFullName && {`${firstName} ${lastName}`}}
+
+ );
+ }
+}
+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
+ }
+ };
+});
diff --git a/app/components/autocomplete/at_mention_item/index.js b/app/components/autocomplete/at_mention_item/index.js
new file mode 100644
index 000000000..93f0de737
--- /dev/null
+++ b/app/components/autocomplete/at_mention_item/index.js
@@ -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);
diff --git a/app/components/autocomplete/autocomplete_section_header.js b/app/components/autocomplete/autocomplete_section_header.js
new file mode 100644
index 000000000..041b87be0
--- /dev/null
+++ b/app/components/autocomplete/autocomplete_section_header.js
@@ -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 (
+
+
+
+
+
+ );
+ }
+}
+
+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
+ }
+ };
+});
diff --git a/app/components/autocomplete/channel_mention/channel_mention.js b/app/components/autocomplete/channel_mention/channel_mention.js
index c13aa9892..af898187c 100644
--- a/app/components/autocomplete/channel_mention/channel_mention.js
+++ b/app/components/autocomplete/channel_mention/channel_mention.js
@@ -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 (
-
-
-
-
-
+
);
};
- renderRow = (data) => {
- const style = getStyleFromTheme(this.props.theme);
-
+ renderItem = ({item}) => {
return (
- this.completeMention(data.name)}
- style={style.row}
- >
- {data.display_name}
- {` (~${data.name})`}
-
+
);
};
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 (
-
-
-
- );
- }
-
return (
-
);
}
@@ -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
}
};
});
diff --git a/app/components/autocomplete/channel_mention/index.js b/app/components/autocomplete/channel_mention/index.js
index bea3809a3..c2ade4959 100644
--- a/app/components/autocomplete/channel_mention/index.js
+++ b/app/components/autocomplete/channel_mention/index.js
@@ -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)
};
diff --git a/app/components/autocomplete/channel_mention_item/channel_mention_item.js b/app/components/autocomplete/channel_mention_item/channel_mention_item.js
new file mode 100644
index 000000000..119ff5ecd
--- /dev/null
+++ b/app/components/autocomplete/channel_mention_item/channel_mention_item.js
@@ -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 (
+
+ {displayName}
+ {` (~${name})`}
+
+ );
+ }
+}
+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
+ }
+ };
+});
diff --git a/app/components/autocomplete/channel_mention_item/index.js b/app/components/autocomplete/channel_mention_item/index.js
new file mode 100644
index 000000000..dc248578b
--- /dev/null
+++ b/app/components/autocomplete/channel_mention_item/index.js
@@ -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);
diff --git a/app/components/autocomplete/index.js b/app/components/autocomplete/index.js
index 6f365bf76..96001bdfb 100644
--- a/app/components/autocomplete/index.js
+++ b/app/components/autocomplete/index.js
@@ -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
+ })
}
});
diff --git a/app/components/autocomplete/special_mention_item.js b/app/components/autocomplete/special_mention_item.js
new file mode 100644
index 000000000..ebba3ab91
--- /dev/null
+++ b/app/components/autocomplete/special_mention_item.js
@@ -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 (
+
+
+
+
+
+ {`@${completeHandle}`}
+ {' - '}
+
+
+
+ );
+ }
+}
+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
+ }
+ };
+});
diff --git a/app/components/channel_intro/channel_intro.js b/app/components/channel_intro/channel_intro.js
index 1d62f9500..5c79d5f71 100644
--- a/app/components/channel_intro/channel_intro.js
+++ b/app/components/channel_intro/channel_intro.js
@@ -71,7 +71,7 @@ class ChannelIntro extends PureComponent {
style={style.profile}
>
diff --git a/app/components/post_profile_picture/post_profile_picture.js b/app/components/post_profile_picture/post_profile_picture.js
index 73ba0b672..79a2e15a8 100644
--- a/app/components/post_profile_picture/post_profile_picture.js
+++ b/app/components/post_profile_picture/post_profile_picture.js
@@ -51,7 +51,7 @@ function PostProfilePicture(props) {
return (
);
@@ -60,7 +60,7 @@ function PostProfilePicture(props) {
return (
diff --git a/app/components/profile_picture/index.js b/app/components/profile_picture/index.js
index d68a54a1d..15e940559 100644
--- a/app/components/profile_picture/index.js
+++ b/app/components/profile_picture/index.js
@@ -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
};
}
diff --git a/app/constants/autocomplete.js b/app/constants/autocomplete.js
new file mode 100644
index 000000000..c72599a85
--- /dev/null
+++ b/app/constants/autocomplete.js
@@ -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;
diff --git a/app/screens/notification/notification.js b/app/screens/notification/notification.js
index 4b2966797..88ba223c7 100644
--- a/app/screens/notification/notification.js
+++ b/app/screens/notification/notification.js
@@ -80,7 +80,7 @@ export default class Notification extends PureComponent {
} else if (user) {
icon = (
);
diff --git a/app/screens/user_profile/user_profile.js b/app/screens/user_profile/user_profile.js
index a0a7ecf47..53006eefe 100644
--- a/app/screens/user_profile/user_profile.js
+++ b/app/screens/user_profile/user_profile.js
@@ -115,7 +115,7 @@ class UserProfile extends PureComponent {
>
{
+ 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);
+ }
+);