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:
parent
4506e13c95
commit
8a7840b58a
24 changed files with 1287 additions and 510 deletions
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
29
app/components/conditional_touchable.js
Normal file
29
app/components/conditional_touchable.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
});
|
||||
});
|
||||
24
app/components/custom_list/channel_list_row/index.js
Normal file
24
app/components/custom_list/channel_list_row/index.js
Normal 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);
|
||||
95
app/components/custom_list/custom_list_row.js
Normal file
95
app/components/custom_list/custom_list_row.js
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
22
app/components/custom_list/user_list_row/index.js
Normal file
22
app/components/custom_list/user_list_row/index.js
Normal 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);
|
||||
117
app/components/custom_list/user_list_row/user_list_row.js
Normal file
117
app/components/custom_list/user_list_row/user_list_row.js
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
310
app/components/custom_section_list.js
Normal file
310
app/components/custom_section_list.js
Normal 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)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
22
app/screens/more_dms/selected_users/index.js
Normal file
22
app/screens/more_dms/selected_users/index.js
Normal 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);
|
||||
90
app/screens/more_dms/selected_users/selected_user.js
Normal file
90
app/screens/more_dms/selected_users/selected_user.js
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
136
app/screens/more_dms/selected_users/selected_users.js
Normal file
136
app/screens/more_dms/selected_users/selected_users.js
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue