diff --git a/app/components/custom_flat_list.js b/app/components/custom_flat_list.js
new file mode 100644
index 000000000..06100d7c2
--- /dev/null
+++ b/app/components/custom_flat_list.js
@@ -0,0 +1,240 @@
+// 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 {FlatList, Platform, View} from 'react-native';
+
+import Loading from 'app/components/loading';
+import FormattedText from 'app/components/formatted_text';
+import {emptyFunction} from 'app/utils/general';
+import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
+
+export default class CustomFlatList extends PureComponent {
+ static propTypes = {
+
+ /*
+ * The current theme.
+ */
+ theme: PropTypes.object.isRequired,
+
+ /*
+ * An array of items to be rendered.
+ */
+ items: PropTypes.array.isRequired,
+
+ /*
+ * A function or React element used to render the items in the list.
+ */
+ renderItem: PropTypes.func,
+
+ /*
+ * Whether or not to render "No results" when the list contains no items.
+ */
+ showNoResults: PropTypes.bool,
+
+ /*
+ * A function to get a unique key for each item in the list. If not provided, the id field of the item will be used as the key.
+ */
+ keyExtractor: PropTypes.func,
+
+ /*
+ * Any extra data needed to render the list. If this value changes, all items of a list will be re-rendered.
+ */
+ extraData: PropTypes.object,
+
+ /*
+ * A function called when an item in the list is pressed. Receives the item that was pressed as an argument.
+ */
+ onRowPress: PropTypes.func,
+
+ /*
+ * A function called when the end of the list is reached. This can be triggered before this list end is reached
+ * by changing onListEndReachedThreshold.
+ */
+ onListEndReached: PropTypes.func,
+
+ /*
+ * How soon before the end of the list onListEndReached should be called.
+ */
+ onListEndReachedThreshold: PropTypes.number,
+
+ /*
+ * Whether or not to display the loading text.
+ */
+ loading: PropTypes.bool,
+
+ /*
+ * The text displayed when loading is set to true.
+ */
+ loadingText: PropTypes.object,
+
+ /*
+ * How many items to render when the list is first rendered.
+ */
+ initialNumToRender: PropTypes.number,
+ };
+
+ static defaultProps = {
+ showNoResults: true,
+ keyExtractor: (item) => {
+ return item.id;
+ },
+ onListEndReached: emptyFunction,
+ onListEndReachedThreshold: 50,
+ loadingText: null,
+ initialNumToRender: 10,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ items: props.items,
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.items !== this.props.items) {
+ this.setState({
+ items: nextProps.items,
+ });
+ }
+ }
+
+ renderItem = ({item}) => {
+ const props = {
+ id: item.id,
+ item,
+ onPress: this.props.onRowPress,
+ };
+
+ // Allow passing in a component like UserListRow or ChannelListRow
+ if (this.props.renderItem.prototype.isReactElement) {
+ const RowComponent = this.props.renderItem;
+ return ;
+ }
+
+ return this.props.renderItem(props);
+ };
+
+ renderItemSeparator = () => {
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ return ;
+ };
+
+ renderFooter = () => {
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (!this.props.loading || !this.props.loadingText) {
+ return null;
+ }
+
+ if (this.props.items.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ renderEmptyList = () => {
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (this.props.loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (this.props.showNoResults) {
+ return (
+
+
+
+ );
+ }
+
+ return null;
+ };
+
+ render() {
+ const style = getStyleFromTheme(this.props.theme);
+
+ return (
+
+ );
+ }
+}
+
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ listView: {
+ flex: 1,
+ backgroundColor: theme.centerChannelBg,
+ ...Platform.select({
+ android: {
+ marginBottom: 20,
+ },
+ }),
+ },
+ loading: {
+ height: 70,
+ backgroundColor: theme.centerChannelBg,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ loadingText: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ },
+ searching: {
+ backgroundColor: theme.centerChannelBg,
+ height: '100%',
+ position: 'absolute',
+ width: '100%',
+ },
+ 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),
+ },
+ };
+});
diff --git a/app/components/emoji_picker/emoji_picker.js b/app/components/emoji_picker/emoji_picker.js
index e0ef77fcf..fb305c24e 100644
--- a/app/components/emoji_picker/emoji_picker.js
+++ b/app/components/emoji_picker/emoji_picker.js
@@ -473,6 +473,7 @@ export default class EmojiPicker extends PureComponent {
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
+ autoCapitalize='none'
value={searchTerm}
/>
diff --git a/app/screens/channel_add_members/channel_add_members.js b/app/screens/channel_add_members/channel_add_members.js
index 4bd531d54..826500c47 100644
--- a/app/screens/channel_add_members/channel_add_members.js
+++ b/app/screens/channel_add_members/channel_add_members.js
@@ -270,6 +270,7 @@ class ChannelAddMembers extends PureComponent {
onChangeText={this.searchProfiles}
onSearchButtonPress={this.searchProfiles}
onCancelButtonPress={this.cancelSearch}
+ autoCapitalize='none'
value={term}
/>
diff --git a/app/screens/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js
index e2cb55440..f9bc65389 100644
--- a/app/screens/channel_members/channel_members.js
+++ b/app/screens/channel_members/channel_members.js
@@ -317,6 +317,7 @@ class ChannelMembers extends PureComponent {
onChangeText={this.searchProfiles}
onSearchButtonPress={this.searchProfiles}
onCancelButtonPress={this.cancelSearch}
+ autoCapitalize='none'
value={term}
/>
diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js
index 4b22a7be0..245e2ee92 100644
--- a/app/screens/more_channels/more_channels.js
+++ b/app/screens/more_channels/more_channels.js
@@ -318,6 +318,7 @@ export default class MoreChannels extends PureComponent {
onChangeText={this.searchProfiles}
onSearchButtonPress={this.searchProfiles}
onCancelButtonPress={this.cancelSearch}
+ autoCapitalize='none'
value={term}
/>
diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js
index b523ad8b6..3a4afeac6 100644
--- a/app/screens/more_dms/more_dms.js
+++ b/app/screens/more_dms/more_dms.js
@@ -15,6 +15,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getGroupDisplayNameFromUserIds} from 'mattermost-redux/utils/channel_utils';
import {displayUsername, filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
+import CustomFlatList from 'app/components/custom_flat_list';
import CustomSectionList from 'app/components/custom_section_list';
import UserListRow from 'app/components/custom_list/user_list_row';
import Loading from 'app/components/loading';
@@ -96,13 +97,21 @@ class MoreDirectMessages extends PureComponent {
this.setState({profiles, showNoResults: true});
} else if (this.state.searching &&
nextProps.searchRequest.status === RequestStatus.SUCCESS) {
- let results = filterProfilesMatchingTerm(nextProps.profiles, this.state.term);
+ const exactMatches = [];
+ let results = filterProfilesMatchingTerm(nextProps.profiles, this.state.term).filter((p) => {
+ if (p.username === this.state.term || p.username.startsWith(this.state.term)) {
+ exactMatches.push(p);
+ return false;
+ }
+
+ return true;
+ });
if (this.state.selectedCount > 0) {
results = this.removeCurrentUserFromProfiles(results);
}
- this.setState({profiles: results, showNoResults: true});
+ this.setState({profiles: [...exactMatches, ...results], showNoResults: true});
}
}
@@ -466,6 +475,39 @@ class MoreDirectMessages extends PureComponent {
}),
};
+ let listComponent;
+ if (term.length) {
+ listComponent = (
+
+ );
+ } else {
+ listComponent = (
+
+ );
+ }
+
return (
@@ -484,6 +526,7 @@ class MoreDirectMessages extends PureComponent {
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
+ autoCapitalize='none'
value={term}
/>
-
+ {listComponent}
);
}
diff --git a/share_extension/android/extension_channels/extension_channels.js b/share_extension/android/extension_channels/extension_channels.js
index 36c974af4..83014092c 100644
--- a/share_extension/android/extension_channels/extension_channels.js
+++ b/share_extension/android/extension_channels/extension_channels.js
@@ -187,6 +187,7 @@ export default class ExtensionTeam extends PureComponent {
tintColorDelete={changeOpacity(defaultTheme.centerChannelColor, 0.3)}
titleCancelColor={defaultTheme.centerChannelColor}
onChangeText={this.handleSearch}
+ autoCapitalize='none'
value={this.state.term}
/>
diff --git a/share_extension/ios/extension_channels.js b/share_extension/ios/extension_channels.js
index fb69dd52e..c312f6d63 100644
--- a/share_extension/ios/extension_channels.js
+++ b/share_extension/ios/extension_channels.js
@@ -221,6 +221,7 @@ export default class ExtensionChannels extends PureComponent {
titleCancelColor={theme.linkColor}
onChangeText={this.handleSearch}
onCancelButtonPress={this.cancelSearch}
+ autoCapitalize='none'
value={this.state.term}
/>