diff --git a/app/components/custom_flat_list.js b/app/components/custom_flat_list.js
deleted file mode 100644
index 8524732dc..000000000
--- a/app/components/custom_flat_list.js
+++ /dev/null
@@ -1,240 +0,0 @@
-// Copyright (c) 2015-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/custom_list/__snapshots__/index.test.js.snap b/app/components/custom_list/__snapshots__/index.test.js.snap
index f23e638ea..9a9d9528f 100644
--- a/app/components/custom_list/__snapshots__/index.test.js.snap
+++ b/app/components/custom_list/__snapshots__/index.test.js.snap
@@ -1,64 +1,102 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`CustomList should match snapshot 1`] = `
-
-
+`;
+
+exports[`CustomList should match snapshot with SectionList 1`] = `
+
-
+ }
+ updateCellsBatchingPeriod={50}
+ windowSize={21}
+/>
`;
exports[`CustomList should match snapshot, renderFooter 1`] = `null`;
-exports[`CustomList should match snapshot, renderFooter 2`] = `
-
-
-
-`;
+exports[`CustomList should match snapshot, renderFooter 2`] = ``;
exports[`CustomList should match snapshot, renderSectionHeader 1`] = `
true,
- onListEndReachedThreshold: 50,
- listPageSize: 10,
- listInitialSize: 10,
- listScrollRenderAheadDistance: 200,
- loadingText: null,
- selectable: false,
- createSections: () => true,
- showSections: true,
+ listType: FLATLIST,
showNoResults: true,
};
constructor(props) {
super(props);
- this.state = this.buildDataSource(props);
+ this.contentOffsetY = 0;
+ this.state = {};
}
- componentWillReceiveProps(nextProps) {
- const {data, showSections, searching} = nextProps;
+ handleLayout = (event) => {
+ const {height} = event.nativeEvent.layout;
+ this.setState({listHeight: height});
+ };
- if (searching || searching !== this.props.searching) {
- this.setState(this.buildDataSource(nextProps));
- } else if (data !== this.props.data || showSections !== this.props.showSections) {
- let newData = data;
- if (showSections) {
- newData = this.props.createSections(data);
+ handleScroll = (event) => {
+ const pageOffsetY = event.nativeEvent.contentOffset.y;
+ if (pageOffsetY > 0) {
+ const contentHeight = event.nativeEvent.contentSize.height;
+ const direction = (this.contentOffsetY < pageOffsetY) ?
+ ListTypes.VISIBILITY_SCROLL_UP :
+ ListTypes.VISIBILITY_SCROLL_DOWN;
+
+ this.contentOffsetY = pageOffsetY;
+ if (
+ direction === ListTypes.VISIBILITY_SCROLL_UP &&
+ (contentHeight - pageOffsetY) < (this.state.listHeight * SCROLL_UP_MULTIPLIER)
+ ) {
+ this.props.onLoadMore();
}
-
- const dataSource = showSections ? this.state.dataSource.cloneWithRowsAndSections(newData) : this.state.dataSource.cloneWithRows(newData);
- this.setState({
- data: newData,
- dataSource,
- });
}
- }
-
- buildDataSource = (props) => {
- const ds = new ListView.DataSource({
- rowHasChanged: (r1, r2) => r1 !== r2,
- sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
- });
- let newData = props.data;
- if (props.showSections) {
- newData = this.props.createSections(props.data);
- }
- const dataSource = props.showSections ? ds.cloneWithRowsAndSections(newData) : ds.cloneWithRows(newData);
- return {
- data: newData,
- dataSource,
- };
};
- handleRowSelect = (sectionId, rowId) => {
- const data = this.state.data;
- const section = [...data[sectionId]];
-
- section[rowId] = Object.assign({}, section[rowId], {selected: !section[rowId].selected});
- const mergedData = Object.assign({}, data, {[sectionId]: section});
-
- const id = section[rowId].id;
-
- const dataSource = this.state.dataSource.cloneWithRowsAndSections(mergedData);
- this.setState({
- data: mergedData,
- dataSource,
- }, () => this.props.onRowSelect(id));
+ keyExtractor = (item) => {
+ return item.id || item.key || item.value || item;
};
- renderSectionHeader = (sectionData, sectionId) => {
- const style = getStyleFromTheme(this.props.theme);
- return (
-
-
- {sectionId}
-
-
- );
+ renderEmptyList = () => {
+ return this.props.noResults || null;
};
- renderRow = (item, sectionId, rowId) => {
+ renderItem = ({item, index, section}) => {
+ const {onRowPress, onRowSelect, selectable} = this.props;
const props = {
id: item.id,
item,
selected: item.selected,
- selectable: this.props.selectable,
- onPress: this.props.onRowPress,
+ selectable,
};
if ('disableSelect' in item) {
props.enabled = !item.disableSelect;
}
- if (this.props.onRowSelect) {
- props.onPress = this.handleRowSelect.bind(this, sectionId, rowId);
+ if (onRowSelect) {
+ props.onPress = onRowSelect(section.title, index);
} else {
- props.onPress = this.props.onRowPress;
+ props.onPress = onRowPress;
}
// Allow passing in a component like UserListRow or ChannelListRow
- if (this.props.renderRow.prototype.isReactComponent) {
- const RowComponent = this.props.renderRow;
+ if (this.props.renderItem.prototype.isReactComponent) {
+ const RowComponent = this.props.renderItem;
return ;
}
- return this.props.renderRow(props);
+ return this.props.renderItem(props);
};
- renderSeparator = (sectionId, rowId) => {
- const style = getStyleFromTheme(this.props.theme);
+ renderFlatList = () => {
+ const {data, extraData, theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
return (
-
);
};
renderFooter = () => {
- const {theme} = this.props;
- const style = getStyleFromTheme(theme);
- if (!this.props.loading || !this.props.loadingText) {
+ const {loading, loadingComponent} = this.props;
+
+ if (!loading || !loadingComponent) {
return null;
}
- const backgroundColor = this.props.data.length > 0 ? theme.centerChannelBg : '#0000';
+ return loadingComponent;
+ };
+
+ renderSectionHeader = ({section}) => {
+ const style = getStyleFromTheme(this.props.theme);
return (
-
-
+
+
+ {section.id}
+
);
};
- render() {
- const {
- data,
- listInitialSize,
- listPageSize,
- listScrollRenderAheadDistance,
- loading,
- onListEndReached,
- onListEndReachedThreshold,
- searching,
- showNoResults,
- showSections,
- theme,
- } = this.props;
- const {dataSource} = this.state;
+ renderSectionList = () => {
+ const {data, loading, theme} = this.props;
const style = getStyleFromTheme(theme);
- let noResults = false;
-
- if (typeof data === 'object') {
- noResults = Object.keys(data).length === 0;
- } else {
- noResults = data.length === 0;
- }
-
- let loadingComponent;
- if (loading && searching) {
- loadingComponent = (
-
-
-
- );
- }
-
- if (showNoResults && noResults) {
- return (
-
-
-
- );
- }
return (
-
-
- {loadingComponent}
-
+
);
+ };
+
+ renderSeparator = () => {
+ const style = getStyleFromTheme(this.props.theme);
+
+ return (
+
+ );
+ };
+
+ setListRef = (ref) => {
+ this.list = ref;
+ };
+
+ render() {
+ const {listType} = this.props;
+
+ if (listType === FLATLIST) {
+ return this.renderFlatList();
+ }
+
+ return this.renderSectionList();
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
- container: {
+ list: {
+ backgroundColor: theme.centerChannelBg,
flex: 1,
+ ...Platform.select({
+ android: {
+ marginBottom: 20,
+ },
+ }),
+ },
+ container: {
+ flexGrow: 1,
+ },
+ separator: {
+ height: 1,
+ flex: 1,
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
listView: {
flex: 1,
@@ -272,11 +256,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontWeight: '600',
color: theme.centerChannelColor,
},
- separator: {
- height: 1,
- flex: 1,
- backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
- },
noResultContainer: {
flex: 1,
flexDirection: 'row',
diff --git a/app/components/custom_list/index.test.js b/app/components/custom_list/index.test.js
index 2dbe15aa8..fd7b4d0f6 100644
--- a/app/components/custom_list/index.test.js
+++ b/app/components/custom_list/index.test.js
@@ -2,86 +2,75 @@
// See LICENSE.txt for license information.
import React from 'react';
+import {View} from 'react-native';
import {shallow} from 'enzyme';
import Preferences from 'mattermost-redux/constants/preferences';
-import {createMembersSections, loadingText} from 'app/utils/member_list';
-
-import CustomList from './index';
+import CustomList, {FLATLIST, SECTIONLIST} from './index';
describe('CustomList', () => {
- function emptyFunc() {} // eslint-disable-line no-empty-function
-
const baseProps = {
data: [{username: 'username_1'}, {username: 'username_2'}],
- theme: Preferences.THEMES.default,
- searching: false,
- onListEndReached: emptyFunc,
- onListEndReachedThreshold: 0,
+ listType: FLATLIST,
loading: false,
- loadingText,
- listPageSize: 0,
+ loadingComponent: (),
+ onLoadMore: jest.fn(),
+ onRowPress: jest.fn(),
+ onRowSelect: null,
+ renderItem: jest.fn(),
listInitialSize: 0,
listScrollRenderAheadDistance: 0,
showSections: true,
- onRowPress: emptyFunc,
selectable: true,
- onRowSelect: emptyFunc,
- renderRow: emptyFunc,
- createSections: createMembersSections,
- showNoResults: false,
+ theme: Preferences.THEMES.default,
};
- test('should match snapshot', () => {
+ test('should match snapshot with FlatList', () => {
const wrapper = shallow(
);
expect(wrapper.getElement()).toMatchSnapshot();
- expect(wrapper.state('data')).toEqual({U: baseProps.data});
-
- // on componentWillReceiveProps
- const newDataProps = [{username: 'username_1'}, {username: 'username_2'}, {username: 'username_3'}];
- wrapper.setProps({data: newDataProps});
- expect(wrapper.state('data')).toEqual({U: newDataProps});
+ expect(wrapper.find('FlatList')).toHaveLength(1);
});
- test('should match return value on buildDataSource', () => {
- const props = {...baseProps};
+ test('should match snapshot with SectionList', () => {
+ const props = {
+ ...baseProps,
+ listType: SECTIONLIST,
+ };
+
const wrapper = shallow(
);
-
- expect(wrapper.instance().buildDataSource(props).data).toEqual({U: [{username: 'username_1'}, {username: 'username_2'}]});
- const newDataProps = [{username: 'aaa_1'}, {username: 'aaa_2'}, {username: 'username_1'}, {username: 'username_2'}, {username: 'username_3'}];
- props.data = newDataProps;
- expect(wrapper.instance().buildDataSource(props).data).toEqual({
- A: [{username: 'aaa_1'}, {username: 'aaa_2'}],
- U: [{username: 'username_1'}, {username: 'username_2'}, {username: 'username_3'}],
- });
+ expect(wrapper.getElement()).toMatchSnapshot();
+ expect(wrapper.find('SectionList')).toHaveLength(1);
});
test('should match snapshot, renderSectionHeader', () => {
const wrapper = shallow(
);
- expect(wrapper.instance().renderSectionHeader([], 'section_id')).toMatchSnapshot();
+ const section = {
+ id: 'section_id',
+ };
+ expect(wrapper.instance().renderSectionHeader({section})).toMatchSnapshot();
});
- test('should call props.renderRow on renderRow', () => {
- const props = {...baseProps, renderRow: jest.fn()};
+ test('should call props.renderItem on renderItem', () => {
+ const props = {...baseProps};
const wrapper = shallow(
);
- wrapper.instance().renderRow({id: 'item_id', selected: true}, 'section_id', 'row_id');
- expect(props.renderRow).toHaveBeenCalledTimes(1);
+ wrapper.instance().renderItem({item: {id: 'item_id', selected: true}, index: 0, section: null});
+ expect(props.renderItem).toHaveBeenCalledTimes(1);
});
test('should match snapshot, renderSeparator', () => {
const wrapper = shallow(
);
- expect(wrapper.instance().renderSeparator('section_id', 'row_id')).toMatchSnapshot();
+ expect(wrapper.instance().renderSeparator()).toMatchSnapshot();
});
test('should match snapshot, renderFooter', () => {
diff --git a/app/components/custom_list/option_list_row/option_list_row.js b/app/components/custom_list/option_list_row/option_list_row.js
index 8b9cb38bf..680b7424b 100644
--- a/app/components/custom_list/option_list_row/option_list_row.js
+++ b/app/components/custom_list/option_list_row/option_list_row.js
@@ -70,6 +70,12 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
},
+ textContainer: {
+ marginLeft: 10,
+ justifyContent: 'center',
+ flexDirection: 'column',
+ flex: 1,
+ },
optionText: {
fontSize: 15,
color: theme.centerChannelColor,
diff --git a/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap b/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap
index 9e83da0b4..d62f004a7 100644
--- a/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap
+++ b/app/components/custom_list/user_list_row/__snapshots__/user_list_test.test.js.snap
@@ -7,6 +7,7 @@ exports[`UserListRow should match snapshot 1`] = `
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
+ "overflow": "hidden",
}
}
>
@@ -65,6 +66,7 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
+ "overflow": "hidden",
}
}
>
@@ -121,6 +123,7 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
"flex": 1,
"flexDirection": "row",
"marginHorizontal": 10,
+ "overflow": "hidden",
}
}
>
diff --git a/app/components/custom_list/user_list_row/user_list_row.js b/app/components/custom_list/user_list_row/user_list_row.js
index 68f6b08fa..39bade2d4 100644
--- a/app/components/custom_list/user_list_row/user_list_row.js
+++ b/app/components/custom_list/user_list_row/user_list_row.js
@@ -119,6 +119,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flex: 1,
flexDirection: 'row',
marginHorizontal: 10,
+ overflow: 'hidden',
},
profileContainer: {
flexDirection: 'row',
diff --git a/app/components/custom_section_list.js b/app/components/custom_section_list.js
deleted file mode 100644
index c95a2ffa1..000000000
--- a/app/components/custom_section_list.js
+++ /dev/null
@@ -1,306 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- Platform,
- SectionList,
- Text,
- View,
-} from 'react-native';
-
-import Loading from 'app/components/loading';
-import FormattedText from 'app/components/formatted_text';
-import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
-
-export default class CustomSectionList extends React.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 section identifier for each item in the list. Items sharing a section identifier will be grouped together.
- */
- sectionKeyExtractor: PropTypes.func.isRequired,
-
- /*
- * A comparison function used when sorting items in the list.
- */
- compareItems: PropTypes.func.isRequired,
-
- /*
- * 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: () => true,
- onListEndReachedThreshold: 50,
- loadingText: null,
- initialNumToRender: 10,
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- sections: this.extractSections(props.items),
- };
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.items !== this.props.items) {
- this.setState({
- sections: this.extractSections(nextProps.items),
- });
- }
- }
-
- extractSections = (items) => {
- const sections = {};
- const sectionKeys = [];
- for (const item of items) {
- const sectionKey = this.props.sectionKeyExtractor(item);
-
- if (!sections[sectionKey]) {
- sections[sectionKey] = [];
- sectionKeys.push(sectionKey);
- }
-
- sections[sectionKey].push(item);
- }
-
- sectionKeys.sort();
-
- return sectionKeys.map((sectionKey) => {
- return {
- key: sectionKey,
- data: sections[sectionKey].sort(this.props.compareItems),
- };
- });
- }
-
- renderSectionHeader = ({section}) => {
- const {theme} = this.props;
- const style = getStyleFromTheme(theme);
-
- return (
-
-
- {section.key}
-
-
- );
- };
-
- 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: {
- ...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%',
- },
- sectionContainer: {
- backgroundColor: changeOpacity(theme.centerChannelColor, 0.07),
- paddingLeft: 10,
- paddingVertical: 2,
- },
- sectionWrapper: {
- backgroundColor: theme.centerChannelBg,
- },
- sectionText: {
- fontWeight: '600',
- color: theme.centerChannelColor,
- },
- 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/edit_channel_info/index.js b/app/components/edit_channel_info/index.js
index 50f8d9cc6..f0ca0dd7a 100644
--- a/app/components/edit_channel_info/index.js
+++ b/app/components/edit_channel_info/index.js
@@ -12,8 +12,6 @@ import {
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
-import EventEmitter from 'mattermost-redux/utils/event_emitter';
-
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
@@ -92,7 +90,6 @@ export default class EditChannelInfo extends PureComponent {
};
close = (goBack = false) => {
- EventEmitter.emit('closing-create-channel', false);
if (goBack) {
this.props.navigator.pop({animated: true});
} else {
diff --git a/app/screens/channel_add_members/channel_add_members.js b/app/screens/channel_add_members/channel_add_members.js
index 60d8a760f..67ce5153d 100644
--- a/app/screens/channel_add_members/channel_add_members.js
+++ b/app/screens/channel_add_members/channel_add_members.js
@@ -3,70 +3,70 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {injectIntl, intlShape} from 'react-intl';
+import {intlShape} from 'react-intl';
import {
Alert,
- InteractionManager,
Platform,
View,
} from 'react-native';
+import {debounce} from 'mattermost-redux/actions/helpers';
+import {General} from 'mattermost-redux/constants';
+import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
+
import Loading from 'app/components/loading';
-import CustomList from 'app/components/custom_list';
+import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list';
import UserListRow from 'app/components/custom_list/user_list_row';
+import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {alertErrorIfInvalidPermissions} from 'app/utils/general';
-import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list';
-import {changeOpacity, setNavigatorStyles} from 'app/utils/theme';
+import {createProfilesSections, loadingText} from 'app/utils/member_list';
+import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
-import {General, RequestStatus} from 'mattermost-redux/constants';
-import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
-
-class ChannelAddMembers extends PureComponent {
+export default class ChannelAddMembers extends PureComponent {
static propTypes = {
- intl: intlShape.isRequired,
- theme: PropTypes.object.isRequired,
- currentChannel: PropTypes.object,
- membersNotInChannel: PropTypes.array.isRequired,
- currentTeam: PropTypes.object,
- navigator: PropTypes.object,
- preferences: PropTypes.object,
- loadMoreRequestStatus: PropTypes.string,
- searchRequestStatus: PropTypes.string,
- addChannelMemberStatus: PropTypes.string,
actions: PropTypes.shape({
getTeamStats: PropTypes.func.isRequired,
getProfilesNotInChannel: PropTypes.func.isRequired,
handleAddChannelMembers: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
- }),
+ }).isRequired,
+ currentChannelId: PropTypes.string.isRequired,
+ currentTeamId: PropTypes.string.isRequired,
+ currentUserId: PropTypes.string.isRequired,
+ membersNotInChannel: PropTypes.array.isRequired,
+ navigator: PropTypes.object,
+ theme: PropTypes.object.isRequired,
};
- addButton = {
- disabled: true,
- id: 'add-members',
- showAsAction: 'always',
+ static contextTypes = {
+ intl: intlShape.isRequired,
};
- constructor(props) {
- super(props);
+ constructor(props, context) {
+ super(props, context);
this.searchTimeoutId = 0;
+ this.page = -1;
+ this.next = true;
this.state = {
- canSelect: true,
- next: true,
- page: 0,
+ adding: false,
+ loading: false,
profiles: [],
- searching: false,
- selectedMembers: {},
- showNoResults: false,
+ searchResults: [],
+ selectedIds: {},
term: '',
- isLoading: true,
};
- this.addButton.title = props.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'});
+
+ this.addButton = {
+ disabled: true,
+ id: 'add-members',
+ title: context.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'}),
+ showAsAction: 'always',
+ };
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons({
@@ -75,136 +75,113 @@ class ChannelAddMembers extends PureComponent {
}
componentDidMount() {
- InteractionManager.runAfterInteractions(() => {
- this.props.actions.getProfilesNotInChannel(this.props.currentTeam.id, this.props.currentChannel.id, 0);
- this.props.actions.getTeamStats(this.props.currentTeam.id);
- });
+ const {actions, currentTeamId} = this.props;
- this.emitCanAddMembers(false);
+ actions.getTeamStats(currentTeamId);
+ this.getProfiles();
+
+ this.enableAddOption(false);
}
- componentWillReceiveProps(nextProps) {
- if (this.props.theme !== nextProps.theme) {
- setNavigatorStyles(this.props.navigator, nextProps.theme);
- }
+ componentDidUpdate(prevProps) {
+ const {navigator, theme} = this.props;
+ const {adding, selectedIds} = this.state;
+ const enabled = Object.keys(selectedIds).length > 0 && !adding;
- const {loadMoreRequestStatus} = this.props;
- if (loadMoreRequestStatus === RequestStatus.STARTED &&
- nextProps.loadMoreRequestStatus === RequestStatus.SUCCESS) {
- const {page} = this.state;
- const profiles = markSelectedProfiles(
- nextProps.membersNotInChannel.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE),
- this.state.selectedMembers
- );
- this.setState({profiles, showNoResults: true});
- } else if (this.state.searching &&
- nextProps.searchRequestStatus === RequestStatus.SUCCESS) {
- const results = markSelectedProfiles(
- filterProfilesMatchingTerm(nextProps.membersNotInChannel, this.state.term),
- this.state.selectedMembers
- );
- this.setState({profiles: results, showNoResults: true});
- }
+ this.enableAddOption(enabled);
- const {addChannelMemberStatus} = nextProps;
-
- if (this.props.addChannelMemberStatus !== addChannelMemberStatus) {
- switch (addChannelMemberStatus) {
- case RequestStatus.STARTED:
- this.emitAdding(true);
- this.setState({error: null, adding: true, canSelect: false});
- break;
- case RequestStatus.SUCCESS:
- this.emitAdding(false);
- this.setState({error: null, adding: false, canSelect: false});
- this.close();
- break;
- case RequestStatus.FAILURE:
- this.emitAdding(false);
- this.setState({adding: false, canSelect: true});
- break;
- }
- }
-
- if ((loadMoreRequestStatus !== nextProps.loadMoreRequestStatus) || (this.props.searchRequestStatus !== nextProps.searchRequestStatus)) {
- const isLoading = (nextProps.loadMoreRequestStatus === RequestStatus.STARTED) ||
- (nextProps.searchRequestStatus === RequestStatus.STARTED);
- this.setState({isLoading});
+ if (theme !== prevProps.theme) {
+ setNavigatorStyles(navigator, theme);
}
}
- cancelSearch = () => {
- this.setState({
- searching: false,
- term: '',
- page: 0,
- profiles: markSelectedProfiles(this.props.membersNotInChannel, this.state.selectedMembers),
- });
+ clearSearch = () => {
+ this.setState({term: '', searchResults: []});
};
close = () => {
this.props.navigator.pop({animated: true});
};
- emitAdding = (loading) => {
- this.props.navigator.setButtons({
- rightButtons: [{...this.addButton, disabled: loading}],
- });
- };
-
- emitCanAddMembers = (enabled) => {
+ enableAddOption = (enabled) => {
this.props.navigator.setButtons({
rightButtons: [{...this.addButton, disabled: !enabled}],
});
};
- handleAddMembersPress = async () => {
- const {selectedMembers} = this.state;
- const {actions, currentChannel} = this.props;
- const membersToAdd = Object.keys(selectedMembers).filter((m) => selectedMembers[m]);
+ getProfiles = debounce(() => {
+ const {loading, term} = this.state;
+ if (this.next && !loading && !term) {
+ this.setState({loading: true}, () => {
+ const {actions, currentChannelId, currentTeamId} = this.props;
+
+ actions.getProfilesNotInChannel(
+ currentTeamId,
+ currentChannelId,
+ this.page + 1,
+ General.PROFILE_CHUNK_SIZE
+ ).then(this.loadedProfiles);
+ });
+ }
+ }, 100);
+
+ handleAddMembersPress = () => {
+ const {formatMessage} = this.context.intl;
+ const {actions, currentChannelId} = this.props;
+ const {selectedIds, adding} = this.state;
+ const membersToAdd = Object.keys(selectedIds).filter((id) => selectedIds[id]);
if (!membersToAdd.length) {
- Alert.alert('Add Members', 'You must select at least one member to add to the channel.');
+ Alert.alert(
+ formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'}),
+ formatMessage({
+ id: 'mobile.channel_members.add_members_alert',
+ defaultMessage: 'You must select at least one member to add to the channel.',
+ })
+ );
+
return;
}
- alertErrorIfInvalidPermissions(
- await actions.handleAddChannelMembers(currentChannel.id, membersToAdd)
- );
- };
+ if (!adding) {
+ this.enableAddOption(false);
+ this.setState({adding: true}, async () => {
+ const result = await actions.handleAddChannelMembers(currentChannelId, membersToAdd);
- handleRowSelect = (id) => {
- const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
-
- if (Object.values(selectedMembers).filter((selected) => selected).length) {
- this.emitCanAddMembers(true);
- } else {
- this.emitCanAddMembers(false);
- }
- this.setState({
- profiles: markSelectedProfiles(this.state.profiles, selectedMembers),
- selectedMembers,
- });
- };
-
- loadMoreMembers = () => {
- const {actions, loadMoreRequestStatus, currentChannel, currentTeam} = this.props;
- const {next, searching} = this.state;
- let {page} = this.state;
- if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) {
- page += 1;
- actions.getProfilesNotInChannel(currentTeam.id, currentChannel.id, page, General.PROFILE_CHUNK_SIZE).then(({data}) => {
- if (data && data.length) {
- this.setState({
- page,
- });
+ if (result.error) {
+ alertErrorIfInvalidPermissions(result);
+ this.enableAddOption(true);
+ this.setState({adding: false});
} else {
- this.setState({next: false});
+ this.close();
}
});
}
};
+ handleSelectProfile = (id) => {
+ const {selectedIds} = this.state;
+ const newSelected = Object.assign({}, selectedIds, {[id]: !selectedIds[id]});
+
+ if (Object.values(newSelected).filter((selected) => selected).length) {
+ this.enableAddOption(true);
+ } else {
+ this.enableAddOption(false);
+ }
+
+ this.setState({selectedIds: newSelected});
+ };
+
+ loadedProfiles = ({data}) => {
+ const {profiles} = this.state;
+ if (data && !data.length) {
+ this.next = false;
+ }
+
+ this.page += 1;
+ this.setState({loading: false, profiles: [...profiles, ...data]});
+ };
+
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === this.addButton.id) {
@@ -213,34 +190,104 @@ class ChannelAddMembers extends PureComponent {
}
};
- searchProfiles = (text) => {
- const term = text;
- const {actions, currentChannel, currentTeam} = this.props;
+ onSearch = (text) => {
+ const term = text.toLowerCase();
if (term) {
- this.setState({searching: true, term});
+ this.setState({term});
clearTimeout(this.searchTimeoutId);
this.searchTimeoutId = setTimeout(() => {
- actions.searchProfiles(term.toLowerCase(), {not_in_channel_id: currentChannel.id, team_id: currentTeam.id});
+ this.searchProfiles(term);
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
- this.cancelSearch();
+ this.clearSearch();
}
};
+ renderItem = (props) => {
+ // The list will re-render when the selection changes because it's passed into the list as extraData
+ const selected = this.state.selectedIds[props.id];
+
+ return (
+
+ );
+ };
+
+ renderLoading = () => {
+ const {theme} = this.props;
+ const {loading} = this.state;
+ const style = getStyleFromTheme(theme);
+
+ if (!loading) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ renderNoResults = () => {
+ const {loading} = this.state;
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (loading || this.page === -1) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ searchProfiles = (term) => {
+ const {actions, currentChannelId, currentTeamId} = this.props;
+ const options = {not_in_channel_id: currentChannelId, team_id: currentTeamId};
+ this.setState({loading: true});
+
+ actions.searchProfiles(term, options).then(({data}) => {
+ this.setState({searchResults: data, loading: false});
+ });
+ };
+
render() {
- const {intl, preferences, theme} = this.props;
- const {adding, profiles, searching, term} = this.state;
- const {formatMessage} = intl;
- const more = searching ? () => true : this.loadMoreMembers;
+ const {formatMessage} = this.context.intl;
+ const {currentUserId, theme} = this.props;
+ const {
+ adding,
+ loading,
+ profiles,
+ searchResults,
+ selectedIds,
+ term,
+ } = this.state;
+ const style = getStyleFromTheme(theme);
if (adding) {
return (
-
+
-
+
);
}
@@ -255,12 +302,33 @@ class ChannelAddMembers extends PureComponent {
}),
};
+ let data;
+ let listType;
+ if (term) {
+ const exactMatches = [];
+ const results = filterProfilesMatchingTerm(searchResults, term).filter((p) => {
+ if (p.id === currentUserId) {
+ return false;
+ }
+
+ if (p.username === term || p.username.startsWith(term)) {
+ exactMatches.push(p);
+ return false;
+ }
+
+ return true;
+ });
+ data = [...exactMatches, ...results];
+ listType = FLATLIST;
+ } else {
+ data = createProfilesSections(profiles);
+ listType = SECTIONLIST;
+ }
+
return (
-
+
);
}
}
-export default injectIntl(ChannelAddMembers);
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ },
+ searchBar: {
+ marginVertical: 5,
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ height: 70,
+ justifyContent: 'center',
+ },
+ loadingText: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ },
+ noResultContainer: {
+ flexGrow: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ noResultText: {
+ fontSize: 26,
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ },
+ };
+});
diff --git a/app/screens/channel_add_members/index.js b/app/screens/channel_add_members/index.js
index 3dda9f28f..2708e8794 100644
--- a/app/screens/channel_add_members/index.js
+++ b/app/screens/channel_add_members/index.js
@@ -4,27 +4,24 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {handleAddChannelMembers} from 'app/actions/views/channel_add_members';
-import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
-import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
-import {getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {getTeamStats} from 'mattermost-redux/actions/teams';
import {getProfilesNotInChannel, searchProfiles} from 'mattermost-redux/actions/users';
+import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
+import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
+import {getCurrentUserId, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
+
+import {handleAddChannelMembers} from 'app/actions/views/channel_add_members';
import ChannelAddMembers from './channel_add_members';
function mapStateToProps(state) {
return {
- theme: getTheme(state),
- currentChannel: getCurrentChannel(state) || {},
+ currentChannelId: getCurrentChannelId(state),
+ currentTeamId: getCurrentTeamId(state),
+ currentUserId: getCurrentUserId(state),
membersNotInChannel: getProfilesNotInCurrentChannel(state),
- currentTeam: getCurrentTeam(state) || {},
- preferences: getMyPreferences(state),
- loadMoreRequestStatus: state.requests.users.getProfilesNotInChannel.status,
- addChannelMemberRequestStatus: state.requests.channels.addChannelMember,
- searchRequestStatus: state.requests.users.searchProfiles.status,
- addChannelMemberStatus: state.requests.channels.addChannelMember.status,
+ theme: getTheme(state),
};
}
diff --git a/app/screens/channel_members/channel_members.js b/app/screens/channel_members/channel_members.js
index b63a9c3ab..e897fae18 100644
--- a/app/screens/channel_members/channel_members.js
+++ b/app/screens/channel_members/channel_members.js
@@ -5,66 +5,67 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
- InteractionManager,
Platform,
View,
} from 'react-native';
-import {injectIntl, intlShape} from 'react-intl';
+import {intlShape} from 'react-intl';
+
+import {debounce} from 'mattermost-redux/actions/helpers';
+import {General} from 'mattermost-redux/constants';
+import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import Loading from 'app/components/loading';
-import CustomList from 'app/components/custom_list';
+import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list';
+import UserListRow from 'app/components/custom_list/user_list_row';
+import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {alertErrorIfInvalidPermissions} from 'app/utils/general';
-import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list';
-import UserListRow from 'app/components/custom_list/user_list_row';
-import {changeOpacity, setNavigatorStyles} from 'app/utils/theme';
+import {createProfilesSections, loadingText} from 'app/utils/member_list';
+import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
-import {General, RequestStatus} from 'mattermost-redux/constants';
-import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
-
-class ChannelMembers extends PureComponent {
+export default class ChannelMembers extends PureComponent {
static propTypes = {
- intl: intlShape.isRequired,
- theme: PropTypes.object.isRequired,
- currentChannel: PropTypes.object,
- currentChannelMembers: PropTypes.array,
- currentUserId: PropTypes.string.isRequired,
- navigator: PropTypes.object,
- requestStatus: PropTypes.string,
- searchRequestStatus: PropTypes.string,
- removeMembersStatus: PropTypes.string,
- canManageUsers: PropTypes.bool.isRequired,
actions: PropTypes.shape({
getProfilesInChannel: PropTypes.func.isRequired,
handleRemoveChannelMembers: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
- }),
+ }).isRequired,
+ canManageUsers: PropTypes.bool.isRequired,
+ currentChannelId: PropTypes.string.isRequired,
+ currentChannelMembers: PropTypes.array,
+ currentUserId: PropTypes.string.isRequired,
+ navigator: PropTypes.object,
+ theme: PropTypes.object.isRequired,
};
- removeButton = {
- disabled: true,
- id: 'remove-members',
- showAsAction: 'always',
+ static contextTypes = {
+ intl: intlShape.isRequired,
};
- constructor(props) {
- super(props);
+ constructor(props, context) {
+ super(props, context);
this.searchTimeoutId = 0;
+ this.page = -1;
+ this.next = true;
this.state = {
- canSelect: true,
- next: true,
- page: 0,
+ loading: false,
profiles: [],
- searching: false,
- selectedMembers: {},
- showNoResults: false,
+ removing: false,
+ searchResults: [],
+ selectedIds: {},
term: '',
};
- this.removeButton.title = props.intl.formatMessage({id: 'channel_members_modal.remove', defaultMessage: 'Remove'});
+
+ this.removeButton = {
+ disabled: true,
+ id: 'remove-members',
+ showAsAction: 'always',
+ title: context.intl.formatMessage({id: 'channel_members_modal.remove', defaultMessage: 'Remove'}),
+ };
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
if (props.canManageUsers) {
@@ -75,71 +76,30 @@ class ChannelMembers extends PureComponent {
}
componentDidMount() {
- InteractionManager.runAfterInteractions(() => {
- this.props.actions.getProfilesInChannel(this.props.currentChannel.id, 0);
- });
-
- this.emitCanRemoveMembers(false);
+ this.getProfiles();
}
- componentWillReceiveProps(nextProps) {
- if (this.props.theme !== nextProps.theme) {
- setNavigatorStyles(this.props.navigator, nextProps.theme);
- }
+ componentDidUpdate(prevProps) {
+ const {navigator, theme} = this.props;
+ const {removing, selectedIds} = this.state;
+ const enabled = Object.keys(selectedIds).length > 0 && !removing;
- const {requestStatus} = this.props;
- if (requestStatus === RequestStatus.STARTED &&
- nextProps.requestStatus === RequestStatus.SUCCESS) {
- const {page} = this.state;
- const profiles = markSelectedProfiles(
- nextProps.currentChannelMembers.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE),
- this.state.selectedMembers
- );
- this.setState({profiles, showNoResults: true});
- } else if (this.state.searching &&
- nextProps.searchRequestStatus === RequestStatus.SUCCESS) {
- const results = markSelectedProfiles(
- filterProfilesMatchingTerm(nextProps.currentChannelMembers, this.state.term),
- this.state.selectedMembers
- );
- this.setState({profiles: results, showNoResults: true});
- }
+ this.enableRemoveOption(enabled);
- const {removeMembersStatus} = nextProps;
-
- if (this.props.removeMembersStatus !== removeMembersStatus) {
- switch (removeMembersStatus) {
- case RequestStatus.STARTED:
- this.emitRemoving(true);
- this.setState({error: null, canSelect: false, removing: true});
- break;
- case RequestStatus.SUCCESS:
- this.emitRemoving(false);
- this.setState({error: null, canSelect: false, removing: false});
- this.close();
- break;
- case RequestStatus.FAILURE:
- this.emitRemoving(false);
- this.setState({canSelect: true, removing: false});
- break;
- }
+ if (theme !== prevProps.theme) {
+ setNavigatorStyles(navigator, theme);
}
}
- cancelSearch = () => {
- this.setState({
- searching: false,
- term: '',
- page: 0,
- profiles: markSelectedProfiles(this.props.currentChannelMembers, this.state.selectedMembers),
- });
+ clearSearch = () => {
+ this.setState({term: '', searchResults: []});
};
close = () => {
this.props.navigator.pop({animated: true});
};
- emitCanRemoveMembers = (enabled) => {
+ enableRemoveOption = (enabled) => {
if (this.props.canManageUsers) {
this.props.navigator.setButtons({
rightButtons: [{...this.removeButton, disabled: !enabled}],
@@ -147,21 +107,26 @@ class ChannelMembers extends PureComponent {
}
};
- emitRemoving = (loading) => {
- this.setState({canSelect: false, removing: loading});
+ getProfiles = debounce(() => {
+ const {loading, term} = this.state;
+ if (this.next && !loading && !term) {
+ this.setState({loading: true}, () => {
+ const {actions, currentChannelId} = this.props;
- if (this.props.canManageUsers) {
- this.props.navigator.setButtons({
- rightButtons: [{...this.removeButton, disabled: loading}],
+ actions.getProfilesInChannel(
+ currentChannelId,
+ this.page + 1,
+ General.PROFILE_CHUNK_SIZE
+ ).then(this.loadedProfiles);
});
}
- };
+ }, 100);
handleRemoveMembersPress = () => {
- const {selectedMembers} = this.state;
- const membersToRemove = Object.keys(selectedMembers).filter((m) => selectedMembers[m]);
+ const {formatMessage} = this.context.intl;
+ const {selectedIds, removing} = this.state;
+ const membersToRemove = Object.keys(selectedIds).filter((id) => selectedIds[id]);
- const {formatMessage} = this.props.intl;
if (!membersToRemove.length) {
Alert.alert(
formatMessage({
@@ -176,113 +141,176 @@ class ChannelMembers extends PureComponent {
return;
}
- Alert.alert(
- formatMessage({
- id: 'mobile.routes.channel_members.action',
- defaultMessage: 'Remove Members',
- }),
- formatMessage({
- id: 'mobile.routes.channel_members.action_message_confirm',
- defaultMessage: 'Are you sure you want to remove the selected members from the channel?',
- }),
- [{
- text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'}),
- }, {
- text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}),
- onPress: () => this.removeMembers(membersToRemove),
- }]
- );
- };
-
- handleRowSelect = (id) => {
- const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
- if (Object.values(selectedMembers).filter((selected) => selected).length) {
- this.emitCanRemoveMembers(true);
- } else {
- this.emitCanRemoveMembers(false);
- }
- this.setState({
- profiles: markSelectedProfiles(this.state.profiles, selectedMembers),
- selectedMembers,
- });
- };
-
- loadMoreMembers = () => {
- const {actions, requestStatus, currentChannel} = this.props;
- const {next, searching} = this.state;
- let {page} = this.state;
- if (requestStatus !== RequestStatus.STARTED && next && !searching) {
- page += 1;
- actions.getProfilesInChannel(currentChannel.id, page, General.PROFILE_CHUNK_SIZE).then(
- ({data}) => {
- if (data && data.length) {
- this.setState({
- page,
- });
- } else {
- this.setState({next: false});
- }
- }
+ if (!removing) {
+ Alert.alert(
+ formatMessage({
+ id: 'mobile.routes.channel_members.action',
+ defaultMessage: 'Remove Members',
+ }),
+ formatMessage({
+ id: 'mobile.routes.channel_members.action_message_confirm',
+ defaultMessage: 'Are you sure you want to remove the selected members from the channel?',
+ }),
+ [{
+ text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'}),
+ }, {
+ text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}),
+ onPress: () => this.removeMembers(membersToRemove),
+ }]
);
}
};
+ handleSelectProfile = (id) => {
+ const {canManageUsers} = this.props;
+ const {selectedIds} = this.state;
+
+ if (canManageUsers) {
+ const newSelected = Object.assign({}, selectedIds, {[id]: !selectedIds[id]});
+
+ if (Object.values(newSelected).filter((selected) => selected).length) {
+ this.enableRemoveOption(true);
+ } else {
+ this.enableRemoveOption(false);
+ }
+
+ this.setState({selectedIds: newSelected});
+ }
+ };
+
+ loadedProfiles = ({data}) => {
+ const {profiles} = this.state;
+ if (data && !data.length) {
+ this.next = false;
+ }
+
+ this.page += 1;
+ this.setState({loading: false, profiles: [...profiles, ...data]});
+ };
+
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
- if (event.id === 'remove-members') {
+ if (event.id === this.removeButton.id) {
this.handleRemoveMembersPress();
}
}
};
- removeMembers = async (membersToRemove) => {
- const {actions, currentChannel} = this.props;
- alertErrorIfInvalidPermissions(
- await actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove)
- );
+ onSearch = (text) => {
+ const term = text.toLowerCase();
+
+ if (term) {
+ this.setState({term});
+ clearTimeout(this.searchTimeoutId);
+
+ this.searchTimeoutId = setTimeout(() => {
+ this.searchProfiles(term);
+ }, General.SEARCH_TIMEOUT_MILLISECONDS);
+ } else {
+ this.clearSearch();
+ }
};
- renderMemberRow = (props) => {
+ removeMembers = async (membersToRemove) => {
+ const {actions, currentChannelId} = this.props;
+ this.enableRemoveOption(false);
+ this.setState({adding: true}, async () => {
+ const result = await actions.handleRemoveChannelMembers(currentChannelId, membersToRemove);
+
+ if (result.error) {
+ alertErrorIfInvalidPermissions(result);
+ this.enableRemoveOption(true);
+ this.setState({removing: false});
+ } else {
+ this.close();
+ }
+ });
+ };
+
+ renderItem = (props) => {
+ // The list will re-render when the selection changes because it's passed into the list as extraData
+ const selected = this.state.selectedIds[props.id];
const enabled = props.id !== this.props.currentUserId;
return (
);
};
- searchProfiles = (text) => {
- const term = text;
+ renderLoading = () => {
+ const {theme} = this.props;
+ const {loading} = this.state;
+ const style = getStyleFromTheme(theme);
- if (term) {
- this.setState({searching: true, term});
- clearTimeout(this.searchTimeoutId);
-
- this.searchTimeoutId = setTimeout(() => {
- this.props.actions.searchProfiles(term.toLowerCase(), {in_channel_id: this.props.currentChannel.id});
- }, General.SEARCH_TIMEOUT_MILLISECONDS);
- } else {
- this.cancelSearch();
+ if (!loading) {
+ return null;
}
+
+ return (
+
+
+
+ );
+ };
+
+ renderNoResults = () => {
+ const {loading} = this.state;
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (loading || this.page === -1) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ searchProfiles = (term) => {
+ const {actions, currentChannelId} = this.props;
+ const options = {in_channel_id: currentChannelId};
+ this.setState({loading: true});
+
+ actions.searchProfiles(term, options).then(({data}) => {
+ this.setState({searchResults: data, loading: false});
+ });
};
render() {
- const {canManageUsers, intl, requestStatus, searchRequestStatus, theme} = this.props;
- const {formatMessage} = intl;
- const {profiles, removing, searching, showNoResults, term} = this.state;
- const isLoading = (requestStatus === RequestStatus.STARTED) || (requestStatus.status === RequestStatus.NOT_STARTED) ||
- (searchRequestStatus === RequestStatus.STARTED);
- const more = searching ? () => true : this.loadMoreMembers;
+ const {formatMessage} = this.context.intl;
+ const {theme} = this.props;
+ const {
+ removing,
+ loading,
+ profiles,
+ searchResults,
+ selectedIds,
+ term,
+ } = this.state;
+ const style = getStyleFromTheme(theme);
if (removing) {
return (
-
+
-
+
);
}
@@ -297,12 +325,29 @@ class ChannelMembers extends PureComponent {
}),
};
+ let data;
+ let listType;
+ if (term) {
+ const exactMatches = [];
+ const results = filterProfilesMatchingTerm(searchResults, term).filter((p) => {
+ if (p.username === term || p.username.startsWith(term)) {
+ exactMatches.push(p);
+ return false;
+ }
+
+ return true;
+ });
+ data = [...exactMatches, ...results];
+ listType = FLATLIST;
+ } else {
+ data = createProfilesSections(profiles);
+ listType = SECTIONLIST;
+ }
+
return (
-
+
);
}
}
-export default injectIntl(ChannelMembers);
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ },
+ searchBar: {
+ marginVertical: 5,
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ height: 70,
+ justifyContent: 'center',
+ },
+ loadingText: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ },
+ noResultContainer: {
+ flexGrow: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ noResultText: {
+ fontSize: 26,
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ },
+ };
+});
diff --git a/app/screens/channel_members/index.js b/app/screens/channel_members/index.js
index 564161070..64bdef801 100644
--- a/app/screens/channel_members/index.js
+++ b/app/screens/channel_members/index.js
@@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
+import {getCurrentChannelId, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
import {makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users';
@@ -16,21 +16,21 @@ function makeMapStateToProps() {
const getChannelMembers = makeGetProfilesInChannel();
return (state) => {
- const currentChannel = getCurrentChannel(state) || {};
+ const currentChannelId = getCurrentChannelId(state);
let currentChannelMembers = [];
- if (currentChannel) {
- currentChannelMembers = getChannelMembers(state, currentChannel.id, true);
+ if (currentChannelId) {
+ currentChannelMembers = getChannelMembers(state, currentChannelId, true);
}
return {
- theme: getTheme(state),
- currentChannel,
+ canManageUsers: canManageChannelMembers(state),
+ currentChannelId,
currentChannelMembers,
currentUserId: state.entities.users.currentUserId,
requestStatus: state.requests.users.getProfilesInChannel.status,
searchRequestStatus: state.requests.users.searchProfiles.status,
removeMembersStatus: state.requests.channels.removeChannelMember.status,
- canManageUsers: canManageChannelMembers(state),
+ theme: getTheme(state),
};
};
}
diff --git a/app/screens/create_channel/create_channel.js b/app/screens/create_channel/create_channel.js
index 4433bf82d..14cc18317 100644
--- a/app/screens/create_channel/create_channel.js
+++ b/app/screens/create_channel/create_channel.js
@@ -110,7 +110,6 @@ export default class CreateChannel extends PureComponent {
}
close = (goBack = false) => {
- EventEmitter.emit('closing-create-channel', false);
if (goBack) {
this.props.navigator.pop({animated: true});
} else {
diff --git a/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap b/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap
index f13d40805..f695b1e5d 100644
--- a/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap
+++ b/app/screens/menu_action_selector/__snapshots__/menu_action_selector.test.js.snap
@@ -1,525 +1,524 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MenuActionSelector should match snapshot for channels 1`] = `
-
+
+
+
+
+
+ />
+
`;
exports[`MenuActionSelector should match snapshot for channels 2`] = `
-
+
+
+
+
+
+ />
+
`;
exports[`MenuActionSelector should match snapshot for explicit options 1`] = `
-
+
+
+
+
+
+ />
+
`;
exports[`MenuActionSelector should match snapshot for searching 1`] = `
-
+
+
+
+
+
+ />
+
`;
exports[`MenuActionSelector should match snapshot for users 1`] = `
-
+
+
+
+
+
+ />
+
`;
exports[`MenuActionSelector should match snapshot for users 2`] = `
-
+
+
+
+
+
+ />
+
`;
diff --git a/app/screens/menu_action_selector/index.js b/app/screens/menu_action_selector/index.js
index d4f08a3b4..bc6713709 100644
--- a/app/screens/menu_action_selector/index.js
+++ b/app/screens/menu_action_selector/index.js
@@ -4,50 +4,31 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {getChannelsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
-import {getProfiles} from 'mattermost-redux/selectors/entities/users';
-import {getProfiles as getProfilesAction, searchProfiles} from 'mattermost-redux/actions/users';
+import {getProfiles, searchProfiles} from 'mattermost-redux/actions/users';
import {getChannels, searchChannels} from 'mattermost-redux/actions/channels';
-import {ViewTypes} from 'app/constants';
-
import MenuActionSelector from './menu_action_selector';
function mapStateToProps(state) {
const menuAction = state.views.post.selectedMenuAction || {};
- let data;
- let loadMoreRequestStatus;
- let searchRequestStatus;
- if (menuAction.dataSource === ViewTypes.DATA_SOURCE_USERS) {
- data = getProfiles(state);
- loadMoreRequestStatus = state.requests.users.getProfiles.status;
- searchRequestStatus = state.requests.users.searchProfiles.status;
- } else if (menuAction.dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
- data = getChannelsInCurrentTeam(state);
- loadMoreRequestStatus = state.requests.channels.getChannels.status;
- searchRequestStatus = state.requests.channels.getChannels.status;
- } else {
- data = menuAction.options || [];
- }
+ const data = menuAction.options || [];
return {
+ currentTeamId: getCurrentTeamId(state),
data,
dataSource: menuAction.dataSource,
onSelect: menuAction.onSelect,
theme: getTheme(state),
- currentTeamId: getCurrentTeamId(state),
- loadMoreRequestStatus,
- searchRequestStatus,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
- getProfiles: getProfilesAction,
+ getProfiles,
getChannels,
searchProfiles,
searchChannels,
diff --git a/app/screens/menu_action_selector/menu_action_selector.js b/app/screens/menu_action_selector/menu_action_selector.js
index 28b2b583f..325fbe611 100644
--- a/app/screens/menu_action_selector/menu_action_selector.js
+++ b/app/screens/menu_action_selector/menu_action_selector.js
@@ -3,82 +3,80 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {injectIntl, intlShape} from 'react-intl';
+import {intlShape} from 'react-intl';
import {
- InteractionManager,
Platform,
View,
} from 'react-native';
-import CustomList from 'app/components/custom_list';
-import UserListRow from 'app/components/custom_list/user_list_row';
-import ChannelListRow from 'app/components/custom_list/channel_list_row';
-import OptionListRow from 'app/components/custom_list/option_list_row';
-import SearchBar from 'app/components/search_bar';
-import StatusBar from 'app/components/status_bar';
-import {loadingText} from 'app/utils/member_list';
-import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
-import {ViewTypes} from 'app/constants';
-
-import {General, RequestStatus} from 'mattermost-redux/constants';
+import {debounce} from 'mattermost-redux/actions/helpers';
+import {General} from 'mattermost-redux/constants';
import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import {filterChannelsMatchingTerm} from 'mattermost-redux/utils/channel_utils';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
-class MenuActionSelector extends PureComponent {
+import CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list';
+import UserListRow from 'app/components/custom_list/user_list_row';
+import ChannelListRow from 'app/components/custom_list/channel_list_row';
+import OptionListRow from 'app/components/custom_list/option_list_row';
+import FormattedText from 'app/components/formatted_text';
+import SearchBar from 'app/components/search_bar';
+import StatusBar from 'app/components/status_bar';
+import {ViewTypes} from 'app/constants';
+import {createProfilesSections, loadingText} from 'app/utils/member_list';
+import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
+import {t} from 'app/utils/i18n';
+
+export default class MenuActionSelector extends PureComponent {
static propTypes = {
- intl: intlShape.isRequired,
- theme: PropTypes.object.isRequired,
- navigator: PropTypes.object,
- data: PropTypes.arrayOf(PropTypes.object),
- dataSource: PropTypes.string,
- onSelect: PropTypes.func.isRequired,
- currentTeamId: PropTypes.string.isRequired,
- loadMoreRequestStatus: PropTypes.string,
- searchRequestStatus: PropTypes.string,
actions: PropTypes.shape({
getProfiles: PropTypes.func.isRequired,
getChannels: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
searchChannels: PropTypes.func.isRequired,
}),
+ currentTeamId: PropTypes.string.isRequired,
+ data: PropTypes.arrayOf(PropTypes.object),
+ dataSource: PropTypes.string,
+ navigator: PropTypes.object,
+ onSelect: PropTypes.func.isRequired,
+ theme: PropTypes.object.isRequired,
+ };
+
+ static contextTypes = {
+ intl: intlShape.isRequired,
};
constructor(props) {
super(props);
this.searchTimeoutId = 0;
+ this.page = -1;
+ this.next = props.dataSource === ViewTypes.DATA_SOURCE_USERS || props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS;
let data = [];
if (!props.dataSource) {
data = props.data;
}
- const needsLoading = props.dataSource === ViewTypes.DATA_SOURCE_USERS || props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS;
-
this.state = {
- next: needsLoading,
- page: 0,
data,
- searching: false,
- showNoResults: false,
+ loading: false,
+ searchResults: [],
term: '',
- isLoading: needsLoading,
- prevLoadMoreRequestStatus: RequestStatus.STARTED,
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
componentDidMount() {
+ const {dataSource} = this.props;
this.mounted = true;
- InteractionManager.runAfterInteractions(() => {
- if (this.props.dataSource === ViewTypes.DATA_SOURCE_USERS) {
- this.props.actions.getProfiles().then(() => this.setState({isLoading: false}));
- } else if (this.props.dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
- this.props.actions.getChannels(this.props.currentTeamId).then(() => this.setState({isLoading: false}));
- }
- });
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ this.getProfiles();
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ this.getChannels();
+ }
}
componentWillUnmount() {
@@ -91,97 +89,199 @@ class MenuActionSelector extends PureComponent {
}
}
- cancelSearch = () => {
- this.setState({
- searching: false,
- isLoading: false,
- term: '',
- page: 0,
- data: filterPageData(this.props.data, 0),
- });
+ clearSearch = () => {
+ this.setState({term: '', searchResults: []});
};
close = () => {
this.props.navigator.pop({animated: true});
};
- handleRowSelect = (id, selected) => {
- this.props.onSelect(selected);
+ handleSelectItem = (id, item) => {
+ this.props.onSelect(item);
this.close();
};
- loadMore = async () => {
- const {actions, loadMoreRequestStatus, currentTeamId, dataSource} = this.props;
- const {next, searching} = this.state;
- let {page} = this.state;
+ getChannels = debounce(() => {
+ const {actions, currentTeamId} = this.props;
+ const {loading, term} = this.state;
+ if (this.next && !loading && !term) {
+ this.setState({loading: true}, () => {
+ actions.getChannels(
+ currentTeamId,
+ this.page += 1,
+ General.CHANNELS_CHUNK_SIZE
+ ).then(this.loadedChannels);
+ });
+ }
+ }, 100);
- if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) {
- page += 1;
+ getDataResults = () => {
+ const {dataSource} = this.props;
+ const {data, searchResults, term} = this.state;
- let results;
- if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
- results = await actions.getProfiles(page, General.PROFILE_CHUNK_SIZE);
- } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
- results = await actions.getChannels(currentTeamId, page, General.PROFILE_CHUNK_SIZE);
- } else {
- return;
- }
+ const result = {
+ data,
+ listType: FLATLIST};
+ if (term) {
+ result.data = filterSearchData(dataSource, searchResults, term);
+ } else if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ result.data = createProfilesSections(data);
+ result.listType = SECTIONLIST;
+ }
- if (!this.mounted) {
- return;
- }
+ return result;
+ };
- if (results.data && results.data.length) {
- this.setState({
- isLoading: false,
- page,
- });
- } else {
- this.setState({
- isLoading: false,
- next: false,
- });
- }
+ getProfiles = debounce(() => {
+ const {loading, term} = this.state;
+ if (this.next && !loading && !term) {
+ this.setState({loading: true}, () => {
+ const {actions} = this.props;
+
+ actions.getProfiles(
+ this.page + 1,
+ General.PROFILE_CHUNK_SIZE
+ ).then(this.loadedProfiles);
+ });
+ }
+ }, 100);
+
+ loadedChannels = ({data: channels}) => {
+ const {data} = this.state;
+ if (channels && !channels.length) {
+ this.next = false;
+ }
+
+ this.page += 1;
+ this.setState({loading: false, data: [...channels, ...data]});
+ };
+
+ loadedProfiles = ({data: profiles}) => {
+ const {data} = this.state;
+ if (profiles && !profiles.length) {
+ this.next = false;
+ }
+
+ this.page += 1;
+ this.setState({loading: false, data: [...profiles, ...data]});
+ };
+
+ loadMore = () => {
+ const {dataSource} = this.props;
+
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ this.getProfiles();
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ this.getChannels();
}
};
- searchProfiles = (text) => {
- const term = text;
- const {actions, currentTeamId, dataSource} = this.props;
+ onSearch = (text) => {
+ const term = text.toLowerCase();
if (term) {
- if (!dataSource) {
- this.setState({data: filterSearchData(null, this.props.data, term.toLowerCase())});
- return;
- }
-
- this.setState({searching: true, isLoading: true, term});
+ const {dataSource, data} = this.props;
+ this.setState({term});
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(async () => {
- if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
- await actions.searchProfiles(term.toLowerCase());
- } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
- await actions.searchChannels(currentTeamId, term.toLowerCase());
- }
-
- if (!this.mounted) {
+ this.searchTimeoutId = setTimeout(() => {
+ if (!dataSource) {
+ this.setState({searchResults: filterSearchData(null, data, term)});
return;
}
- this.setState({isLoading: false});
+ if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
+ this.searchProfiles(term);
+ } else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
+ this.searchChannels(term);
+ }
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
- this.cancelSearch();
+ this.clearSearch();
}
};
- render() {
- const {intl, data, theme, dataSource} = this.props;
- const {searching, term, page} = this.state;
- const {formatMessage} = intl;
+ searchChannels = (term) => {
+ const {actions, currentTeamId} = this.props;
+
+ actions.searchChannels(currentTeamId, term).then(({data}) => {
+ this.setState({searchResults: data, loading: false});
+ });
+ };
+
+ searchProfiles = (term) => {
+ const {actions} = this.props;
+ this.setState({loading: true});
+
+ actions.searchProfiles(term).then(({data}) => {
+ this.setState({searchResults: data, loading: false});
+ });
+ };
+
+ renderLoading = () => {
+ const {dataSource, theme} = this.props;
+ const {loading} = this.state;
+ const style = getStyleFromTheme(theme);
+
+ if (!loading) {
+ return null;
+ }
+
+ let text;
+ switch (dataSource) {
+ case ViewTypes.DATA_SOURCE_USERS:
+ text = loadingText;
+ break;
+ case ViewTypes.DATA_SOURCE_CHANNELS:
+ text = {
+ id: t('mobile.loading_channels'),
+ defaultMessage: 'Loading Channels...',
+ };
+ break;
+ default:
+ text = {
+ id: t('mobile.loading_options'),
+ defaultMessage: 'Loading Options...',
+ };
+ break;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ renderNoResults = () => {
+ const {loading} = this.state;
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (loading || this.page === -1) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ render() {
+ const {formatMessage} = this.context.intl;
+ const {theme, dataSource} = this.props;
+ const {loading, term} = this.state;
const style = getStyleFromTheme(theme);
- const more = searching ? () => true : this.loadMore;
const searchBarInput = {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
@@ -203,19 +303,12 @@ class MenuActionSelector extends PureComponent {
rowComponent = OptionListRow;
}
- let filteredData;
- if (searching) {
- filteredData = filterSearchData(dataSource, data, term);
- } else {
- filteredData = filterPageData(data, page);
- }
+ const {data, listType} = this.getDataResults();
return (
-
+
);
@@ -259,16 +350,31 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flex: 1,
backgroundColor: theme.centerChannelBg,
},
- searchContainer: {
+ searchBar: {
marginVertical: 5,
},
+ loadingContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ height: 70,
+ justifyContent: 'center',
+ },
+ loadingText: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ },
+ noResultContainer: {
+ flexGrow: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ noResultText: {
+ fontSize: 26,
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ },
};
});
-const filterPageData = memoizeResult((data, page) => {
- return data.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE);
-});
-
const filterSearchData = memoizeResult((dataSource, data, term) => {
if (!data) {
return [];
@@ -282,5 +388,3 @@ const filterSearchData = memoizeResult((dataSource, data, term) => {
return data.filter((option) => option.text && option.text.toLowerCase().startsWith(term));
});
-
-export default injectIntl(MenuActionSelector);
diff --git a/app/screens/more_channels/__snapshots__/more_channels.test.js.snap b/app/screens/more_channels/__snapshots__/more_channels.test.js.snap
index 3be047f24..f1090561f 100644
--- a/app/screens/more_channels/__snapshots__/more_channels.test.js.snap
+++ b/app/screens/more_channels/__snapshots__/more_channels.test.js.snap
@@ -37,7 +37,6 @@ exports[`MoreChannels should match snapshot 1`] = `
/>
+
+
}
- onListEndReached={[Function]}
- onListEndReachedThreshold={50}
+ noResults={
+
+
+
+ }
+ onLoadMore={[Function]}
onRowPress={[Function]}
- renderRow={[Function]}
- searching={false}
- selectable={false}
- showNoResults={false}
- showSections={false}
+ renderItem={[Function]}
+ showNoResults={true}
theme={
Object {
"awayIndicator": "#ffbc42",
diff --git a/app/screens/more_channels/index.js b/app/screens/more_channels/index.js
index 096e644cc..679744979 100644
--- a/app/screens/more_channels/index.js
+++ b/app/screens/more_channels/index.js
@@ -8,7 +8,8 @@ import {createSelector} from 'reselect';
import {General} from 'mattermost-redux/constants';
import {getChannels, joinChannel, searchChannels} from 'mattermost-redux/actions/channels';
import {getChannelsInCurrentTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
-import {getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
+import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
+import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
@@ -29,21 +30,18 @@ const joinableChannels = createSelector(
);
function mapStateToProps(state) {
- const {currentUserId} = state.entities.users;
- const {currentTeamId} = state.entities.teams;
- const {getChannels: requestStatus} = state.requests.channels;
const config = getConfig(state);
const license = getLicense(state);
const roles = getCurrentUserRoles(state);
const channels = joinableChannels(state);
+ const currentTeamId = getCurrentTeamId(state);
return {
canCreateChannels: showCreateOption(state, config, license, currentTeamId, General.OPEN_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
- currentUserId,
+ currentUserId: getCurrentUserId(state),
currentTeamId,
channels,
theme: getTheme(state),
- requestStatus,
};
}
diff --git a/app/screens/more_channels/more_channels.js b/app/screens/more_channels/more_channels.js
index 8a7e0f810..27e168d4f 100644
--- a/app/screens/more_channels/more_channels.js
+++ b/app/screens/more_channels/more_channels.js
@@ -4,36 +4,24 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
-import {
- Platform,
- InteractionManager,
- StyleSheet,
- View,
-} from 'react-native';
+import {Platform, View} from 'react-native';
-import {General, RequestStatus} from 'mattermost-redux/constants';
+import {debounce} from 'mattermost-redux/actions/helpers';
+import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import CustomList from 'app/components/custom_list';
import ChannelListRow from 'app/components/custom_list/channel_list_row';
+import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {alertErrorWithFallback} from 'app/utils/general';
-import {changeOpacity, setNavigatorStyles} from 'app/utils/theme';
-import {t} from 'app/utils/i18n';
+import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
export default class MoreChannels extends PureComponent {
static propTypes = {
- currentUserId: PropTypes.string.isRequired,
- currentTeamId: PropTypes.string.isRequired,
- navigator: PropTypes.object,
- theme: PropTypes.object.isRequired,
- canCreateChannels: PropTypes.bool.isRequired,
- channels: PropTypes.array,
- closeButton: PropTypes.object,
- requestStatus: PropTypes.object.isRequired,
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
joinChannel: PropTypes.func.isRequired,
@@ -41,38 +29,43 @@ export default class MoreChannels extends PureComponent {
searchChannels: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}).isRequired,
+ canCreateChannels: PropTypes.bool.isRequired,
+ channels: PropTypes.array,
+ closeButton: PropTypes.object,
+ currentUserId: PropTypes.string.isRequired,
+ currentTeamId: PropTypes.string.isRequired,
+ navigator: PropTypes.object,
+ theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
- leftButton = {
- id: 'close-more-channels',
- };
-
- rightButton = {
- id: 'create-pub-channel',
- showAsAction: 'always',
- };
-
constructor(props, context) {
super(props, context);
this.searchTimeoutId = 0;
+ this.page = -1;
+ this.next = true;
this.state = {
channels: props.channels.slice(0, General.CHANNELS_CHUNK_SIZE),
- createScreenVisible: false,
- page: 0,
+ loading: false,
adding: false,
- next: true,
- searching: false,
- showNoResults: false,
term: '',
};
- this.rightButton.title = context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'});
- this.leftButton = {...this.leftButton, icon: props.closeButton};
+
+ this.rightButton = {
+ id: 'create-pub-channel',
+ title: context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}),
+ showAsAction: 'always',
+ };
+
+ this.leftButton = {
+ id: 'close-more-channels',
+ icon: props.closeButton,
+ };
const buttons = {
leftButtons: [this.leftButton],
@@ -86,136 +79,83 @@ export default class MoreChannels extends PureComponent {
props.navigator.setButtons(buttons);
}
- componentWillMount() {
- EventEmitter.on('closing-create-channel', this.handleCreateScreenVisible);
- }
-
componentDidMount() {
- // set the timeout to 400 cause is the time that the modal takes to open
- // Somehow interactionManager doesn't care
- setTimeout(() => {
- this.props.actions.getChannels(this.props.currentTeamId, 0);
- }, 400);
+ this.getChannels();
}
componentWillReceiveProps(nextProps) {
+ const {term} = this.state;
+ let channels;
+
if (this.props.theme !== nextProps.theme) {
setNavigatorStyles(this.props.navigator, nextProps.theme);
}
- const {page, searching, term} = this.state;
-
- let channels;
if (nextProps.channels !== this.props.channels) {
- channels = nextProps.channels.slice(0, (page + 1) * General.CHANNELS_CHUNK_SIZE);
+ channels = nextProps.channels.slice(0, (this.page + 1) * General.CHANNELS_CHUNK_SIZE);
if (term) {
channels = this.filterChannels(nextProps.channels, term);
}
}
- const {requestStatus} = this.props;
- if (
- searching &&
- nextProps.requestStatus.status === RequestStatus.SUCCESS
- ) {
- channels = this.filterChannels(nextProps.channels, term);
- } else if (
- requestStatus.status === RequestStatus.STARTED &&
- nextProps.requestStatus.status === RequestStatus.SUCCESS
- ) {
- channels = nextProps.channels.slice(0, (page + 1) * General.CHANNELS_CHUNK_SIZE);
- }
-
if (channels) {
- this.setState({channels, showNoResults: true});
- }
-
- if (!this.state.createScreenVisible) {
- this.headerButtons(nextProps.canCreateChannels, true);
+ this.setState({channels});
}
}
- componentWillUnmount() {
- EventEmitter.off('closing-create-channel', this.handleCreateScreenVisible);
- }
+ cancelSearch = () => {
+ const {channels} = this.props;
+
+ this.setState({
+ channels,
+ term: '',
+ });
+ };
close = () => {
this.props.navigator.dismissModal({animationType: 'slide-down'});
};
- emitCanCreateChannel = (enabled) => {
- this.headerButtons(this.props.canCreateChannels, enabled);
- };
-
- handleCreateScreenVisible = (createScreenVisible) => {
- this.setState({createScreenVisible});
- };
-
- headerButtons = (canCreateChannels, enabled) => {
- const buttons = {
- leftButtons: [this.leftButton],
- };
-
- if (canCreateChannels) {
- buttons.rightButtons = [{...this.rightButton, disabled: !enabled}];
- }
-
- this.props.navigator.setButtons(buttons);
- };
-
filterChannels = (channels, term) => {
return channels.filter((c) => {
return (c.name.toLowerCase().includes(term) || c.display_name.toLowerCase().includes(term));
});
};
- searchProfiles = (text) => {
- const term = text.toLowerCase();
-
- if (term) {
- const channels = this.filterChannels(this.props.channels, term);
- this.setState({
- channels,
- term,
- searching: true,
- });
- clearTimeout(this.searchTimeoutId);
-
- this.searchTimeoutId = setTimeout(() => {
- this.props.actions.searchChannels(this.props.currentTeamId, term);
- }, General.SEARCH_TIMEOUT_MILLISECONDS);
- } else {
- this.cancelSearch();
- }
- };
-
- cancelSearch = () => {
- this.props.actions.getChannels(this.props.currentTeamId, 0);
- this.setState({
- term: '',
- searching: false,
- page: 0,
- });
- };
-
- loadMoreChannels = () => {
- let {page} = this.state;
- if (this.props.requestStatus.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) {
- page += 1;
- this.props.actions.getChannels(
- this.props.currentTeamId,
- page,
- General.CHANNELS_CHUNK_SIZE
- ).then(({data}) => {
- if (data && data.length) {
- this.setState({
- page,
- });
- } else {
- this.setState({next: false});
- }
+ getChannels = debounce(() => {
+ const {actions, currentTeamId} = this.props;
+ const {loading, term} = this.state;
+ if (this.next && !loading && !term) {
+ this.setState({loading: true}, () => {
+ actions.getChannels(
+ currentTeamId,
+ this.page + 1,
+ General.CHANNELS_CHUNK_SIZE
+ ).then(this.loadedChannels);
});
}
+ }, 100);
+
+ headerButtons = (createEnabled) => {
+ const {canCreateChannels} = this.props;
+ const buttons = {
+ leftButtons: [this.leftButton],
+ };
+
+ if (canCreateChannels) {
+ buttons.rightButtons = [{...this.rightButton, disabled: !createEnabled}];
+ }
+
+ this.props.navigator.setButtons(buttons);
+ };
+
+ loadedChannels = ({data}) => {
+ if (data && !data.length) {
+ this.next = false;
+ }
+
+ this.page += 1;
+ this.setState({loading: false});
};
onNavigatorEvent = (event) => {
@@ -225,9 +165,7 @@ export default class MoreChannels extends PureComponent {
this.close();
break;
case 'create-pub-channel':
- this.setState({
- createScreenVisible: true,
- }, this.onCreateChannel);
+ this.onCreateChannel();
break;
}
}
@@ -238,7 +176,7 @@ export default class MoreChannels extends PureComponent {
const {actions, currentTeamId, currentUserId} = this.props;
const {channels} = this.state;
- this.emitCanCreateChannel(false);
+ this.headerButtons(false);
this.setState({adding: true});
const channel = channels.find((c) => c.id === id);
@@ -256,7 +194,7 @@ export default class MoreChannels extends PureComponent {
displayName: channel ? channel.display_name : '',
}
);
- this.emitCanCreateChannel(true);
+ this.headerButtons(true);
this.setState({adding: false});
} else {
if (channel) {
@@ -267,20 +205,20 @@ export default class MoreChannels extends PureComponent {
await actions.handleSelectChannel(id);
EventEmitter.emit('close_channel_drawer');
- InteractionManager.runAfterInteractions(() => {
+ requestAnimationFrame(() => {
this.close();
});
}
};
onCreateChannel = () => {
- const {intl} = this.context;
+ const {formatMessage} = this.context.intl;
const {navigator, theme} = this.props;
navigator.push({
screen: 'CreateChannel',
animationType: 'slide-up',
- title: intl.formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}),
+ title: formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}),
backButtonTitle: '',
animated: true,
navigatorStyle: {
@@ -295,13 +233,83 @@ export default class MoreChannels extends PureComponent {
});
};
+ renderLoading = () => {
+ const {theme, channels} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (!channels.length && this.page <= 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ renderNoResults = () => {
+ const {term, loading} = this.state;
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (loading) {
+ return null;
+ }
+
+ if (term) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ searchChannels = (text) => {
+ const {actions, channels, currentTeamId} = this.props;
+ const term = text.toLowerCase();
+
+ if (term) {
+ const filtered = this.filterChannels(channels, term);
+ this.setState({
+ channels: filtered,
+ term,
+ });
+ clearTimeout(this.searchTimeoutId);
+
+ this.searchTimeoutId = setTimeout(() => {
+ actions.searchChannels(currentTeamId, term);
+ }, General.SEARCH_TIMEOUT_MILLISECONDS);
+ } else {
+ this.cancelSearch();
+ }
+ };
+
render() {
- const {intl} = this.context;
- const {requestStatus, theme} = this.props;
- const {adding, channels, searching, term} = this.state;
- const {formatMessage} = intl;
- const isLoading = requestStatus.status === RequestStatus.STARTED || requestStatus.status === RequestStatus.NOT_STARTED;
- const more = searching ? () => true : this.loadMoreChannels;
+ const {formatMessage} = this.context.intl;
+ const {theme} = this.props;
+ const {adding, channels, loading, term} = this.state;
+ const more = term ? () => true : this.getChannels;
+ const style = getStyleFromTheme(theme);
let content;
if (adding) {
@@ -320,7 +328,7 @@ export default class MoreChannels extends PureComponent {
content = (
-
+
);
@@ -365,8 +372,29 @@ export default class MoreChannels extends PureComponent {
}
}
-const style = StyleSheet.create({
- searchbar: {
- marginVertical: 5,
- },
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ searchBar: {
+ marginVertical: 5,
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ height: 70,
+ justifyContent: 'center',
+ },
+ loadingText: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ },
+ noResultContainer: {
+ flexGrow: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ noResultText: {
+ fontSize: 26,
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ },
+ };
});
diff --git a/app/screens/more_channels/more_channels.test.js b/app/screens/more_channels/more_channels.test.js
index 89f8e862f..51787ffcc 100644
--- a/app/screens/more_channels/more_channels.test.js
+++ b/app/screens/more_channels/more_channels.test.js
@@ -12,31 +12,29 @@ jest.mock('react-intl');
describe('MoreChannels', () => {
const navigator = {
- setOnNavigatorEvent: () => {}, // eslint-disable-line no-empty-function
- setButtons: () => {}, // eslint-disable-line no-empty-function
- dismissModal: () => {}, // eslint-disable-line no-empty-function
- push: () => {}, // eslint-disable-line no-empty-function
+ setOnNavigatorEvent: jest.fn(),
+ setButtons: jest.fn(),
+ dismissModal: jest.fn(),
+ push: jest.fn(),
};
const actions = {
- handleSelectChannel: () => {}, // eslint-disable-line no-empty-function
- joinChannel: () => {}, // eslint-disable-line no-empty-function
- getChannels: () => {}, // eslint-disable-line no-empty-function
- removeHiddenDefaultChannel: () => {}, // eslint-disable-line no-empty-function
- searchChannels: () => {}, // eslint-disable-line no-empty-function
- setChannelDisplayName: () => {}, // eslint-disable-line no-empty-function
+ handleSelectChannel: jest.fn(),
+ joinChannel: jest.fn(),
+ getChannels: jest.fn().mockResolvedValue({data: [{id: 'id2', name: 'name2', display_name: 'display_name2'}]}),
+ searchChannels: jest.fn(),
+ setChannelDisplayName: jest.fn(),
};
const baseProps = {
+ actions,
+ canCreateChannels: true,
+ channels: [{id: 'id', name: 'name', display_name: 'display_name'}],
+ closeButton: {},
currentUserId: 'current_user_id',
currentTeamId: 'current_team_id',
navigator,
theme: Preferences.THEMES.default,
- canCreateChannels: true,
- channels: [{id: 'id', name: 'name', display_name: 'display_name'}],
- closeButton: {},
- requestStatus: {},
- actions,
};
test('should match snapshot', () => {
@@ -49,48 +47,14 @@ describe('MoreChannels', () => {
});
test('should call props.navigator.dismissModal on close', () => {
- const props = {...baseProps, navigator: {...navigator, dismissModal: jest.fn()}};
const wrapper = shallow(
- ,
+ ,
{context: {intl: {formatMessage: jest.fn()}}},
);
wrapper.instance().close();
- expect(props.navigator.dismissModal).toHaveBeenCalledTimes(1);
- expect(props.navigator.dismissModal).toHaveBeenCalledWith({animationType: 'slide-down'});
- });
-
- test('should call headerButtons on emitCanCreateChannel', () => {
- const wrapper = shallow(
- ,
- {context: {intl: {formatMessage: jest.fn()}}},
- );
-
- const instance = wrapper.instance();
- instance.headerButtons = jest.fn();
-
- wrapper.instance().emitCanCreateChannel(true);
- expect(instance.headerButtons).toHaveBeenCalledTimes(1);
- expect(instance.headerButtons).toHaveBeenCalledWith(baseProps.canCreateChannels, true);
-
- wrapper.instance().emitCanCreateChannel(false);
- expect(instance.headerButtons).toHaveBeenCalledTimes(2);
- expect(instance.headerButtons).toHaveBeenCalledWith(baseProps.canCreateChannels, false);
- });
-
- test('should match state on handleCreateScreenVisible', () => {
- const wrapper = shallow(
- ,
- {context: {intl: {formatMessage: jest.fn()}}},
- );
-
- wrapper.setState({createScreenVisible: false});
-
- wrapper.instance().handleCreateScreenVisible(true);
- expect(wrapper.state('createScreenVisible')).toEqual(true);
-
- wrapper.instance().handleCreateScreenVisible(false);
- expect(wrapper.state('createScreenVisible')).toEqual(false);
+ expect(baseProps.navigator.dismissModal).toHaveBeenCalledTimes(1);
+ expect(baseProps.navigator.dismissModal).toHaveBeenCalledWith({animationType: 'slide-down'});
});
test('should call props.navigator.setButtons on headerButtons', () => {
@@ -101,7 +65,7 @@ describe('MoreChannels', () => {
);
expect(props.navigator.setButtons).toHaveBeenCalledTimes(1);
- wrapper.instance().headerButtons();
+ wrapper.instance().headerButtons(true);
expect(props.navigator.setButtons).toHaveBeenCalledTimes(2);
});
@@ -119,18 +83,14 @@ describe('MoreChannels', () => {
});
test('should match state on cancelSearch', () => {
- const props = {...baseProps, actions: {...actions, getChannels: jest.fn()}};
const wrapper = shallow(
- ,
+ ,
{context: {intl: {formatMessage: jest.fn()}}},
);
- wrapper.setState({term: 'term', searching: true, page: 1});
+ wrapper.setState({term: 'term'});
wrapper.instance().cancelSearch(true);
- expect(props.actions.getChannels).toHaveBeenCalledTimes(1);
- expect(props.actions.getChannels).toHaveBeenCalledWith(props.currentTeamId, 0);
expect(wrapper.state('term')).toEqual('');
- expect(wrapper.state('searching')).toEqual(false);
- expect(wrapper.state('page')).toEqual(0);
+ expect(wrapper.state('channels')).toEqual(baseProps.channels);
});
});
diff --git a/app/screens/more_dms/index.js b/app/screens/more_dms/index.js
index bca519d57..3427eaff8 100644
--- a/app/screens/more_dms/index.js
+++ b/app/screens/more_dms/index.js
@@ -3,7 +3,6 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
-import {createSelector} from 'reselect';
import {setChannelDisplayName} from 'app/actions/views/channel';
import {makeDirectChannel, makeGroupChannel} from 'app/actions/views/more_dms';
@@ -13,55 +12,22 @@ import {General} from 'mattermost-redux/constants';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
-import {getCurrentUserId, getProfilesInCurrentTeam, getUsers} from 'mattermost-redux/selectors/entities/users';
+import {getCurrentUserId, getUsers} from 'mattermost-redux/selectors/entities/users';
import MoreDirectMessages from './more_dms';
-function sortCurrentUsers(profiles) {
- return Object.values(profiles).sort((a, b) => {
- const nameA = a.username;
- const nameB = b.username;
-
- return nameA.localeCompare(nameB);
- });
-}
-
-const getUsersInCurrentTeamForMoreDirectMessages = createSelector(
- getProfilesInCurrentTeam,
- sortCurrentUsers
-);
-
-const getUsersForMoreDirectMessages = createSelector(
- getUsers,
- sortCurrentUsers
-);
-
function mapStateToProps(state) {
- const {searchProfiles: searchRequest} = state.requests.users;
-
const config = getConfig(state);
-
- let getRequest;
- let profiles;
- if (config.RestrictDirectMessage === General.RESTRICT_DIRECT_MESSAGE_ANY) {
- getRequest = state.requests.users.getProfiles;
- profiles = getUsersForMoreDirectMessages(state);
- } else {
- getRequest = state.requests.users.getProfilesInTeam;
- profiles = getUsersInCurrentTeamForMoreDirectMessages(state);
- }
+ const restrictDirectMessage = config.RestrictDirectMessage === General.RESTRICT_DIRECT_MESSAGE_ANY;
return {
- config,
+ restrictDirectMessage,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
allProfiles: getUsers(state),
- profiles,
theme: getTheme(state),
currentDisplayName: state.views.channel.displayName,
currentUserId: getCurrentUserId(state),
currentTeamId: getCurrentTeamId(state),
- getRequest,
- searchRequest,
};
}
diff --git a/app/screens/more_dms/more_dms.js b/app/screens/more_dms/more_dms.js
index 81990323c..aac978391 100644
--- a/app/screens/more_dms/more_dms.js
+++ b/app/screens/more_dms/more_dms.js
@@ -3,29 +3,25 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
-import {injectIntl, intlShape} from 'react-intl';
-import {
- InteractionManager,
- Platform,
- StyleSheet,
- View,
-} from 'react-native';
+import {intlShape} from 'react-intl';
+import {Platform, View} from 'react-native';
-import {General, RequestStatus} from 'mattermost-redux/constants';
+import {debounce} from 'mattermost-redux/actions/helpers';
+import {General} from 'mattermost-redux/constants';
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 CustomList, {FLATLIST, SECTIONLIST} from 'app/components/custom_list';
import UserListRow from 'app/components/custom_list/user_list_row';
+import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {alertErrorWithFallback} from 'app/utils/general';
-import {loadingText} from 'app/utils/member_list';
-import {changeOpacity, setNavigatorStyles} from 'app/utils/theme';
+import {createProfilesSections, loadingText} from 'app/utils/member_list';
+import {changeOpacity, makeStyleSheetFromTheme, setNavigatorStyles} from 'app/utils/theme';
import {t} from 'app/utils/i18n';
import SelectedUsers from './selected_users';
@@ -33,20 +29,8 @@ import SelectedUsers from './selected_users';
const START_BUTTON = 'start-conversation';
const CLOSE_BUTTON = 'close-dms';
-class MoreDirectMessages extends PureComponent {
+export default class MoreDirectMessages extends PureComponent {
static propTypes = {
- currentDisplayName: PropTypes.string,
- intl: intlShape.isRequired,
- navigator: PropTypes.object,
- config: PropTypes.object.isRequired,
- currentUserId: PropTypes.string.isRequired,
- currentTeamId: PropTypes.string.isRequired,
- teammateNameDisplay: PropTypes.string,
- theme: PropTypes.object.isRequired,
- allProfiles: PropTypes.object.isRequired,
- profiles: PropTypes.array.isRequired,
- getRequest: PropTypes.object.isRequired,
- searchRequest: PropTypes.object.isRequired,
actions: PropTypes.shape({
makeDirectChannel: PropTypes.func.isRequired,
makeGroupChannel: PropTypes.func.isRequired,
@@ -55,198 +39,81 @@ class MoreDirectMessages extends PureComponent {
searchProfiles: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
}).isRequired,
+ allProfiles: PropTypes.object.isRequired,
+ currentDisplayName: PropTypes.string,
+ currentTeamId: PropTypes.string.isRequired,
+ currentUserId: PropTypes.string.isRequired,
+ navigator: PropTypes.object,
+ restrictDirectMessage: PropTypes.bool.isRequired,
+ teammateNameDisplay: PropTypes.string,
+ theme: PropTypes.object.isRequired,
};
- constructor(props) {
- super(props);
+ static contextTypes = {
+ intl: intlShape.isRequired,
+ };
+
+ constructor(props, context) {
+ super(props, context);
this.searchTimeoutId = 0;
+ this.next = true;
+ this.page = -1;
this.state = {
- profiles: props.profiles.slice(0, General.PROFILE_CHUNK_SIZE),
- page: 0,
- next: true,
- searching: false,
- showNoResults: false,
+ profiles: [],
+ searchResults: [],
+ loading: false,
term: '',
- canStartConversation: false,
- loadingChannel: false,
- canSelect: true,
+ startingConversation: false,
selectedIds: {},
selectedCount: 0,
};
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
- this.updateNavigationButtons(false);
+ this.updateNavigationButtons(false, context);
}
componentDidMount() {
- // set the timeout to 400 cause is the time that the modal takes to open
- // Somehow interactionManager doesn't care
- setTimeout(() => {
- this.getProfiles(0);
- }, 400);
+ this.getProfiles();
}
- componentWillReceiveProps(nextProps) {
- if (this.props.theme !== nextProps.theme) {
- setNavigatorStyles(this.props.navigator, nextProps.theme);
- }
+ componentDidUpdate(prevProps) {
+ const {navigator, theme} = this.props;
+ const {selectedCount, startingConversation} = this.state;
+ const canStart = selectedCount > 0 && !startingConversation;
- const {getRequest} = this.props;
- if (getRequest.status === RequestStatus.STARTED &&
- nextProps.getRequest.status === RequestStatus.SUCCESS) {
- const profiles = this.sliceProfiles(nextProps.profiles);
- this.setState({profiles, showNoResults: true});
- } else if (
- this.state.searching &&
- nextProps.searchRequest.status === RequestStatus.SUCCESS
- ) {
- let profiles = nextProps.profiles;
- if (this.state.selectedCount > 0) {
- profiles = this.removeCurrentUserFromProfiles(profiles);
- }
+ this.updateNavigationButtons(canStart);
- const exactMatches = [];
- const results = filterProfilesMatchingTerm(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;
- });
-
- this.setState({profiles: [...exactMatches, ...results], showNoResults: true});
+ if (theme !== prevProps.theme) {
+ setNavigatorStyles(navigator, theme);
}
}
- componentDidUpdate(prevProps, prevState) {
- const startEnabled = this.isStartEnabled(this.state);
- const wasStartEnabled = this.isStartEnabled(prevState);
-
- if (startEnabled && !wasStartEnabled) {
- this.updateNavigationButtons(true);
- } else if (!startEnabled && !wasStartEnabled) {
- this.updateNavigationButtons(false);
- }
- }
-
- removeCurrentUserFromProfiles(profiles = []) {
- return profiles.filter((profile) => {
- return profile.id !== this.props.currentUserId;
- });
- }
-
- sliceProfiles(profiles = []) {
- return profiles.slice(0, (this.state.page + 1) * General.PROFILE_CHUNK_SIZE);
- }
-
- isStartEnabled = (state) => {
- if (state.loadingChannel) {
- return false;
- }
-
- return state.selectedCount >= 1 && state.selectedCount <= General.MAX_USERS_IN_GM - 1;
- };
-
- updateNavigationButtons = (startEnabled) => {
- this.props.navigator.setButtons({
- rightButtons: [{
- id: START_BUTTON,
- title: this.props.intl.formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}),
- showAsAction: 'always',
- disabled: !startEnabled,
- }],
- });
- };
-
close = () => {
- this.props.navigator.dismissModal({
- animationType: 'slide-down',
- });
- };
-
- onNavigatorEvent = (event) => {
- if (event.type === 'NavBarButtonPress') {
- if (event.id === START_BUTTON) {
- this.startConversation();
- } else if (event.id === CLOSE_BUTTON) {
- this.close();
- }
- }
- };
-
- onSearch = (text) => {
- const term = text;
-
- if (term) {
- this.setState({searching: true, term});
- clearTimeout(this.searchTimeoutId);
-
- this.searchTimeoutId = setTimeout(() => {
- this.searchProfiles(term.toLowerCase());
- }, General.SEARCH_TIMEOUT_MILLISECONDS);
- } else {
- this.clearSearch();
- }
+ this.props.navigator.dismissModal({animationType: 'slide-down'});
};
clearSearch = () => {
- const {profiles} = this.props;
-
- let newProfiles;
- if (this.state.selectedCount > 0) {
- newProfiles = this.removeCurrentUserFromProfiles(profiles);
- } else {
- newProfiles = this.sliceProfiles(profiles);
- }
-
- this.setState({
- searching: false,
- term: '',
- page: 0,
- profiles: newProfiles,
- });
+ this.setState({term: '', searchResults: []});
};
- getProfiles = (page) => {
- if (this.props.config.RestrictDirectMessage === General.RESTRICT_DIRECT_MESSAGE_ANY) {
- return this.props.actions.getProfiles(page, General.PROFILE_CHUNK_SIZE);
- }
+ getProfiles = debounce(() => {
+ const {loading, term} = this.state;
+ if (this.next && !loading && !term) {
+ this.setState({loading: true}, () => {
+ const {actions, currentTeamId, restrictDirectMessage} = this.props;
- return this.props.actions.getProfilesInTeam(this.props.currentTeamId, page, General.PROFILE_CHUNK_SIZE);
- };
-
- searchProfiles = (term) => {
- if (this.props.config.RestrictDirectMessage === General.RESTRICT_DIRECT_MESSAGE_ANY) {
- return this.props.actions.searchProfiles(term);
- }
-
- return this.props.actions.searchProfiles(term, {team_id: this.props.currentTeamId});
- };
-
- loadMoreProfiles = () => {
- if (this.state.searching) {
- return;
- }
-
- let {page} = this.state;
- if (this.props.getRequest.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) {
- page += 1;
- this.getProfiles(page).then(({data}) => {
- if (data && data.length) {
- this.setState({
- page,
- });
+ if (restrictDirectMessage) {
+ actions.getProfiles(this.page + 1, General.PROFILE_CHUNK_SIZE).then(this.loadedProfiles);
} else {
- this.setState({next: false});
+ actions.getProfilesInTeam(currentTeamId, this.page + 1, General.PROFILE_CHUNK_SIZE).then(this.loadedProfiles);
}
});
}
- };
+ }, 100);
- handleSelectUser = (id) => {
+ handleSelectProfile = (id) => {
const {currentUserId} = this.props;
if (id === currentUserId) {
@@ -256,9 +123,7 @@ class MoreDirectMessages extends PureComponent {
this.startConversation(selectedId);
} else {
this.setState((prevState) => {
- const {
- selectedIds,
- } = prevState;
+ const {selectedIds} = prevState;
const wasSelected = selectedIds[id];
@@ -282,107 +147,42 @@ class MoreDirectMessages extends PureComponent {
}
};
- handleRemoveUser = (id) => {
+ handleRemoveProfile = (id) => {
this.setState((prevState) => {
- const {
- profiles,
- selectedCount,
- selectedIds,
- } = prevState;
+ const {selectedIds} = prevState;
const newSelectedIds = Object.assign({}, selectedIds);
Reflect.deleteProperty(newSelectedIds, id);
- let newProfiles = profiles;
- if (selectedCount === 1) {
- newProfiles = this.sliceProfiles(this.props.profiles);
- }
-
return {
- profiles: newProfiles,
selectedIds: newSelectedIds,
selectedCount: Object.keys(newSelectedIds).length,
};
});
- }
-
- startConversation = async (selectedId) => {
- const {
- currentDisplayName,
- actions,
- } = this.props;
-
- if (this.state.loadingChannel) {
- return;
- }
-
- this.setState({
- loadingChannel: true,
- });
-
- // Save the current channel display name in case it fails
- const currentChannelDisplayName = currentDisplayName;
-
- const selectedIds = selectedId ? Object.keys(selectedId) : Object.keys(this.state.selectedIds);
- let success;
- if (selectedIds.length === 0) {
- success = false;
- } else if (selectedIds.length > 1) {
- success = await this.makeGroupChannel(selectedIds);
- } else {
- success = await this.makeDirectChannel(selectedIds[0]);
- }
-
- if (success) {
- EventEmitter.emit('close_channel_drawer');
- InteractionManager.runAfterInteractions(() => {
- this.close();
- });
- } else {
- this.setState({
- loadingChannel: false,
- });
-
- actions.setChannelDisplayName(currentChannelDisplayName);
- }
};
- makeGroupChannel = async (ids) => {
- const {
- actions,
- allProfiles,
- currentUserId,
- intl,
- teammateNameDisplay,
- } = this.props;
-
- const result = await actions.makeGroupChannel(ids);
-
- const displayName = getGroupDisplayNameFromUserIds(ids, allProfiles, currentUserId, teammateNameDisplay);
- actions.setChannelDisplayName(displayName);
-
- if (result.error) {
- alertErrorWithFallback(
- intl,
- result.error,
- {
- id: t('mobile.open_gm.error'),
- defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
- }
- );
+ isStartEnabled = (state) => {
+ if (state.startingConversation) {
+ return false;
}
- return !result.error;
+ return state.selectedCount >= 1 && state.selectedCount <= General.MAX_USERS_IN_GM - 1;
+ };
+
+ loadedProfiles = ({data}) => {
+ const {profiles} = this.state;
+ if (data && !data.length) {
+ this.next = false;
+ }
+
+ this.page += 1;
+ this.setState({loading: false, profiles: [...profiles, ...data]});
};
makeDirectChannel = async (id) => {
- const {
- actions,
- allProfiles,
- intl,
- teammateNameDisplay,
- } = this.props;
+ const {intl} = this.context;
+ const {actions, allProfiles, teammateNameDisplay} = this.props;
const user = allProfiles[id];
@@ -408,19 +208,129 @@ class MoreDirectMessages extends PureComponent {
return !result.error;
};
- sectionKeyExtractor = (user) => {
- // Group items alphabetically by first letter of username
- return user.username[0].toUpperCase();
- }
+ makeGroupChannel = async (ids) => {
+ const {intl} = this.context;
+ const {
+ actions,
+ allProfiles,
+ currentUserId,
+ teammateNameDisplay,
+ } = this.props;
- compareItems = (a, b) => {
- return a.username.localeCompare(b.username);
+ const result = await actions.makeGroupChannel(ids);
+ const displayName = getGroupDisplayNameFromUserIds(ids, allProfiles, currentUserId, teammateNameDisplay);
+ actions.setChannelDisplayName(displayName);
+
+ if (result.error) {
+ alertErrorWithFallback(
+ intl,
+ result.error,
+ {
+ id: t('mobile.open_gm.error'),
+ defaultMessage: "We couldn't open a group message with those users. Please check your connection and try again.",
+ }
+ );
+ }
+
+ return !result.error;
+ };
+
+ onNavigatorEvent = (event) => {
+ if (event.type === 'NavBarButtonPress') {
+ if (event.id === START_BUTTON) {
+ this.startConversation();
+ } else if (event.id === CLOSE_BUTTON) {
+ this.close();
+ }
+ }
+ };
+
+ onSearch = (text) => {
+ const term = text.toLowerCase();
+
+ if (term) {
+ this.setState({term});
+ clearTimeout(this.searchTimeoutId);
+
+ this.searchTimeoutId = setTimeout(() => {
+ this.searchProfiles(term);
+ }, General.SEARCH_TIMEOUT_MILLISECONDS);
+ } else {
+ this.clearSearch();
+ }
+ };
+
+ searchProfiles = (term) => {
+ const {actions, currentTeamId, restrictDirectMessage} = this.props;
+ this.setState({loading: true});
+
+ if (restrictDirectMessage) {
+ actions.searchProfiles(term).then(({data}) => {
+ this.setState({searchResults: data, loading: false});
+ });
+ } else {
+ actions.searchProfiles(term, {team_id: currentTeamId}).then(({data}) => {
+ this.setState({searchResults: data, loading: false});
+ });
+ }
+ };
+
+ startConversation = async (selectedId) => {
+ const {
+ currentDisplayName,
+ actions,
+ } = this.props;
+
+ if (this.state.startingConversation) {
+ return;
+ }
+
+ this.setState({
+ startingConversation: true,
+ });
+
+ // Save the current channel display name in case it fails
+ const currentChannelDisplayName = currentDisplayName;
+
+ const selectedIds = selectedId ? Object.keys(selectedId) : Object.keys(this.state.selectedIds);
+ let success;
+ if (selectedIds.length === 0) {
+ success = false;
+ } else if (selectedIds.length > 1) {
+ success = await this.makeGroupChannel(selectedIds);
+ } else {
+ success = await this.makeDirectChannel(selectedIds[0]);
+ }
+
+ if (success) {
+ EventEmitter.emit('close_channel_drawer');
+ requestAnimationFrame(() => {
+ this.close();
+ });
+ } else {
+ this.setState({
+ startingConversation: false,
+ });
+
+ actions.setChannelDisplayName(currentChannelDisplayName);
+ }
+ };
+
+ updateNavigationButtons = (startEnabled, context = this.context) => {
+ const {formatMessage} = context.intl;
+ this.props.navigator.setButtons({
+ rightButtons: [{
+ id: START_BUTTON,
+ title: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}),
+ showAsAction: 'always',
+ disabled: !startEnabled,
+ }],
+ });
};
renderItem = (props) => {
// The list will re-render when the selection changes because it's passed into the list as extraData
const selected = this.state.selectedIds[props.id];
- const enabled = selected || this.state.selectedCount < General.MAX_USERS_IN_GM - 1;
return (
);
};
+ renderLoading = () => {
+ const {theme} = this.props;
+ const {loading} = this.state;
+ const style = getStyleFromTheme(theme);
+
+ if (!loading) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ renderNoResults = () => {
+ const {loading} = this.state;
+ const {theme} = this.props;
+ const style = getStyleFromTheme(theme);
+
+ if (loading || this.page === -1) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ };
+
render() {
+ const {formatMessage} = this.context.intl;
+ const {currentUserId, theme} = this.props;
const {
- getRequest,
- searchRequest,
- theme,
- } = this.props;
- const {
- loadingChannel,
- showNoResults,
+ loading,
+ profiles,
+ searchResults,
+ selectedIds,
+ selectedCount,
+ startingConversation,
term,
} = this.state;
+ const style = getStyleFromTheme(theme);
- const isLoading = (
- getRequest.status === RequestStatus.STARTED) || (getRequest.status === RequestStatus.NOT_STARTED) ||
- (searchRequest.status === RequestStatus.STARTED);
-
- if (loadingChannel) {
+ if (startingConversation) {
return (
@@ -469,47 +416,37 @@ class MoreDirectMessages extends PureComponent {
}),
};
- let listComponent;
- if (term.length) {
- listComponent = (
-
- );
+ let data;
+ let listType;
+ if (term) {
+ const exactMatches = [];
+ const results = filterProfilesMatchingTerm(searchResults, term).filter((p) => {
+ if (selectedCount > 0 && p.id === currentUserId) {
+ return false;
+ }
+
+ if (p.username === term || p.username.startsWith(term)) {
+ exactMatches.push(p);
+ return false;
+ }
+
+ return true;
+ });
+ data = [...exactMatches, ...results];
+ listType = FLATLIST;
} else {
- listComponent = (
-
- );
+ data = createProfilesSections(profiles);
+ listType = SECTIONLIST;
}
return (
-
+
- {listComponent}
+
);
}
}
-const style = StyleSheet.create({
- searchContainer: {
- marginVertical: 5,
- },
+const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
+ return {
+ container: {
+ flex: 1,
+ },
+ searchBar: {
+ marginVertical: 5,
+ },
+ loadingContainer: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ height: 70,
+ justifyContent: 'center',
+ },
+ loadingText: {
+ color: changeOpacity(theme.centerChannelColor, 0.6),
+ },
+ noResultContainer: {
+ flexGrow: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ noResultText: {
+ fontSize: 26,
+ color: changeOpacity(theme.centerChannelColor, 0.5),
+ },
+ };
});
-
-export default injectIntl(MoreDirectMessages);
diff --git a/app/utils/member_list.js b/app/utils/member_list.js
index c3a9e9601..3595a3c7b 100644
--- a/app/utils/member_list.js
+++ b/app/utils/member_list.js
@@ -8,20 +8,33 @@ export const loadingText = {
defaultMessage: 'Loading Members...',
};
-export function createMembersSections(data) {
+const sectionKeyExtractor = (profile) => {
+ // Group items alphabetically by first letter of username
+ return profile.username[0].toUpperCase();
+};
+
+export function createProfilesSections(profiles) {
const sections = {};
- data.forEach((d) => {
- const name = d.username;
- const sectionKey = name.substring(0, 1).toUpperCase();
+ const sectionKeys = [];
+ for (const profile of profiles) {
+ const sectionKey = sectionKeyExtractor(profile);
if (!sections[sectionKey]) {
sections[sectionKey] = [];
+ sectionKeys.push(sectionKey);
}
- sections[sectionKey].push(d);
- });
+ sections[sectionKey].push(profile);
+ }
- return sections;
+ sectionKeys.sort();
+
+ return sectionKeys.map((sectionKey) => {
+ return {
+ id: sectionKey,
+ data: sections[sectionKey],
+ };
+ });
}
export function markSelectedProfiles(profiles, selectedProfiles) {
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index f8f7a783c..a58f21225 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -171,6 +171,7 @@
"mobile.channel_list.not_member": "NOT A MEMBER",
"mobile.channel_list.unreads": "UNREADS",
"mobile.channel.markAsRead": "Mark As Read",
+ "mobile.channel_members.add_members_alert": "You must select at least one member to add to the channel.",
"mobile.client_upgrade": "Update App",
"mobile.client_upgrade.can_upgrade_subtitle": "A new version is available for download.",
"mobile.client_upgrade.can_upgrade_title": "Update Available",
@@ -260,6 +261,7 @@
"mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.",
"mobile.loading_channels": "Loading Channels...",
"mobile.loading_members": "Loading Members...",
+ "mobile.loading_options": "Loading Options...",
"mobile.loading_posts": "Loading messages...",
"mobile.login_options.choose_title": "Choose your login method",
"mobile.long_post_title": "{channelName} - Post",
@@ -429,6 +431,7 @@
"modal.manual_status.auto_responder.message_dnd": "Would you like to switch your status to \"Do Not Disturb\" and disable Automatic Replies?",
"modal.manual_status.auto_responder.message_offline": "Would you like to switch your status to \"Offline\" and disable Automatic Replies?",
"modal.manual_status.auto_responder.message_online": "Would you like to switch your status to \"Online\" and disable Automatic Replies?",
+ "more_channels.noMore": "No more channels to join",
"more_channels.title": "More Channels",
"msg_typing.areTyping": "{users} and {last} are typing...",
"msg_typing.isTyping": "{user} is typing...",