Refactor CustomList to use Section and Flat Lists (#2332)
This commit is contained in:
parent
b5ddce6770
commit
592fab2375
26 changed files with 2103 additions and 2416 deletions
|
|
@ -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 <RowComponent {...props}/>;
|
||||
}
|
||||
|
||||
return this.props.renderItem(props);
|
||||
};
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return <View style={style.separator}/>;
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
{...this.props.loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderEmptyList = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<View style={style.searching}>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.showNoResults) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
style={style.listView}
|
||||
initialNumToRender={this.props.initialNumToRender}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
ListEmptyComponent={this.renderEmptyList}
|
||||
onEndReached={this.props.onListEndReached}
|
||||
onEndReachedThreshold={this.props.onListEndReachedThreshold}
|
||||
extraData={this.props.extraData}
|
||||
data={this.state.items}
|
||||
keyExtractor={this.props.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -1,64 +1,102 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CustomList should match snapshot 1`] = `
|
||||
<Component
|
||||
exports[`CustomList should match snapshot with FlatList 1`] = `
|
||||
<FlatList
|
||||
ItemSeparatorComponent={[Function]}
|
||||
ListEmptyComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"username": "username_1",
|
||||
},
|
||||
Object {
|
||||
"username": "username_2",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableVirtualization={false}
|
||||
horizontal={false}
|
||||
initialNumToRender={15}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={16}
|
||||
numColumns={1}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
stickySectionHeadersEnabled={true}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ListViewMock
|
||||
dataSource={
|
||||
ListViewDataSource {
|
||||
"items": 2,
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`CustomList should match snapshot with SectionList 1`] = `
|
||||
<SectionList
|
||||
ItemSeparatorComponent={[Function]}
|
||||
ListEmptyComponent={[Function]}
|
||||
ListFooterComponent={[Function]}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
enableEmptySections={true}
|
||||
initialListSize={0}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={0}
|
||||
pageSize={0}
|
||||
renderFooter={[Function]}
|
||||
renderRow={[Function]}
|
||||
renderScrollComponent={[Function]}
|
||||
renderSectionHeader={[Function]}
|
||||
renderSeparator={[Function]}
|
||||
scrollRenderAheadDistance={0}
|
||||
style={
|
||||
}
|
||||
data={Array []}
|
||||
disableVirtualization={false}
|
||||
extraData={false}
|
||||
horizontal={false}
|
||||
initialNumToRender={15}
|
||||
keyExtractor={[Function]}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={16}
|
||||
onEndReachedThreshold={2}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
renderSectionHeader={[Function]}
|
||||
scrollEventThrottle={60}
|
||||
sections={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
"username": "username_1",
|
||||
},
|
||||
Object {
|
||||
"username": "username_2",
|
||||
},
|
||||
]
|
||||
}
|
||||
stickySectionHeadersEnabled={false}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={21}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`CustomList should match snapshot, renderFooter 1`] = `null`;
|
||||
|
||||
exports[`CustomList should match snapshot, renderFooter 2`] = `
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": 70,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Loading Members..."
|
||||
id="mobile.loading_members"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.6)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
exports[`CustomList should match snapshot, renderFooter 2`] = `<Component />`;
|
||||
|
||||
exports[`CustomList should match snapshot, renderSectionHeader 1`] = `
|
||||
<Component
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export default class CustomListRow extends React.PureComponent {
|
|||
const style = StyleSheet.create({
|
||||
touchable: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
|||
|
|
@ -1,246 +1,230 @@
|
|||
// 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 {ListView, Platform, Text, View} from 'react-native';
|
||||
import {FlatList, Platform, SectionList, Text, View} from 'react-native';
|
||||
|
||||
import Loading from 'app/components/loading';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {ListTypes} from 'app/constants';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export const FLATLIST = 'flat';
|
||||
export const SECTIONLIST = 'section';
|
||||
const INITIAL_BATCH_TO_RENDER = 15;
|
||||
const SCROLL_UP_MULTIPLIER = 6;
|
||||
|
||||
export default class CustomList extends PureComponent {
|
||||
static propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
searching: PropTypes.bool,
|
||||
onListEndReached: PropTypes.func,
|
||||
onListEndReachedThreshold: PropTypes.number,
|
||||
extraData: PropTypes.any,
|
||||
listType: PropTypes.oneOf([FLATLIST, SECTIONLIST]),
|
||||
loading: PropTypes.bool,
|
||||
loadingText: PropTypes.object,
|
||||
listPageSize: PropTypes.number,
|
||||
listInitialSize: PropTypes.number,
|
||||
listScrollRenderAheadDistance: PropTypes.number,
|
||||
showSections: PropTypes.bool,
|
||||
loadingComponent: PropTypes.element,
|
||||
noResults: PropTypes.element,
|
||||
onLoadMore: PropTypes.func,
|
||||
onRowPress: PropTypes.func,
|
||||
selectable: PropTypes.bool,
|
||||
onRowSelect: PropTypes.func,
|
||||
renderRow: PropTypes.func.isRequired,
|
||||
createSections: PropTypes.func,
|
||||
showNoResults: PropTypes.bool,
|
||||
renderItem: PropTypes.oneOfType([
|
||||
PropTypes.func,
|
||||
PropTypes.element,
|
||||
]).isRequired,
|
||||
selectable: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
searching: false,
|
||||
onListEndReached: () => 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 (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{sectionId}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
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 <RowComponent {...props}/>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
key={`${sectionId}-${rowId}`}
|
||||
style={style.separator}
|
||||
<FlatList
|
||||
contentContainerStyle={style.container}
|
||||
data={data}
|
||||
extraData={extraData}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
keyExtractor={this.keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
ItemSeparatorComponent={this.renderSeparator}
|
||||
ListEmptyComponent={this.renderEmptyList}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
onLayout={this.handleLayout}
|
||||
onScroll={this.handleScroll}
|
||||
ref={this.setListRef}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
scrollEventThrottle={60}
|
||||
style={style.list}
|
||||
stickySectionHeadersEnabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={{height: 70, backgroundColor, alignItems: 'center', justifyContent: 'center'}}>
|
||||
<FormattedText
|
||||
{...this.props.loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{section.id}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<View
|
||||
style={style.searching}
|
||||
>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (showNoResults && noResults) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<ListView
|
||||
style={style.listView}
|
||||
dataSource={dataSource}
|
||||
renderRow={this.renderRow}
|
||||
renderSectionHeader={showSections ? this.renderSectionHeader : null}
|
||||
renderSeparator={this.renderSeparator}
|
||||
renderFooter={this.renderFooter}
|
||||
enableEmptySections={true}
|
||||
onEndReached={onListEndReached}
|
||||
onEndReachedThreshold={onListEndReachedThreshold}
|
||||
pageSize={listPageSize}
|
||||
initialListSize={listInitialSize}
|
||||
scrollRenderAheadDistance={listScrollRenderAheadDistance}
|
||||
/>
|
||||
{loadingComponent}
|
||||
</View>
|
||||
<SectionList
|
||||
contentContainerStyle={style.container}
|
||||
extraData={loading}
|
||||
keyboardShouldPersistTaps='always'
|
||||
keyboardDismissMode='interactive'
|
||||
keyExtractor={this.keyExtractor}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
ItemSeparatorComponent={this.renderSeparator}
|
||||
ListEmptyComponent={this.renderEmptyList}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
maxToRenderPerBatch={INITIAL_BATCH_TO_RENDER + 1}
|
||||
onLayout={this.handleLayout}
|
||||
onScroll={this.handleScroll}
|
||||
ref={this.setListRef}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={this.renderItem}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
scrollEventThrottle={60}
|
||||
sections={data}
|
||||
style={style.list}
|
||||
stickySectionHeadersEnabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderSeparator = () => {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<View style={style.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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: (<View/>),
|
||||
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(
|
||||
<CustomList {...baseProps}/>
|
||||
);
|
||||
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(
|
||||
<CustomList {...props}/>
|
||||
);
|
||||
|
||||
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(
|
||||
<CustomList {...baseProps}/>
|
||||
);
|
||||
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(
|
||||
<CustomList {...props}/>
|
||||
);
|
||||
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(
|
||||
<CustomList {...baseProps}/>
|
||||
);
|
||||
expect(wrapper.instance().renderSeparator('section_id', 'row_id')).toMatchSnapshot();
|
||||
expect(wrapper.instance().renderSeparator()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, renderFooter', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
|||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
marginHorizontal: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
profileContainer: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={style.sectionWrapper}>
|
||||
<View style={style.sectionContainer}>
|
||||
<Text style={style.sectionText}>{section.key}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 <RowComponent {...props}/>;
|
||||
}
|
||||
|
||||
return this.props.renderItem(props);
|
||||
};
|
||||
|
||||
renderItemSeparator = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.separator}/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={style.loading}>
|
||||
<FormattedText
|
||||
{...this.props.loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderEmptyList = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (this.props.loading) {
|
||||
return (
|
||||
<View
|
||||
style={style.searching}
|
||||
>
|
||||
<Loading/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.showNoResults) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getStyleFromTheme(this.props.theme);
|
||||
|
||||
return (
|
||||
<SectionList
|
||||
style={style.listView}
|
||||
initialNumToRender={this.props.initialNumToRender}
|
||||
renderSectionHeader={this.renderSectionHeader}
|
||||
ItemSeparatorComponent={this.renderItemSeparator}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
ListEmptyComponent={this.renderEmptyList}
|
||||
onEndReached={this.props.onListEndReached}
|
||||
onEndReachedThreshold={this.props.onListEndReachedThreshold}
|
||||
extraData={this.props.extraData}
|
||||
sections={this.state.sections}
|
||||
keyExtractor={this.props.keyExtractor}
|
||||
renderItem={this.renderItem}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<UserListRow
|
||||
key={props.id}
|
||||
{...props}
|
||||
selectable={true}
|
||||
selected={selected}
|
||||
enabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderLoading = () => {
|
||||
const {theme} = this.props;
|
||||
const {loading} = this.state;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (!loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
{...loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = () => {
|
||||
const {loading} = this.state;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (loading || this.page === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<KeyboardLayout>
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
</KeyboardLayout>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<KeyboardLayout>
|
||||
<StatusBar/>
|
||||
<View
|
||||
style={{marginVertical: 5}}
|
||||
>
|
||||
<View style={style.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
|
|
@ -272,31 +340,57 @@ class ChannelAddMembers extends PureComponent {
|
|||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.searchProfiles}
|
||||
onSearchButtonPress={this.searchProfiles}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
onChangeText={this.onSearch}
|
||||
onSearchButtonPress={this.onSearch}
|
||||
onCancelButtonPress={this.clearSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
data={profiles}
|
||||
data={data}
|
||||
extraData={selectedIds}
|
||||
key='custom_list'
|
||||
listType={listType}
|
||||
loading={loading}
|
||||
loadingComponent={this.renderLoading()}
|
||||
noResults={this.renderNoResults()}
|
||||
onLoadMore={this.getProfiles}
|
||||
onRowPress={this.handleSelectProfile}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
searching={searching}
|
||||
onListEndReached={more}
|
||||
preferences={preferences}
|
||||
listScrollRenderAheadDistance={50}
|
||||
loading={this.state.isLoading}
|
||||
loadingText={loadingText}
|
||||
selectable={this.state.canSelect}
|
||||
onRowSelect={this.handleRowSelect}
|
||||
renderRow={UserListRow}
|
||||
createSections={createMembersSections}
|
||||
showNoResults={this.state.showNoResults}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<UserListRow
|
||||
key={props.id}
|
||||
{...props}
|
||||
selectable={true}
|
||||
selected={selected}
|
||||
enabled={enabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
{...loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = () => {
|
||||
const {loading} = this.state;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (loading || this.page === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<KeyboardLayout>
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<Loading/>
|
||||
</KeyboardLayout>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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 (
|
||||
<KeyboardLayout>
|
||||
<StatusBar/>
|
||||
<View
|
||||
style={{marginVertical: 5}}
|
||||
>
|
||||
<View style={style.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
|
|
@ -314,29 +359,57 @@ class ChannelMembers extends PureComponent {
|
|||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.searchProfiles}
|
||||
onSearchButtonPress={this.searchProfiles}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
onChangeText={this.onSearch}
|
||||
onSearchButtonPress={this.onSearch}
|
||||
onCancelButtonPress={this.clearSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
data={profiles}
|
||||
data={data}
|
||||
extraData={selectedIds}
|
||||
key='custom_list'
|
||||
listType={listType}
|
||||
loading={loading}
|
||||
loadingComponent={this.renderLoading()}
|
||||
noResults={this.renderNoResults()}
|
||||
onLoadMore={this.getProfiles}
|
||||
onRowPress={this.handleSelectProfile}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
searching={searching}
|
||||
onListEndReached={more}
|
||||
listScrollRenderAheadDistance={50}
|
||||
loading={isLoading}
|
||||
loadingText={loadingText}
|
||||
onRowSelect={canManageUsers && this.state.canSelect ? this.handleRowSelect : null}
|
||||
renderRow={this.renderMemberRow}
|
||||
createSections={createMembersSections}
|
||||
showNoResults={showNoResults}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,525 +1,524 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MenuActionSelector should match snapshot for channels 1`] = `
|
||||
<MenuActionSelector
|
||||
actions={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"getChannels": [Function],
|
||||
"getProfiles": [Function],
|
||||
"searchChannels": [Function],
|
||||
"searchProfiles": [Function],
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
currentTeamId="someId"
|
||||
data={
|
||||
Array [
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"display_name": "display_name",
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
},
|
||||
"marginVertical": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SearchBarIos
|
||||
autoCapitalize="none"
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={true}
|
||||
cancelTitle="Cancel"
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"display_name": "display_name2",
|
||||
"id": "id2",
|
||||
"name": "name2",
|
||||
},
|
||||
]
|
||||
}
|
||||
dataSource="channels"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onSelect={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`MenuActionSelector should match snapshot for channels 2`] = `
|
||||
<MenuActionSelector
|
||||
actions={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"getChannels": [Function],
|
||||
"getProfiles": [Function],
|
||||
"searchChannels": [Function],
|
||||
"searchProfiles": [Function],
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
currentTeamId="someId"
|
||||
data={
|
||||
Array [
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"display_name": "display_name",
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
},
|
||||
"marginVertical": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SearchBarIos
|
||||
autoCapitalize="none"
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={true}
|
||||
cancelTitle="Cancel"
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"display_name": "display_name2",
|
||||
"id": "id2",
|
||||
"name": "name2",
|
||||
},
|
||||
]
|
||||
}
|
||||
dataSource="channels"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onSelect={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`MenuActionSelector should match snapshot for explicit options 1`] = `
|
||||
<MenuActionSelector
|
||||
actions={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"getChannels": [Function],
|
||||
"getProfiles": [Function],
|
||||
"searchChannels": [Function],
|
||||
"searchProfiles": [Function],
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
currentTeamId="someId"
|
||||
data={
|
||||
Array [
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"text": "text",
|
||||
"value": "value",
|
||||
},
|
||||
]
|
||||
}
|
||||
dataSource={null}
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
"marginVertical": 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
>
|
||||
<SearchBarIos
|
||||
autoCapitalize="none"
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={true}
|
||||
cancelTitle="Cancel"
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"text": "text",
|
||||
"value": "value",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
onSelect={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`MenuActionSelector should match snapshot for searching 1`] = `
|
||||
<MenuActionSelector
|
||||
actions={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"getChannels": [Function],
|
||||
"getProfiles": [Function],
|
||||
"searchChannels": [Function],
|
||||
"searchProfiles": [Function],
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
currentTeamId="someId"
|
||||
data={
|
||||
Array [
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"display_name": "display_name",
|
||||
"id": "id",
|
||||
"name": "name",
|
||||
},
|
||||
"marginVertical": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SearchBarIos
|
||||
autoCapitalize="none"
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={true}
|
||||
cancelTitle="Cancel"
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value="name2"
|
||||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
data={Array []}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"display_name": "display_name2",
|
||||
"id": "id2",
|
||||
"name": "name2",
|
||||
},
|
||||
]
|
||||
}
|
||||
dataSource="channels"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onSelect={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`MenuActionSelector should match snapshot for users 1`] = `
|
||||
<MenuActionSelector
|
||||
actions={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"getChannels": [Function],
|
||||
"getProfiles": [Function],
|
||||
"searchChannels": [Function],
|
||||
"searchProfiles": [Function],
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
currentTeamId="someId"
|
||||
data={
|
||||
Array [
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"id": "id",
|
||||
"username": "username",
|
||||
},
|
||||
"marginVertical": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SearchBarIos
|
||||
autoCapitalize="none"
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={true}
|
||||
cancelTitle="Cancel"
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
data={Array []}
|
||||
listType="section"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"id": "id2",
|
||||
"username": "username2",
|
||||
},
|
||||
]
|
||||
}
|
||||
dataSource="users"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onSelect={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`MenuActionSelector should match snapshot for users 2`] = `
|
||||
<MenuActionSelector
|
||||
actions={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"getChannels": [Function],
|
||||
"getProfiles": [Function],
|
||||
"searchChannels": [Function],
|
||||
"searchProfiles": [Function],
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
currentTeamId="someId"
|
||||
data={
|
||||
Array [
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"id": "id",
|
||||
"username": "username",
|
||||
},
|
||||
"marginVertical": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<SearchBarIos
|
||||
autoCapitalize="none"
|
||||
backgroundColor="transparent"
|
||||
blurOnSubmit={true}
|
||||
cancelTitle="Cancel"
|
||||
inputHeight={33}
|
||||
inputStyle={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
onBlur={[Function]}
|
||||
onCancelButtonPress={[Function]}
|
||||
onChangeText={[Function]}
|
||||
onFocus={[Function]}
|
||||
onSearchButtonPress={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="rgba(61,60,64,0.5)"
|
||||
tintColorDelete="rgba(61,60,64,0.5)"
|
||||
tintColorSearch="rgba(61,60,64,0.5)"
|
||||
titleCancelColor="#3d3c40"
|
||||
value=""
|
||||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
data={Array []}
|
||||
listType="section"
|
||||
loading={false}
|
||||
loadingComponent={null}
|
||||
noResults={null}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"id": "id2",
|
||||
"username": "username2",
|
||||
},
|
||||
]
|
||||
}
|
||||
dataSource="users"
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
navigator={
|
||||
Object {
|
||||
"setOnNavigatorEvent": [MockFunction],
|
||||
}
|
||||
}
|
||||
onSelect={[MockFunction]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
{...text}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = () => {
|
||||
const {loading} = this.state;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (loading || this.page === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
<View
|
||||
style={style.searchContainer}
|
||||
>
|
||||
<View style={style.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
|
|
@ -227,26 +320,24 @@ class MenuActionSelector extends PureComponent {
|
|||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.searchProfiles}
|
||||
onSearchButtonPress={this.searchProfiles}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
onChangeText={this.onSearch}
|
||||
onSearchButtonPress={this.onSearch}
|
||||
onCancelButtonPress={this.clearSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<CustomList
|
||||
data={filteredData}
|
||||
data={data}
|
||||
key='custom_list'
|
||||
listType={listType}
|
||||
loading={loading}
|
||||
loadingComponent={this.renderLoading()}
|
||||
noResults={this.renderNoResults()}
|
||||
onLoadMore={this.loadMore}
|
||||
onRowPress={this.handleSelectItem}
|
||||
renderItem={rowComponent}
|
||||
theme={theme}
|
||||
searching={searching}
|
||||
onListEndReached={more}
|
||||
listScrollRenderAheadDistance={50}
|
||||
loading={this.state.isLoading}
|
||||
loadingText={loadingText}
|
||||
selectable={false}
|
||||
onRowPress={this.handleRowSelect}
|
||||
renderRow={rowComponent}
|
||||
showNoResults={this.state.showNoResults}
|
||||
showSections={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ exports[`MoreChannels should match snapshot 1`] = `
|
|||
/>
|
||||
</Component>
|
||||
<CustomList
|
||||
createSections={[Function]}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
|
|
@ -47,24 +46,58 @@ exports[`MoreChannels should match snapshot 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
listInitialSize={10}
|
||||
listPageSize={10}
|
||||
listScrollRenderAheadDistance={50}
|
||||
extraData={false}
|
||||
listType="flat"
|
||||
loading={false}
|
||||
loadingText={
|
||||
Object {
|
||||
"defaultMessage": "Loading Channels...",
|
||||
"id": "mobile.loading_channels",
|
||||
}
|
||||
loadingComponent={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": 70,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="Loading Channels..."
|
||||
id="mobile.loading_channels"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.6)",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
}
|
||||
onListEndReached={[Function]}
|
||||
onListEndReachedThreshold={50}
|
||||
noResults={
|
||||
<Component
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"flexGrow": 1,
|
||||
"justifyContent": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedText
|
||||
defaultMessage="No more channels to join"
|
||||
id="more_channels.noMore"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(61,60,64,0.5)",
|
||||
"fontSize": 26,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Component>
|
||||
}
|
||||
onLoadMore={[Function]}
|
||||
onRowPress={[Function]}
|
||||
renderRow={[Function]}
|
||||
searching={false}
|
||||
selectable={false}
|
||||
showNoResults={false}
|
||||
showSections={false}
|
||||
renderItem={[Function]}
|
||||
showNoResults={true}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
id='mobile.loading_channels'
|
||||
defaultMessage='Loading Channels...'
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = () => {
|
||||
const {term, loading} = this.state;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (term) {
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='more_channels.noMore'
|
||||
defaultMessage='No more channels to join'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 = (
|
||||
<React.Fragment>
|
||||
<View style={style.searchbar}>
|
||||
<View style={style.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
|
|
@ -332,8 +340,8 @@ export default class MoreChannels extends PureComponent {
|
|||
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
titleCancelColor={theme.centerChannelColor}
|
||||
onChangeText={this.searchProfiles}
|
||||
onSearchButtonPress={this.searchProfiles}
|
||||
onChangeText={this.searchChannels}
|
||||
onSearchButtonPress={this.searchChannels}
|
||||
onCancelButtonPress={this.cancelSearch}
|
||||
autoCapitalize='none'
|
||||
value={term}
|
||||
|
|
@ -341,16 +349,15 @@ export default class MoreChannels extends PureComponent {
|
|||
</View>
|
||||
<CustomList
|
||||
data={channels}
|
||||
theme={theme}
|
||||
searching={searching}
|
||||
onListEndReached={more}
|
||||
loading={isLoading}
|
||||
listScrollRenderAheadDistance={50}
|
||||
showSections={false}
|
||||
renderRow={ChannelListRow}
|
||||
extraData={loading}
|
||||
key='custom_list'
|
||||
loading={loading}
|
||||
loadingComponent={this.renderLoading()}
|
||||
noResults={this.renderNoResults()}
|
||||
onLoadMore={more}
|
||||
onRowPress={this.onSelectChannel}
|
||||
loadingText={{id: t('mobile.loading_channels'), defaultMessage: 'Loading Channels...'}}
|
||||
showNoResults={this.state.showNoResults}
|
||||
renderItem={ChannelListRow}
|
||||
theme={theme}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<MoreChannels {...props}/>,
|
||||
<MoreChannels {...baseProps}/>,
|
||||
{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(
|
||||
<MoreChannels {...baseProps}/>,
|
||||
{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(
|
||||
<MoreChannels {...baseProps}/>,
|
||||
{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(
|
||||
<MoreChannels {...props}/>,
|
||||
<MoreChannels {...baseProps}/>,
|
||||
{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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<UserListRow
|
||||
|
|
@ -428,28 +338,65 @@ class MoreDirectMessages extends PureComponent {
|
|||
{...props}
|
||||
selectable={true}
|
||||
selected={selected}
|
||||
enabled={enabled}
|
||||
enabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderLoading = () => {
|
||||
const {theme} = this.props;
|
||||
const {loading} = this.state;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (!loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.loadingContainer}>
|
||||
<FormattedText
|
||||
{...loadingText}
|
||||
style={style.loadingText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
renderNoResults = () => {
|
||||
const {loading} = this.state;
|
||||
const {theme} = this.props;
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
if (loading || this.page === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.noResultContainer}>
|
||||
<FormattedText
|
||||
id='mobile.custom_list.no_results'
|
||||
defaultMessage='No Results'
|
||||
style={style.noResultText}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={style.container}>
|
||||
<StatusBar/>
|
||||
|
|
@ -469,47 +416,37 @@ class MoreDirectMessages extends PureComponent {
|
|||
}),
|
||||
};
|
||||
|
||||
let listComponent;
|
||||
if (term.length) {
|
||||
listComponent = (
|
||||
<CustomFlatList
|
||||
theme={theme}
|
||||
items={this.state.profiles}
|
||||
renderItem={this.renderItem}
|
||||
showNoResults={showNoResults}
|
||||
extraData={this.state.selectedIds}
|
||||
onRowPress={this.handleSelectUser}
|
||||
loading={isLoading}
|
||||
loadingText={loadingText}
|
||||
/>
|
||||
);
|
||||
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 = (
|
||||
<CustomSectionList
|
||||
theme={theme}
|
||||
items={this.state.profiles}
|
||||
renderItem={this.renderItem}
|
||||
showNoResults={showNoResults}
|
||||
sectionKeyExtractor={this.sectionKeyExtractor}
|
||||
compareItems={this.compareItems}
|
||||
extraData={this.state.selectedIds}
|
||||
onListEndReached={this.loadMoreProfiles}
|
||||
listScrollRenderAheadDistance={50}
|
||||
onRowPress={this.handleSelectUser}
|
||||
loading={isLoading}
|
||||
loadingText={loadingText}
|
||||
/>
|
||||
);
|
||||
data = createProfilesSections(profiles);
|
||||
listType = SECTIONLIST;
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardLayout>
|
||||
<StatusBar/>
|
||||
<View style={style.searchContainer}>
|
||||
<View style={style.searchBar}>
|
||||
<SearchBar
|
||||
ref='search_bar'
|
||||
placeholder={this.props.intl.formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={this.props.intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={33}
|
||||
inputStyle={searchBarInput}
|
||||
|
|
@ -529,19 +466,53 @@ class MoreDirectMessages extends PureComponent {
|
|||
warnMessage={{id: t('mobile.more_dms.add_more'), defaultMessage: 'You can add {remaining, number} more users'}}
|
||||
maxCount={7}
|
||||
maxMessage={{id: t('mobile.more_dms.cannot_add_more'), defaultMessage: 'You cannot add more users'}}
|
||||
onRemove={this.handleRemoveUser}
|
||||
onRemove={this.handleRemoveProfile}
|
||||
/>
|
||||
</View>
|
||||
{listComponent}
|
||||
<CustomList
|
||||
data={data}
|
||||
extraData={selectedIds}
|
||||
key='custom_list'
|
||||
listType={listType}
|
||||
loading={loading}
|
||||
loadingComponent={this.renderLoading()}
|
||||
noResults={this.renderNoResults()}
|
||||
onLoadMore={this.getProfiles}
|
||||
onRowPress={this.handleSelectProfile}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
/>
|
||||
</KeyboardLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
Loading…
Reference in a new issue