RN-152 Add ability to create new group direct messages (#793)

* RN-152 Added ability to open group messages from MoreDirectMessages

* Refactored row components for CustomList

* Added CustomSectionList component to mirror CustomList

* Added control to display all selected users in More DMs list

* Updated title of More DMs modal

* Updated yarn.lock

* Fixed channel name not being set when creating GM channel

* Fixed error on Android
This commit is contained in:
Harrison Healey 2017-07-27 19:08:13 -04:00 committed by enahum
parent 4506e13c95
commit 8a7840b58a
24 changed files with 1287 additions and 510 deletions

View file

@ -2,9 +2,9 @@
// See License.txt for license information.
import {getDirectChannelName} from 'mattermost-redux/utils/channel_utils';
import {createDirectChannel} from 'mattermost-redux/actions/channels';
import {createDirectChannel, createGroupChannel} from 'mattermost-redux/actions/channels';
import {getProfilesByIds, getStatusesByIds} from 'mattermost-redux/actions/users';
import {handleSelectChannel, toggleDMChannel} from 'app/actions/views/channel';
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
export function makeDirectChannel(otherUserId) {
return async (dispatch, getState) => {
@ -12,21 +12,45 @@ export function makeDirectChannel(otherUserId) {
const {currentUserId} = state.entities.users;
const channelName = getDirectChannelName(currentUserId, otherUserId);
const {channels, myMembers} = state.entities.channels;
const channel = Object.values(channels).find((c) => c.name === channelName);
getProfilesByIds([otherUserId])(dispatch, getState);
getStatusesByIds([otherUserId])(dispatch, getState);
let result;
let channel = Object.values(channels).find((c) => c.name === channelName);
if (channel && myMembers[channel.id]) {
result = {data: channel};
toggleDMChannel(otherUserId, 'true')(dispatch, getState);
handleSelectChannel(channel.id)(dispatch, getState);
return true;
}
const created = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
if (created.data) {
handleSelectChannel(created.data.id)(dispatch, getState);
} else {
result = await createDirectChannel(currentUserId, otherUserId)(dispatch, getState);
channel = result.data;
}
return created;
if (channel) {
handleSelectChannel(channel.id)(dispatch, getState);
}
return result;
};
}
export function makeGroupChannel(otherUserIds) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId} = state.entities.users;
getProfilesByIds(otherUserIds)(dispatch, getState);
getStatusesByIds(otherUserIds)(dispatch, getState);
const result = await createGroupChannel([currentUserId, ...otherUserIds])(dispatch, getState);
const channel = result.data;
if (channel) {
toggleGMChannel(channel.id, 'true')(dispatch, getState);
handleSelectChannel(channel.id)(dispatch, getState);
}
return result;
};
}

View file

@ -47,8 +47,7 @@ class List extends Component {
showAbove: false
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).
then((source) => {
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
@ -239,7 +238,7 @@ class List extends Component {
navigator.showModal({
screen: 'MoreDirectMessages',
title: intl.formatMessage({id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}),
title: intl.formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',

View file

@ -0,0 +1,29 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {TouchableOpacity} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class ConditionalTouchable extends React.PureComponent {
static propTypes = {
touchable: PropTypes.bool,
children: CustomPropTypes.Children.isRequired
};
render() {
const {touchable, children, ...otherProps} = this.props;
if (touchable) {
return (
<TouchableOpacity {...otherProps}>
{children}
</TouchableOpacity>
);
}
return React.Children.only(children);
}
}

View file

@ -1,148 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
function createTouchableComponent(children, action) {
return (
<TouchableOpacity onPress={action}>
{children}
</TouchableOpacity>
);
}
function ChannelListRow(props) {
const {id, displayName, purpose, onPress, theme} = props;
const style = getStyleFromTheme(theme);
let purposeComponent;
if (purpose) {
purposeComponent = (
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
<Text
style={style.purpose}
ellipsizeMode='tail'
numberOfLines={1}
>
{purpose}
</Text>
</View>
);
}
const RowComponent = (
<View style={style.container}>
<View style={style.titleContainer}>
{props.selectable &&
<TouchableWithoutFeedback onPress={props.onRowSelect}>
<View style={style.selectorContainer}>
<View style={[style.selector, (props.selected && style.selectorFilled)]}>
{props.selected &&
<Icon
name='check'
size={16}
color='#fff'
/>
}
</View>
</View>
</TouchableWithoutFeedback>
}
<Icon
name='globe'
style={style.icon}
/>
<View style={style.textContainer}>
<View style={{flexGrow: 1, flexDirection: 'column'}}>
<Text style={style.displayName}>
{displayName}
</Text>
</View>
</View>
</View>
{purposeComponent}
</View>
);
if (typeof onPress === 'function') {
return createTouchableComponent(RowComponent, () => onPress(id));
}
return RowComponent;
}
ChannelListRow.propTypes = {
id: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
purpose: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
onPress: PropTypes.func,
selectable: PropTypes.bool,
onRowSelect: PropTypes.func,
selected: PropTypes.bool
};
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flexDirection: 'column',
height: 65,
paddingHorizontal: 15,
justifyContent: 'center',
backgroundColor: theme.centerChannelBg
},
titleContainer: {
alignItems: 'center',
flexDirection: 'row'
},
displayName: {
fontSize: 16,
color: theme.centerChannelColor
},
icon: {
fontSize: 16,
color: theme.centerChannelColor
},
textContainer: {
flex: 1,
flexDirection: 'row',
marginLeft: 5
},
purpose: {
marginTop: 7,
fontSize: 13,
color: changeOpacity(theme.centerChannelColor, 0.5)
},
selector: {
height: 28,
width: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: '#888',
alignItems: 'center',
justifyContent: 'center'
},
selectorContainer: {
height: 50,
paddingRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
selectorFilled: {
backgroundColor: '#378FD2',
borderWidth: 0
}
});
});
export default ChannelListRow;

View file

@ -0,0 +1,95 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import CustomListRow from 'app/components/custom_list/custom_list_row';
export default class ChannelListRow extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
...CustomListRow.propTypes
};
onPress = () => {
this.props.onPress(this.props.id);
};
render() {
const style = getStyleFromTheme(this.props.theme);
let purpose;
if (this.props.channel.purpose) {
purpose = (
<Text
style={style.purpose}
ellipsizeMode='tail'
numberOfLines={1}
>
{this.props.channel.purpose}
</Text>
);
}
return (
<CustomListRow
id={this.props.id}
theme={this.props.theme}
onPress={this.props.onPress ? this.onPress : null}
enabled={this.props.enabled}
selectable={this.props.selectable}
selected={this.props.selected}
>
<View style={style.container}>
<View style={style.titleContainer}>
<Icon
name='globe'
style={style.icon}
/>
<Text style={style.displayName}>
{this.props.channel.display_name}
</Text>
</View>
{purpose}
</View>
</CustomListRow>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
titleContainer: {
alignItems: 'center',
flexDirection: 'row'
},
displayName: {
fontSize: 16,
color: theme.centerChannelColor,
marginLeft: 5
},
icon: {
fontSize: 16,
color: theme.centerChannelColor
},
container: {
flex: 1,
flexDirection: 'column'
},
purpose: {
marginTop: 7,
fontSize: 13,
color: changeOpacity(theme.centerChannelColor, 0.5)
}
});
});

View file

@ -0,0 +1,24 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import ChannelListRow from './channel_list_row';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
return {
theme: getTheme(state),
channel: getChannel(state, ownProps),
...ownProps
};
};
}
export default connect(makeMapStateToProps)(ChannelListRow);

View file

@ -0,0 +1,95 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {
StyleSheet,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import ConditionalTouchable from 'app/components/conditional_touchable';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class CustomListRow extends React.PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired,
onPress: PropTypes.func,
enabled: PropTypes.bool,
selectable: PropTypes.bool,
selected: PropTypes.bool,
children: CustomPropTypes.Children
};
static defaultProps = {
enabled: true
};
render() {
const style = getStyleFromTheme(this.props.theme);
return (
<ConditionalTouchable
touchable={Boolean(this.props.enabled && this.props.onPress)}
onPress={this.props.onPress}
>
<View style={style.container}>
{this.props.selectable &&
<View style={style.selectorContainer}>
<View style={[style.selector, (this.props.selected && style.selectorFilled), (!this.props.enabled && style.selectorDisabled)]}>
{this.props.selected &&
<Icon
name='check'
size={16}
color='#fff'
/>
}
</View>
</View>
}
{this.props.children}
</View>
</ConditionalTouchable>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flexDirection: 'row',
height: 65,
paddingHorizontal: 15,
alignItems: 'center',
backgroundColor: theme.centerChannelBg
},
displayName: {
fontSize: 15,
color: theme.centerChannelColor
},
selector: {
height: 28,
width: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: '#888',
alignItems: 'center',
justifyContent: 'center'
},
selectorContainer: {
height: 50,
paddingRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
selectorDisabled: {
backgroundColor: '#888'
},
selectorFilled: {
backgroundColor: '#378FD2',
borderWidth: 0
}
});
});

View file

@ -13,16 +13,15 @@ export default class CustomList extends PureComponent {
data: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
searching: PropTypes.bool,
onRowPress: PropTypes.func,
onListEndReached: PropTypes.func,
onListEndReachedThreshold: PropTypes.number,
teammateNameDisplay: PropTypes.string,
loading: PropTypes.bool,
loadingText: PropTypes.object,
listPageSize: PropTypes.number,
listInitialSize: PropTypes.number,
listScrollRenderAheadDistance: PropTypes.number,
showSections: PropTypes.bool,
onRowPress: PropTypes.func,
selectable: PropTypes.bool,
onRowSelect: PropTypes.func,
renderRow: PropTypes.func.isRequired,
@ -37,9 +36,8 @@ export default class CustomList extends PureComponent {
listPageSize: 10,
listInitialSize: 10,
listScrollRenderAheadDistance: 200,
selectable: false,
loadingText: null,
onRowSelect: () => true,
selectable: false,
createSections: () => true,
showSections: true,
showNoResults: true
@ -114,17 +112,32 @@ export default class CustomList extends PureComponent {
);
};
renderRow = (rowData, sectionId, rowId) => {
return this.props.renderRow(
rowData,
sectionId,
rowId,
this.props.teammateNameDisplay,
this.props.theme,
this.props.selectable,
this.props.onRowPress,
this.handleRowSelect
);
renderRow = (item, sectionId, rowId) => {
const props = {
id: item.id,
item,
selected: item.selected,
selectable: this.props.selectable,
onPress: this.props.onRowPress
};
if ('disableSelect' in item) {
props.enabled = !item.disableSelect;
}
if (this.props.onRowSelect) {
props.onPress = this.handleRowSelect.bind(this, sectionId, rowId);
} else {
props.onPress = this.props.onRowPress;
}
// Allow passing in a component like UserListRow or ChannelListRow
if (this.props.renderRow.prototype.isReactComponent) {
const RowComponent = this.props.renderRow;
return <RowComponent {...props}/>;
}
return this.props.renderRow(props);
};
renderSeparator = (sectionId, rowId) => {

View file

@ -1,142 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
function createTouchableComponent(children, action) {
return (
<TouchableOpacity onPress={action}>
{children}
</TouchableOpacity>
);
}
function MemberListRow(props) {
const {id, displayName, username, onPress, theme, user, disableSelect} = props;
const style = getStyleFromTheme(theme);
const RowComponent = (
<View style={style.container}>
{props.selectable &&
<TouchableWithoutFeedback onPress={disableSelect ? () => false : props.onRowSelect}>
<View style={style.selectorContainer}>
<View style={[style.selector, (props.selected && style.selectorFilled), (disableSelect && style.selectorDisabled)]}>
{props.selected &&
<Icon
name='check'
size={16}
color='#fff'
/>
}
</View>
</View>
</TouchableWithoutFeedback>
}
<ProfilePicture
user={user}
size={32}
/>
<View style={style.textContainer}>
<View style={{flexDirection: 'column'}}>
<Text style={style.displayName}>
{displayName}
</Text>
</View>
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
<Text
style={style.username}
ellipsizeMode='tail'
numberOfLines={1}
>
{`(@${username})`}
</Text>
</View>
</View>
</View>
);
if (typeof onPress === 'function') {
return createTouchableComponent(RowComponent, () => onPress(id));
} else if (typeof props.onRowSelect === 'function') {
return createTouchableComponent(RowComponent, disableSelect ? () => false : props.onRowSelect);
}
return RowComponent;
}
MemberListRow.propTypes = {
id: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
pictureURL: PropTypes.string,
username: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
onPress: PropTypes.func,
selectable: PropTypes.bool,
onRowSelect: PropTypes.func,
selected: PropTypes.bool,
disableSelect: PropTypes.bool
};
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flexDirection: 'row',
height: 65,
paddingHorizontal: 15,
alignItems: 'center',
backgroundColor: theme.centerChannelBg
},
displayName: {
fontSize: 15,
color: theme.centerChannelColor
},
icon: {
fontSize: 20,
color: theme.centerChannelColor
},
textContainer: {
flexDirection: 'row',
marginLeft: 5
},
username: {
marginLeft: 5,
fontSize: 15,
color: changeOpacity(theme.centerChannelColor, 0.5)
},
selector: {
height: 28,
width: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: '#888',
alignItems: 'center',
justifyContent: 'center'
},
selectorContainer: {
height: 50,
paddingRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
selectorDisabled: {
backgroundColor: '#888'
},
selectorFilled: {
backgroundColor: '#378FD2',
borderWidth: 0
}
});
});
export default MemberListRow;

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import UserListRow from './user_list_row';
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
user: getUser(state, ownProps.id),
teammateNameDisplay: getTeammateNameDisplaySetting(state),
...ownProps
};
}
export default connect(mapStateToProps)(UserListRow);

View file

@ -0,0 +1,117 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import CustomListRow from 'app/components/custom_list/custom_list_row';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
export default class UserListRow extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
user: PropTypes.object.isRequired,
teammateNameDisplay: PropTypes.string.isRequired,
...CustomListRow.propTypes
};
onPress = () => {
this.props.onPress(this.props.id);
};
render() {
const style = getStyleFromTheme(this.props.theme);
return (
<CustomListRow
id={this.props.id}
theme={this.props.theme}
onPress={this.props.onPress ? this.onPress : null}
enabled={this.props.enabled}
selectable={this.props.selectable}
selected={this.props.selected}
>
<ProfilePicture
user={this.props.user}
size={32}
/>
<View style={style.textContainer}>
<View>
<Text style={style.displayName}>
{displayUsername(this.props.user, this.props.teammateNameDisplay)}
</Text>
</View>
<View>
<Text
style={style.username}
ellipsizeMode='tail'
numberOfLines={1}
>
{`(@${this.props.user.username})`}
</Text>
</View>
</View>
</CustomListRow>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flexDirection: 'row',
height: 65,
paddingHorizontal: 15,
alignItems: 'center',
backgroundColor: theme.centerChannelBg
},
displayName: {
fontSize: 15,
color: theme.centerChannelColor
},
icon: {
fontSize: 20,
color: theme.centerChannelColor
},
textContainer: {
flexDirection: 'row',
marginLeft: 5
},
username: {
marginLeft: 5,
fontSize: 15,
color: changeOpacity(theme.centerChannelColor, 0.5)
},
selector: {
height: 28,
width: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: '#888',
alignItems: 'center',
justifyContent: 'center'
},
selectorContainer: {
height: 50,
paddingRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
selectorDisabled: {
backgroundColor: '#888'
},
selectorFilled: {
backgroundColor: '#378FD2',
borderWidth: 0
}
});
});

View file

@ -0,0 +1,310 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
SectionList,
StyleSheet,
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 defaultKeyExtractor = (item) => {
return item.id;
};
static defaultProps = {
showNoResults: true,
keyExtractor: CustomSectionList.defaultKeyExtractor,
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}
/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
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%'
},
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)
}
});
});

View file

@ -12,10 +12,11 @@ import {
} from 'react-native';
import Loading from 'app/components/loading';
import MemberList from 'app/components/custom_list';
import CustomList from 'app/components/custom_list';
import UserListRow from 'app/components/custom_list/user_list_row';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {createMembersSections, loadingText, markSelectedProfiles, renderMemberRow} from 'app/utils/member_list';
import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General, RequestStatus} from 'mattermost-redux/constants';
@ -177,8 +178,7 @@ class ChannelAddMembers extends PureComponent {
let {page} = this.state;
if (loadMoreRequestStatus !== RequestStatus.STARTED && next && !searching) {
page = page + 1;
actions.getProfilesNotInChannel(currentTeam.id, currentChannel.id, page, General.PROFILE_CHUNK_SIZE).
then((data) => {
actions.getProfilesNotInChannel(currentTeam.id, currentChannel.id, page, General.PROFILE_CHUNK_SIZE).then((data) => {
if (data && data.length) {
this.setState({
page
@ -192,7 +192,7 @@ class ChannelAddMembers extends PureComponent {
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'add-members') {
if (event.id === this.addButton.id) {
this.handleAddMembersPress();
}
}
@ -259,7 +259,7 @@ class ChannelAddMembers extends PureComponent {
value={term}
/>
</View>
<MemberList
<CustomList
data={profiles}
theme={theme}
searching={searching}
@ -270,7 +270,7 @@ class ChannelAddMembers extends PureComponent {
loadingText={loadingText}
selectable={this.state.canSelect}
onRowSelect={this.handleRowSelect}
renderRow={renderMemberRow}
renderRow={UserListRow}
createSections={createMembersSections}
/>
</View>

View file

@ -12,15 +12,15 @@ import {
import {injectIntl, intlShape} from 'react-intl';
import Loading from 'app/components/loading';
import MemberList from 'app/components/custom_list';
import CustomList from 'app/components/custom_list';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list';
import MemberListRow from 'app/components/custom_list/member_list_row';
import UserListRow from 'app/components/custom_list/user_list_row';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {displayUsername, filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
class ChannelMembers extends PureComponent {
static propTypes = {
@ -30,7 +30,6 @@ class ChannelMembers extends PureComponent {
currentChannelMembers: PropTypes.array.isRequired,
currentUserId: PropTypes.string.isRequired,
navigator: PropTypes.object,
teammateNameDisplay: PropTypes.string,
requestStatus: PropTypes.string,
searchRequestStatus: PropTypes.string,
removeMembersStatus: PropTypes.string,
@ -235,28 +234,14 @@ class ChannelMembers extends PureComponent {
actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove);
};
renderMemberRow = (user, sectionId, rowId, teammateNameDisplay, theme, selectable, onPress, onSelect) => {
const {id, username} = user;
const displayName = displayUsername(user, teammateNameDisplay);
let onRowSelect = null;
if (selectable) {
onRowSelect = () => onSelect(sectionId, rowId);
}
const disableSelect = user.id === this.props.currentUserId;
renderMemberRow = (props) => {
const enabled = props.id !== this.props.currentUserId;
return (
<MemberListRow
id={id}
user={user}
displayName={displayName}
username={username}
theme={theme}
onPress={onPress}
selectable={selectable}
selected={user.selected}
onRowSelect={onRowSelect}
disableSelect={disableSelect}
<UserListRow
{...props}
selectable={true}
enabled={enabled}
/>
);
};
@ -277,7 +262,7 @@ class ChannelMembers extends PureComponent {
};
render() {
const {canManageUsers, intl, teammateNameDisplay, requestStatus, searchRequestStatus, theme} = this.props;
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) ||
@ -321,17 +306,15 @@ class ChannelMembers extends PureComponent {
value={term}
/>
</View>
<MemberList
<CustomList
data={profiles}
theme={theme}
searching={searching}
onListEndReached={more}
teammateNameDisplay={teammateNameDisplay}
listScrollRenderAheadDistance={50}
loading={isLoading}
loadingText={loadingText}
selectable={canManageUsers && this.state.canSelect}
onRowSelect={this.handleRowSelect}
onRowSelect={canManageUsers && this.state.canSelect ? this.handleRowSelect : null}
renderRow={this.renderMemberRow}
createSections={createMembersSections}
showNoResults={showNoResults}

View file

@ -7,7 +7,6 @@ import {connect} from 'react-redux';
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {getProfilesInChannel, searchProfiles} from 'mattermost-redux/actions/users';
@ -19,7 +18,6 @@ function mapStateToProps(state) {
currentChannel: getCurrentChannel(state) || {},
currentChannelMembers: getProfilesInCurrentChannel(state),
currentUserId: state.entities.users.currentUserId,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
requestStatus: state.requests.users.getProfilesInChannel.status,
searchRequestStatus: state.requests.users.searchProfiles.status,
removeMembersStatus: state.requests.channels.removeChannelMember.status,

View file

@ -14,7 +14,7 @@ import {
import {General, RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ChannelList from 'app/components/custom_list';
import CustomList from 'app/components/custom_list';
import ChannelListRow from 'app/components/custom_list/channel_list_row';
import Loading from 'app/components/loading';
import SearchBar from 'app/components/search_bar';
@ -162,8 +162,8 @@ class MoreChannels extends PureComponent {
this.props.actions.getChannels(
this.props.currentTeamId,
page,
General.CHANNELS_CHUNK_SIZE).
then((data) => {
General.CHANNELS_CHUNK_SIZE
).then((data) => {
if (data && data.length) {
this.setState({
page
@ -175,27 +175,6 @@ class MoreChannels extends PureComponent {
}
};
renderChannelRow = (channel, sectionId, rowId, preferences, theme, selectable, onPress, onSelect) => {
const {id, display_name: displayName, purpose} = channel;
let onRowSelect = null;
if (selectable) {
onRowSelect = () => onSelect(sectionId, rowId);
}
return (
<ChannelListRow
id={id}
displayName={displayName}
purpose={purpose}
theme={theme}
onPress={onPress}
selectable={selectable}
selected={channel.selected}
onRowSelect={onRowSelect}
/>
);
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
@ -307,16 +286,15 @@ class MoreChannels extends PureComponent {
value={term}
/>
</View>
<ChannelList
<CustomList
data={channels}
theme={theme}
searching={searching}
onListEndReached={more}
loading={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
showSections={false}
renderRow={this.renderChannelRow}
renderRow={ChannelListRow}
onRowPress={this.onSelectChannel}
loadingText={{id: 'mobile.loading_channels', defaultMessage: 'Loading Channels...'}}
showNoResults={this.state.showNoResults}

View file

@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {setChannelDisplayName} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {makeDirectChannel, makeGroupChannel} from 'app/actions/views/more_dms';
import {getTheme} from 'app/selectors/preferences';
import {getProfiles, getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users';
@ -60,9 +60,11 @@ function mapStateToProps(state, ownProps) {
...ownProps,
config,
teammateNameDisplay: getTeammateNameDisplaySetting(state),
allProfiles: getUsers(state),
profiles,
theme: getTheme(state),
currentDisplayName: state.views.channel.displayName,
currentUserId: getCurrentUserId(state),
currentTeamId: getCurrentTeamId(state),
getRequest,
searchRequest
@ -73,6 +75,7 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
makeDirectChannel,
makeGroupChannel,
getProfiles,
getProfilesInTeam,
searchProfiles,

View file

@ -12,30 +12,40 @@ import {
import {General, RequestStatus} 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 CustomSectionList from 'app/components/custom_section_list';
import UserListRow from 'app/components/custom_list/user_list_row';
import Loading from 'app/components/loading';
import MemberList from 'app/components/custom_list';
import SearchBar from 'app/components/search_bar';
import StatusBar from 'app/components/status_bar';
import {alertErrorWithFallback} from 'app/utils/general';
import {createMembersSections, loadingText, renderMemberRow} from 'app/utils/member_list';
import {loadingText} from 'app/utils/member_list';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import SelectedUsers from './selected_users';
const START_BUTTON = 'start-conversation';
const CLOSE_BUTTON = 'close-dms';
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,
profiles: PropTypes.array,
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,
getProfiles: PropTypes.func.isRequired,
getProfilesInTeam: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired,
@ -49,29 +59,21 @@ class MoreDirectMessages extends PureComponent {
this.searchTimeoutId = 0;
this.state = {
profiles: props.profiles.splice(0, General.PROFILE_CHUNK_SIZE),
profiles: props.profiles.slice(0, General.PROFILE_CHUNK_SIZE),
page: 0,
adding: false,
next: true,
searching: false,
showNoResults: false,
term: ''
term: '',
canStartConversation: false,
loadingChannel: false,
canSelect: true,
selectedIds: {},
selectedCount: 0
};
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
}
componentWillReceiveProps(nextProps) {
const {getRequest} = this.props;
if (getRequest.status === RequestStatus.STARTED &&
nextProps.getRequest.status === RequestStatus.SUCCESS) {
const {page} = this.state;
const profiles = nextProps.profiles.splice(0, (page + 1) * General.PROFILE_CHUNK_SIZE);
this.setState({profiles, showNoResults: true});
} else if (this.state.searching &&
nextProps.searchRequest.status === RequestStatus.SUCCESS) {
const results = filterProfilesMatchingTerm(nextProps.profiles, this.state.term);
this.setState({profiles: results, showNoResults: true});
}
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
this.updateNavigationButtons(false);
}
componentDidMount() {
@ -82,6 +84,50 @@ class MoreDirectMessages extends PureComponent {
}, 400);
}
componentWillReceiveProps(nextProps) {
const {getRequest} = this.props;
if (getRequest.status === RequestStatus.STARTED &&
nextProps.getRequest.status === RequestStatus.SUCCESS) {
const {page} = this.state;
const profiles = nextProps.profiles.slice(0, (page + 1) * General.PROFILE_CHUNK_SIZE);
this.setState({profiles, showNoResults: true});
} else if (this.state.searching &&
nextProps.searchRequest.status === RequestStatus.SUCCESS) {
const results = filterProfilesMatchingTerm(nextProps.profiles, this.state.term);
this.setState({profiles: results, showNoResults: true});
}
}
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);
}
}
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'
@ -90,7 +136,9 @@ class MoreDirectMessages extends PureComponent {
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'close-dms') {
if (event.id === START_BUTTON) {
this.startConversation();
} else if (event.id === CLOSE_BUTTON) {
this.close();
}
}
@ -137,6 +185,10 @@ class MoreDirectMessages extends PureComponent {
};
loadMoreProfiles = () => {
if (this.state.searching) {
return;
}
let {page} = this.state;
if (this.props.getRequest.status !== RequestStatus.STARTED && this.state.next && !this.state.searching) {
page = page + 1;
@ -152,126 +204,224 @@ class MoreDirectMessages extends PureComponent {
}
};
onSelectMember = async (id) => {
const {actions, currentDisplayName, intl, teammateNameDisplay, profiles} = this.props;
const user = profiles.find((p) => p.id === id);
handleSelectUser = (id) => {
this.setState((prevState) => {
const wasSelected = prevState.selectedIds[id];
this.setState({adding: true});
// Prevent selecting too many users
if (!wasSelected && Object.keys(prevState.selectedIds).length >= General.MAX_USERS_IN_GM - 1) {
return {};
}
// save the current channel display name in case it fails
const currentChannelDisplayName = currentDisplayName;
const selectedIds = {...prevState.selectedIds};
const userDisplayName = displayUsername(user, teammateNameDisplay);
if (wasSelected) {
Reflect.deleteProperty(selectedIds, id);
} else {
selectedIds[id] = true;
}
if (user) {
actions.setChannelDisplayName(userDisplayName);
} else {
actions.setChannelDisplayName('');
return {
selectedIds,
selectedCount: Object.keys(selectedIds).length
};
});
};
handleRemoveUser = (id) => {
this.setState((prevState) => {
const selectedIds = {...prevState.selectedIds};
Reflect.deleteProperty(selectedIds, id);
return {
selectedIds,
selectedCount: Object.keys(selectedIds).length
};
});
}
startConversation = async () => {
if (this.state.loadingChannel) {
return;
}
const result = await actions.makeDirectChannel(id);
this.setState({
loadingChannel: true
});
// Save the current channel display name in case it fails
const currentChannelDisplayName = this.props.currentDisplayName;
const selectedIds = 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
});
this.props.actions.setChannelDisplayName(currentChannelDisplayName);
}
};
makeGroupChannel = async (ids) => {
const result = await this.props.actions.makeGroupChannel(ids);
const displayName = getGroupDisplayNameFromUserIds(ids, this.props.allProfiles, this.props.currentUserId, this.props.teammateNameDisplay);
this.props.actions.setChannelDisplayName(displayName);
if (result.error) {
actions.setChannelDisplayName(currentChannelDisplayName);
alertErrorWithFallback(
intl,
this.props.intl,
result.error,
{
id: '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;
};
makeDirectChannel = async (id) => {
const user = this.state.profiles[id];
const displayName = displayUsername(user, this.props.teammateNameDisplay);
this.props.actions.setChannelDisplayName(displayName);
const result = await this.props.actions.makeDirectChannel(id);
if (result.error) {
alertErrorWithFallback(
this.props.intl,
result.error,
{
id: 'mobile.open_dm.error',
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again."
},
{
displayName: userDisplayName
displayName
}
);
this.setState({adding: false});
} else {
EventEmitter.emit('close_channel_drawer');
InteractionManager.runAfterInteractions(() => {
this.close();
});
}
return !result.error;
};
sectionKeyExtractor = (user) => {
// Group items alphabetically by first letter
return displayUsername(user, this.props.teammateNameDisplay)[0].toUpperCase();
}
compareItems = (a, b) => {
const aName = displayUsername(a, this.props.teammateNameDisplay);
const bName = displayUsername(b, this.props.teammateNameDisplay);
return aName.localeCompare(bName);
};
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
key={props.id}
{...props}
selectable={true}
selected={selected}
enabled={enabled}
/>
);
};
render() {
const {
intl,
teammateNameDisplay,
getRequest,
searchRequest,
theme
} = this.props;
const {
adding,
profiles,
searching,
loadingChannel,
showNoResults,
term
} = this.state;
const {formatMessage} = intl;
const isLoading = (
getRequest.status === RequestStatus.STARTED) || (getRequest.status === RequestStatus.NOT_STARTED) ||
(searchRequest.status === RequestStatus.STARTED);
const style = getStyleFromTheme(theme);
const more = this.state.searching ? () => true : this.loadMoreProfiles;
let content;
if (adding) {
content = (
if (loadingChannel) {
return (
<View style={style.container}>
<StatusBar/>
<Loading/>
</View>
);
} else {
content = (
<View style={style.container}>
<StatusBar/>
<View
style={{marginVertical: 5}}
>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
color: theme.centerChannelColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
value={term}
/>
</View>
<MemberList
data={profiles}
theme={theme}
searching={searching}
onListEndReached={more}
teammateNameDisplay={teammateNameDisplay}
loading={isLoading}
selectable={false}
listScrollRenderAheadDistance={50}
createSections={createMembersSections}
renderRow={renderMemberRow}
onRowPress={this.onSelectMember}
loadingText={loadingText}
showNoResults={showNoResults}
/>
</View>
);
}
return content;
return (
<View style={style.container}>
<StatusBar/>
<View style={style.searchContainer}>
<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'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
color: theme.centerChannelColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.onSearch}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
value={term}
/>
<SelectedUsers
selectedIds={this.state.selectedIds}
warnCount={5}
warnMessage={{id: 'mobile.more_dms.add_more', defaultMessage: 'You can add {remaining, number} more users'}}
maxCount={7}
maxMessage={{id: 'mobile.more_dms.cannot_add_more', defaultMessage: 'You cannot add more users'}}
onRemove={this.handleRemoveUser}
/>
</View>
<CustomSectionList
theme={theme}
items={this.state.profiles}
renderItem={this.renderItem}
showNoResults={showNoResults}
sectionKeyExtractor={this.sectionKeyExtractor}
compareItems={this.compareItems}
extraData={this.state.selectedIds}
onRowPress={this.handleSelectUser}
loading={isLoading}
loadingText={loadingText}
/>
</View>
);
}
}
@ -280,6 +430,9 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
searchContainer: {
marginVertical: 5
}
});
});

View file

@ -0,0 +1,22 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getUsers} from 'mattermost-redux/selectors/entities/users';
import SelectedUsers from './selected_users';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state),
profiles: getUsers(state),
teammateNameDisplay: getTeammateNameDisplaySetting(state)
};
}
export default connect(mapStateToProps)(SelectedUsers);

View file

@ -0,0 +1,90 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
export default class SelectedUser extends React.PureComponent {
static propTypes = {
/*
* The current theme.
*/
theme: PropTypes.object.isRequired,
/*
* How to display the names of users.
*/
teammateNameDisplay: PropTypes.string.isRequired,
/*
* The user that this component represents.
*/
user: PropTypes.object.isRequired,
/*
* A handler function that will deselect a user when clicked on.
*/
onRemove: PropTypes.func.isRequired
};
onRemove = () => {
this.props.onRemove(this.props.user.id);
};
render() {
const style = getStyleFromTheme(this.props.theme);
return (
<View style={style.container}>
<Text style={style.text}>
{displayUsername(this.props.user, this.props.teammateNameDisplay)}
</Text>
<TouchableOpacity
style={style.remove}
onPress={this.onRemove}
>
<Icon
name='close'
size={14}
color={this.props.theme.centerChannelColor}
/>
</TouchableOpacity>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
alignItems: 'center',
flexDirection: 'row',
height: 27,
borderRadius: 3,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
marginBottom: 2,
marginRight: 10,
marginTop: 10,
paddingLeft: 10
},
remove: {
paddingHorizontal: 10
},
text: {
color: theme.centerChannelColor,
fontSize: 13
}
});
});

View file

@ -0,0 +1,136 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {StyleSheet, View} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import SelectedUser from 'app/screens/more_dms/selected_users/selected_user';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SelectedUsers extends React.PureComponent {
static propTypes = {
/*
* The current theme.
*/
theme: PropTypes.object.isRequired,
/*
* An object mapping user ids to a falsey value indicating whether or not they've been selected.
*/
selectedIds: PropTypes.object.isRequired,
/*
* An object mapping user ids to users.
*/
profiles: PropTypes.object.isRequired,
/*
* How to display the names of users.
*/
teammateNameDisplay: PropTypes.string.isRequired,
/*
* The number of users that will be selected when we start to display a message indicating
* the remaining number of users that can be selected.
*/
warnCount: PropTypes.number.isRequired,
/*
* An i18n string displaying how many more users can be selected.
*/
warnMessage: PropTypes.object.isRequired,
/*
* The maximum number of users that can be selected.
*/
maxCount: PropTypes.number.isRequired,
/*
* An i18n string displayed when no more users can be selected.
*/
maxMessage: PropTypes.object.isRequired,
/*
* A handler function that will deselect a user when clicked on.
*/
onRemove: PropTypes.func.isRequired
};
render() {
const users = [];
for (const id of Object.keys(this.props.selectedIds)) {
if (!this.props.selectedIds[id]) {
continue;
}
users.push(
<SelectedUser
key={id}
user={this.props.profiles[id]}
theme={this.props.theme}
teammateNameDisplay={this.props.teammateNameDisplay}
onRemove={this.props.onRemove}
/>
);
}
if (users.length === 0) {
return null;
}
const style = getStyleFromTheme(this.props.theme);
let message = null;
if (users.length >= this.props.maxCount) {
message = (
<FormattedText
style={style.message}
{...this.props.maxMessage}
/>
);
} else if (users.length >= this.props.warnCount) {
message = (
<FormattedText
style={style.message}
{...this.props.warnMessage}
values={{
remaining: this.props.maxCount - users.length
}}
/>
);
}
return (
<View style={style.container}>
<View style={style.users}>
{users}
</View>
{message}
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
marginLeft: 5,
marginBottom: 5
},
users: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap'
},
message: {
color: changeOpacity(theme.centerChannelColor, 0.6),
fontSize: 12,
marginRight: 5,
marginTop: 10,
marginBottom: 2
}
});
});

View file

@ -1,10 +1,6 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import MemberListRow from 'app/components/custom_list/member_list_row';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
export const loadingText = {
id: 'mobile.loading_members',
defaultMessage: 'Loading Members...'
@ -26,29 +22,6 @@ export function createMembersSections(data) {
return sections;
}
export function renderMemberRow(user, sectionId, rowId, teammateNameDisplay, theme, selectable, onPress, onSelect) {
const {id, username} = user;
const displayName = displayUsername(user, teammateNameDisplay);
let onRowSelect = null;
if (selectable) {
onRowSelect = () => onSelect(sectionId, rowId);
}
return (
<MemberListRow
id={id}
user={user}
displayName={displayName}
username={username}
theme={theme}
onPress={onPress}
selectable={selectable}
selected={user.selected}
onRowSelect={onRowSelect}
/>
);
}
export function markSelectedProfiles(profiles, selectedProfiles) {
return profiles.map((p) => {
const profile = {...p};

View file

@ -1766,11 +1766,14 @@
"mobile.loading_members": "Loading Members...",
"mobile.loading_posts": "Loading Messages...",
"mobile.login_options.choose_title": "Choose your login method",
"mobile.more_dms.start": "Start",
"mobile.more_dms.title": "New Conversation",
"mobile.notification.in": " in ",
"mobile.offlineIndicator.connected": "Connected",
"mobile.offlineIndicator.connecting": "Connecting...",
"mobile.offlineIndicator.offline": "No internet connection",
"mobile.open_dm.error": "We couldn't open a direct message with {displayName}. Please check your connection and try again.",
"mobile.open_gm.error": "We couldn't open a group message with those users. Please check your connection and try again.",
"mobile.post.cancel": "Cancel",
"mobile.post.delete_question": "Are you sure you want to delete this post?",
"mobile.post.delete_title": "Delete Post",

View file

@ -3645,7 +3645,7 @@ makeerror@1.0.x:
mattermost-redux@mattermost/mattermost-redux#master:
version "0.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/81416da8fd9f2df12286a732c7e98fd031dfd329"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/9797cb8bd8fa61252336a7c6150bd364f7ca28b1"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"