Modal with options (#179)
* modal options and channel long press * PureComponent optimization (#183) * Bug fix when leaving the current channel * Rebased with new navigation * Add disabled drawer to unit test * re-organize modal up in the stack and controlled by redux * renaming modalOptions to optionsModal
This commit is contained in:
parent
0996644512
commit
bfd70f742c
20 changed files with 627 additions and 43 deletions
|
|
@ -6,7 +6,11 @@ import {batchActions} from 'redux-batched-actions';
|
|||
import {ViewTypes} from 'app/constants';
|
||||
import {updateStorage} from 'app/actions/storage';
|
||||
|
||||
import {fetchMyChannelsAndMembers, getMyChannelMembers, selectChannel} from 'service/actions/channels';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getMyChannelMembers,
|
||||
selectChannel,
|
||||
leaveChannel as serviceLeaveChannel} from 'service/actions/channels';
|
||||
import {getPosts} from 'service/actions/posts';
|
||||
import {savePreferences, deletePreferences} from 'service/actions/preferences';
|
||||
import {getTeamMembersByIds} from 'service/actions/teams';
|
||||
|
|
@ -96,10 +100,10 @@ export function loadPostsIfNecessary(channel) {
|
|||
export function selectInitialChannel(teamId) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const channels = state.entities.channels.channels;
|
||||
const {channels, myMembers} = state.entities.channels;
|
||||
const currentChannelId = state.entities.channels.currentId;
|
||||
|
||||
if (channels[currentChannelId] && channels[currentChannelId].team_id === teamId) {
|
||||
if (channels[currentChannelId] && myMembers[currentChannelId] && channels[currentChannelId].team_id === teamId) {
|
||||
await selectChannel(currentChannelId)(dispatch, getState);
|
||||
return;
|
||||
}
|
||||
|
|
@ -109,7 +113,9 @@ export function selectInitialChannel(teamId) {
|
|||
await selectChannel(channel.id)(dispatch, getState);
|
||||
} else {
|
||||
// Handle case when the default channel cannot be found
|
||||
const firstChannel = Object.values(channels)[0];
|
||||
// so we need to get the first available channel of the team
|
||||
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
|
||||
const firstChannel = channelsInTeam[0].id;
|
||||
await selectChannel(firstChannel.id)(dispatch, getState);
|
||||
}
|
||||
};
|
||||
|
|
@ -145,12 +151,7 @@ export function closeDMChannel(channel) {
|
|||
}];
|
||||
|
||||
if (channel.isFavorite) {
|
||||
const fav = [{
|
||||
user_id: userId,
|
||||
category: Constants.CATEGORY_FAVORITE_CHANNEL,
|
||||
name: channel.id
|
||||
}];
|
||||
deletePreferences(fav)(dispatch, getState);
|
||||
unmarkFavorite(channel.id)(dispatch, getState);
|
||||
}
|
||||
savePreferences(dm)(dispatch, getState).then(() => {
|
||||
if (channel.isCurrent) {
|
||||
|
|
@ -159,3 +160,39 @@ export function closeDMChannel(channel) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function markFavorite(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const userId = getState().entities.users.currentId;
|
||||
const fav = [{
|
||||
user_id: userId,
|
||||
category: Constants.CATEGORY_FAVORITE_CHANNEL,
|
||||
name: channelId,
|
||||
value: 'true'
|
||||
}];
|
||||
savePreferences(fav)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function unmarkFavorite(channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const userId = getState().entities.users.currentId;
|
||||
const fav = [{
|
||||
user_id: userId,
|
||||
category: Constants.CATEGORY_FAVORITE_CHANNEL,
|
||||
name: channelId
|
||||
}];
|
||||
deletePreferences(fav)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function leaveChannel(channel) {
|
||||
return async (dispatch, getState) => {
|
||||
const {currentId: teamId} = getState().entities.teams;
|
||||
serviceLeaveChannel(teamId, channel.id)(dispatch, getState).then(() => {
|
||||
if (channel.isCurrent) {
|
||||
selectInitialChannel(teamId)(dispatch, getState);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
30
app/actions/views/options_modal.js
Normal file
30
app/actions/views/options_modal.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function openModal(title, options) {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ViewTypes.OPTIONS_MODAL_CHANGED,
|
||||
data: {
|
||||
title,
|
||||
options,
|
||||
visible: true
|
||||
}
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function closeModal() {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ViewTypes.OPTIONS_MODAL_CHANGED,
|
||||
data: {
|
||||
title: '',
|
||||
options: [],
|
||||
visible: false
|
||||
}
|
||||
}, getState);
|
||||
};
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ export default class ChannelItem extends React.Component {
|
|||
channel: React.PropTypes.object.isRequired,
|
||||
onSelectChannel: React.PropTypes.func.isRequired,
|
||||
handleClose: React.PropTypes.func,
|
||||
onLongPress: React.PropTypes.func,
|
||||
isActive: React.PropTypes.bool.isRequired,
|
||||
hasUnread: React.PropTypes.bool.isRequired,
|
||||
mentions: React.PropTypes.number.isRequired,
|
||||
|
|
@ -34,6 +35,7 @@ export default class ChannelItem extends React.Component {
|
|||
} = this.props;
|
||||
|
||||
let iconColor = changeOpacity(theme.centerChannelColor, 0.7);
|
||||
const isDirectMessage = channel.type === Constants.DM_CHANNEL;
|
||||
let icon;
|
||||
let activeBorder;
|
||||
let badge;
|
||||
|
|
@ -153,7 +155,7 @@ export default class ChannelItem extends React.Component {
|
|||
}
|
||||
|
||||
let closeButton = null;
|
||||
if (handleClose && !badge) {
|
||||
if (isDirectMessage && !badge) {
|
||||
const closeStyle = {
|
||||
position: 'absolute',
|
||||
justifyContent: 'center',
|
||||
|
|
@ -183,6 +185,10 @@ export default class ChannelItem extends React.Component {
|
|||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.3)}
|
||||
onPress={() => this.props.onSelectChannel(channel)}
|
||||
delayLongPress={1000}
|
||||
onLongPress={() => {
|
||||
this.props.onLongPress(channel);
|
||||
}}
|
||||
>
|
||||
<View style={{flex: 1}}>
|
||||
{activeBorder}
|
||||
|
|
@ -59,8 +59,15 @@ class ChannelList extends React.Component {
|
|||
channelMembers: React.PropTypes.object,
|
||||
theme: React.PropTypes.object.isRequired,
|
||||
onSelectChannel: React.PropTypes.func.isRequired,
|
||||
onViewChannel: React.PropTypes.func.isRequired,
|
||||
handleCloseDM: React.PropTypes.func.isRequired
|
||||
actions: React.PropTypes.shape({
|
||||
viewChannel: React.PropTypes.func.isRequired,
|
||||
closeDMChannel: React.PropTypes.func.isRequired,
|
||||
leaveChannel: React.PropTypes.func.isRequired,
|
||||
markFavorite: React.PropTypes.func.isRequired,
|
||||
unmarkFavorite: React.PropTypes.func.isRequired,
|
||||
openModal: React.PropTypes.func.isRequired,
|
||||
closeModal: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
|
@ -131,36 +138,139 @@ class ChannelList extends React.Component {
|
|||
};
|
||||
|
||||
onSelectChannel = (channel) => {
|
||||
console.log('clicked channel ' + channel.name); // eslint-disable-line no-console
|
||||
|
||||
const {
|
||||
currentChannel,
|
||||
currentTeam
|
||||
} = this.props;
|
||||
|
||||
this.props.onSelectChannel(channel.id);
|
||||
this.props.onViewChannel(currentTeam.id, channel.id, currentChannel.id);
|
||||
this.props.actions.viewChannel(currentTeam.id, channel.id, currentChannel.id);
|
||||
};
|
||||
|
||||
handleClose = (channel) => {
|
||||
this.setState({showOptions: false});
|
||||
this.props.actions.closeDMChannel(channel);
|
||||
};
|
||||
|
||||
handleLeave = (channel, term) => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
Alert.alert(
|
||||
formatMessage({id: 'mobile.channel_list.alertTitleCloseDM', defaultMessage: 'Close Direct Message'}),
|
||||
formatMessage({id: 'mobile.channel_list.alertTitleLeaveChannel', defaultMessage: 'Leave {term}'}, {term}),
|
||||
formatMessage({
|
||||
id: 'mobile.channel_list.alertMessageCloseDM',
|
||||
defaultMessage: 'Are you sure you want to close the DM with {username}'
|
||||
id: 'mobile.channel_list.alertMessageLeaveChannel',
|
||||
defaultMessage: 'Are you sure you want to leave the {term} with {name}?'
|
||||
}, {
|
||||
username: channel.display_name
|
||||
term: term.toLowerCase(),
|
||||
name: channel.display_name
|
||||
}),
|
||||
[
|
||||
{text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'})},
|
||||
{
|
||||
text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}),
|
||||
onPress: () => this.props.handleCloseDM(channel)
|
||||
}]
|
||||
[{
|
||||
text: formatMessage({id: 'mobile.channel_list.alertNo', defaultMessage: 'No'})
|
||||
}, {
|
||||
text: formatMessage({id: 'mobile.channel_list.alertYes', defaultMessage: 'Yes'}),
|
||||
onPress: () => {
|
||||
this.setState({showOptions: false});
|
||||
this.props.actions.leaveChannel(channel);
|
||||
}
|
||||
}]
|
||||
);
|
||||
};
|
||||
|
||||
onShowModal = (channel) => {
|
||||
const {formatMessage} = this.props.intl;
|
||||
let open;
|
||||
let close;
|
||||
let favorite;
|
||||
let title;
|
||||
|
||||
if (channel.type === Constants.DM_CHANNEL) {
|
||||
title = formatMessage({
|
||||
id: 'mobile.channel_list.modalTitle',
|
||||
defaultMessage: 'Select an action for the {term} {name}'},
|
||||
{
|
||||
name: channel.display_name,
|
||||
term: formatMessage({id: 'mobile.channel_list.dm', defaultMessage: 'Direct Message'}).toLowerCase()
|
||||
});
|
||||
|
||||
open = {
|
||||
action: () => {
|
||||
this.props.actions.closeModal();
|
||||
this.onSelectChannel(channel);
|
||||
},
|
||||
text: formatMessage({id: 'mobile.channel_list.openDM', defaultMessage: 'Open Direct Message'})
|
||||
};
|
||||
|
||||
close = {
|
||||
action: () => {
|
||||
this.handleClose(channel);
|
||||
},
|
||||
text: formatMessage({id: 'sidebar.removeList', defaultMessage: 'Remove from list'}),
|
||||
textStyle: {
|
||||
color: '#CC3239'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const term = channel.type === Constants.OPEN_CHANNEL ?
|
||||
formatMessage({id: 'mobile.channel_list.publicChannel', defaultMessage: 'Public Channel'}) :
|
||||
formatMessage({id: 'mobile.channel_list.privateChannel', defaultMessage: 'Private Channel'});
|
||||
|
||||
title = formatMessage({
|
||||
id: 'mobile.channel_list.modalTitle',
|
||||
defaultMessage: 'Select an action for the {term} {name}'},
|
||||
{
|
||||
name: channel.display_name,
|
||||
term: term.toLowerCase()
|
||||
});
|
||||
|
||||
open = {
|
||||
action: () => {
|
||||
this.props.actions.closeModal();
|
||||
this.onSelectChannel(channel);
|
||||
},
|
||||
text: formatMessage({id: 'mobile.channel_list.openChannel', defaultMessage: 'Open {term}'}, {
|
||||
term
|
||||
})
|
||||
};
|
||||
|
||||
if (channel.name !== Constants.DEFAULT_CHANNEL) {
|
||||
close = {
|
||||
action: () => {
|
||||
this.handleLeave(channel, term);
|
||||
},
|
||||
text: formatMessage({id: 'channel_header.leave', defaultMessage: 'Leave {term}'}, {
|
||||
term
|
||||
}),
|
||||
textStyle: {
|
||||
color: '#CC3239'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.isFavorite) {
|
||||
favorite = {
|
||||
action: () => {
|
||||
this.props.actions.closeModal();
|
||||
this.props.actions.unmarkFavorite(channel.id);
|
||||
},
|
||||
text: formatMessage({id: 'channelHeader.removeFromFavorites', defaultMessage: 'Remove from Favorites'})
|
||||
};
|
||||
} else {
|
||||
favorite = {
|
||||
action: () => {
|
||||
this.props.actions.closeModal();
|
||||
this.props.actions.markFavorite(channel.id);
|
||||
},
|
||||
text: formatMessage({id: 'channelHeader.addToFavorites', defaultMessage: 'Add to Favorites'})
|
||||
};
|
||||
}
|
||||
|
||||
const options = [open, favorite];
|
||||
if (close) {
|
||||
options.push(close);
|
||||
}
|
||||
this.props.actions.openModal(title, options);
|
||||
};
|
||||
|
||||
getUnreadMessages = (channel) => {
|
||||
const member = this.props.channelMembers[channel.id];
|
||||
let mentions = 0;
|
||||
|
|
@ -207,7 +317,8 @@ class ChannelList extends React.Component {
|
|||
hasUnread={unread}
|
||||
mentions={mentions}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
handleClose={channel.type === Constants.DM_CHANNEL ? this.handleClose : null}
|
||||
onLongPress={this.onShowModal}
|
||||
handleClose={this.handleClose}
|
||||
isActive={channel.isCurrent}
|
||||
theme={this.props.theme}
|
||||
/>
|
||||
|
|
@ -319,6 +430,7 @@ class ChannelList extends React.Component {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.showBelow) {
|
||||
below = (
|
||||
<UnreadIndicator
|
||||
33
app/components/channel_list/channel_list_container.js
Normal file
33
app/components/channel_list/channel_list_container.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {openModal, closeModal} from 'app/actions/views/options_modal';
|
||||
import {closeDMChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel';
|
||||
|
||||
import {viewChannel} from 'service/actions/channels';
|
||||
import ChannelList from './channel_list';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
viewChannel,
|
||||
closeDMChannel,
|
||||
leaveChannel,
|
||||
markFavorite,
|
||||
unmarkFavorite,
|
||||
openModal,
|
||||
closeModal
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ChannelList);
|
||||
6
app/components/channel_list/index.js
Normal file
6
app/components/channel_list/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ChannelListContainer from './channel_list_container';
|
||||
|
||||
export default ChannelListContainer;
|
||||
6
app/components/options_modal/index.js
Normal file
6
app/components/options_modal/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import OptionsModalContainer from './options_modal_container';
|
||||
|
||||
export default OptionsModalContainer;
|
||||
223
app/components/options_modal/options_modal.js
Normal file
223
app/components/options_modal/options_modal.js
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React, {PropTypes, PureComponent} from 'react';
|
||||
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
Modal,
|
||||
Text,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
|
||||
const {width} = Dimensions.get('window');
|
||||
const styles = StyleSheet.create({
|
||||
overlayStyle: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: 10,
|
||||
paddingLeft: 5,
|
||||
backgroundColor: 'rgba(0,0,0,0.3)'
|
||||
},
|
||||
|
||||
optionContainer: {
|
||||
borderRadius: 10,
|
||||
width: (width - 10),
|
||||
minHeight: 50,
|
||||
backgroundColor: '#fff',
|
||||
marginBottom: 5
|
||||
},
|
||||
|
||||
cancelStyle: {
|
||||
borderRadius: 10,
|
||||
width: (width - 10),
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#fff',
|
||||
padding: 8
|
||||
},
|
||||
|
||||
cancelTextStyle: {
|
||||
textAlign: 'center',
|
||||
color: '#4E8ACC',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 17
|
||||
},
|
||||
|
||||
optionStyle: {
|
||||
padding: 8,
|
||||
height: 50,
|
||||
justifyContent: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#999999'
|
||||
},
|
||||
|
||||
optionTextStyle: {
|
||||
textAlign: 'center',
|
||||
fontSize: 17,
|
||||
color: '#4E8ACC'
|
||||
},
|
||||
|
||||
titleStyle: {
|
||||
paddingTop: 8,
|
||||
paddingHorizontal: 10,
|
||||
height: 70,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#999999'
|
||||
},
|
||||
|
||||
titleTextStyle: {
|
||||
fontSize: 15,
|
||||
color: '#7f8180'
|
||||
}
|
||||
});
|
||||
|
||||
export default class OptionsModal extends PureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
visible: PropTypes.bool,
|
||||
onOptionSelected: PropTypes.func,
|
||||
style: View.propTypes.style,
|
||||
titleStyle: View.propTypes.style,
|
||||
titleTextStyle: View.propTypes.style,
|
||||
cancelStyle: View.propTypes.style,
|
||||
cancelTextStyle: Text.propTypes.style,
|
||||
overlayStyle: View.propTypes.style,
|
||||
cancelText: PropTypes.string,
|
||||
actions: React.PropTypes.shape({
|
||||
closeModal: React.PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
title: '',
|
||||
options: [],
|
||||
visible: false,
|
||||
style: {},
|
||||
cancelStyle: {},
|
||||
cancelTextStyle: {},
|
||||
overlayStyle: {},
|
||||
titleStyle: {},
|
||||
titleTextStyle: {},
|
||||
cancelText: 'Cancel'
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
animationType: 'slide',
|
||||
transparent: false
|
||||
};
|
||||
}
|
||||
|
||||
onOptionSelected = (option) => {
|
||||
// do not change the way this is event handler as its necessary to properly display any alert boxes
|
||||
// that needs to be shown by selecting an option from the modal.
|
||||
// By hiding the modal previous to that will cause a rendering bug.
|
||||
if (option) {
|
||||
option.action();
|
||||
} else {
|
||||
this.props.actions.closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
renderOption = (option, index) => {
|
||||
const optionStyle = [styles.optionStyle];
|
||||
const optionTextStyle = [styles.optionTextStyle];
|
||||
|
||||
if (option.style) {
|
||||
optionStyle.push(option.style);
|
||||
}
|
||||
|
||||
if (option.textStyle) {
|
||||
optionTextStyle.push(option.textStyle);
|
||||
}
|
||||
|
||||
let text = option.text;
|
||||
if (!(text instanceof FormattedText)) {
|
||||
text = (
|
||||
<Text style={optionTextStyle}>{option.text}</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
onPress={() => this.onOptionSelected(option)}
|
||||
>
|
||||
<View style={optionStyle}>
|
||||
{text}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
renderOptionList = () => {
|
||||
const options = this.props.options.map((option, index) => {
|
||||
return this.renderOption(option, index);
|
||||
});
|
||||
|
||||
let title;
|
||||
if (this.props.title) {
|
||||
title = (
|
||||
<View style={[styles.titleStyle, this.props.titleStyle]}>
|
||||
<Text style={[styles.titleTextStyle, this.props.titleTextStyle]}>
|
||||
{this.props.title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.overlayStyle, this.props.overlayStyle]}>
|
||||
<View style={styles.optionContainer}>
|
||||
<ScrollView keyboardShouldPersistTaps='never'>
|
||||
<View style={{paddingHorizontal: 10}}>
|
||||
{title}
|
||||
{options}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<View>
|
||||
<TouchableOpacity onPress={() => this.onOptionSelected(null)}>
|
||||
<View style={[styles.cancelStyle, this.props.cancelStyle]}>
|
||||
<Text style={[styles.cancelTextStyle, this.props.cancelTextStyle]}>
|
||||
{this.props.cancelText}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.options.length) {
|
||||
return (
|
||||
<View style={this.props.style}>
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={this.props.visible}
|
||||
onRequestClose={() => this.onOptionSelected(null)}
|
||||
animationType={this.state.animationType}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={() => this.onOptionSelected(null)}>
|
||||
{this.renderOptionList()}
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
25
app/components/options_modal/options_modal_container.js
Normal file
25
app/components/options_modal/options_modal_container.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {closeModal} from 'app/actions/views/options_modal';
|
||||
import OptionsModal from './options_modal';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
...state.views.optionsModal
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
closeModal
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(OptionsModal);
|
||||
|
|
@ -9,8 +9,9 @@ const ViewTypes = keyMirror({
|
|||
LOGIN_ID_CHANGED: null,
|
||||
PASSWORD_CHANGED: null,
|
||||
|
||||
TOGGLE_CHANNEL_DRAWER: null,
|
||||
POST_DRAFT_CHANGED: null
|
||||
POST_DRAFT_CHANGED: null,
|
||||
|
||||
OPTIONS_MODAL_CHANGED: null
|
||||
});
|
||||
|
||||
export default ViewTypes;
|
||||
|
|
|
|||
|
|
@ -271,6 +271,11 @@ const state = {
|
|||
loginId: '',
|
||||
password: ''
|
||||
},
|
||||
optionsModal: {
|
||||
title: '',
|
||||
options: [],
|
||||
visible: false
|
||||
},
|
||||
selectServer: {
|
||||
serverUrl: Config.DefaultServerUrl
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ import {bindActionCreators} from 'redux';
|
|||
import {closeDrawers, goBack} from 'app/actions/navigation';
|
||||
import Drawer from 'app/components/drawer';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import OptionsModal from 'app/components/options_modal';
|
||||
import {RouteTransitions} from 'app/navigation/routes';
|
||||
import {getComponentForScene} from 'app/scenes';
|
||||
|
||||
class Router extends React.Component {
|
||||
static propTypes = {
|
||||
navigation: React.PropTypes.object,
|
||||
modalVisible: React.PropTypes.bool.isRequired,
|
||||
actions: React.PropTypes.shape({
|
||||
closeDrawers: React.PropTypes.func.isRequired,
|
||||
goBack: React.PropTypes.func.isRequired
|
||||
|
|
@ -97,7 +99,7 @@ class Router extends React.Component {
|
|||
const SceneComponent = getComponentForScene(route.key);
|
||||
|
||||
return <SceneComponent {...route.props}/>;
|
||||
}
|
||||
};
|
||||
|
||||
configureTransition = () => {
|
||||
return {
|
||||
|
|
@ -116,6 +118,8 @@ class Router extends React.Component {
|
|||
|
||||
let leftDrawerContent;
|
||||
if (leftDrawerRoute) {
|
||||
// TODO: We should make it so that this is availabe once we login
|
||||
// son when sliding it renders correctly without the neeed to open using the top-bar
|
||||
leftDrawerContent = this.renderRoute(leftDrawerRoute);
|
||||
}
|
||||
|
||||
|
|
@ -124,19 +128,28 @@ class Router extends React.Component {
|
|||
rightDrawerContent = this.renderRoute(rightDrawerRoute);
|
||||
}
|
||||
|
||||
const {modalVisible} = this.props;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={leftDrawerOpen}
|
||||
type='displace'
|
||||
disabled={modalVisible}
|
||||
content={leftDrawerContent}
|
||||
tapToClose={true}
|
||||
openDrawerOffset={0.2}
|
||||
onRequestClose={this.props.actions.closeDrawers}
|
||||
panOpenMask={0.1}
|
||||
panCloseMask={0.2}
|
||||
panThreshold={0.2}
|
||||
acceptPan={true}
|
||||
negotiatePan={true}
|
||||
>
|
||||
<Drawer
|
||||
open={rightDrawerOpen}
|
||||
type='displace'
|
||||
side='right'
|
||||
disabled={modalVisible}
|
||||
content={rightDrawerContent}
|
||||
tapToClose={true}
|
||||
openDrawerOffset={0.2}
|
||||
|
|
@ -148,6 +161,7 @@ class Router extends React.Component {
|
|||
render={this.renderTransition}
|
||||
configureTransition={this.configureTransition}
|
||||
/>
|
||||
<OptionsModal/>
|
||||
</Drawer>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
@ -155,8 +169,10 @@ class Router extends React.Component {
|
|||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const modalVisible = state.views.optionsModal.visible;
|
||||
return {
|
||||
navigation: state.navigation
|
||||
navigation: state.navigation,
|
||||
modalVisible
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ import {combineReducers} from 'redux';
|
|||
import channel from './channel';
|
||||
import i18n from './i18n';
|
||||
import login from './login';
|
||||
import optionsModal from './options_modal';
|
||||
import selectServer from './select_server';
|
||||
|
||||
export default combineReducers({
|
||||
channel,
|
||||
i18n,
|
||||
login,
|
||||
optionsModal,
|
||||
selectServer
|
||||
});
|
||||
|
|
|
|||
46
app/reducers/views/options_modal.js
Normal file
46
app/reducers/views/options_modal.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {combineReducers} from 'redux';
|
||||
|
||||
import {UsersTypes} from 'service/constants';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
function title(state = '', action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.OPTIONS_MODAL_CHANGED:
|
||||
return action.data.title;
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
return '';
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function options(state = [], action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.OPTIONS_MODAL_CHANGED:
|
||||
return action.data.options;
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
return [];
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function visible(state = false, action) {
|
||||
switch (action.type) {
|
||||
case ViewTypes.OPTIONS_MODAL_CHANGED:
|
||||
return action.data.visible;
|
||||
case UsersTypes.LOGOUT_SUCCESS:
|
||||
return false;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
title,
|
||||
options,
|
||||
visible
|
||||
});
|
||||
|
|
@ -3,15 +3,13 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import ChannelList from 'app/components/channel_drawer/channel_list';
|
||||
import ChannelList from 'app/components/channel_list';
|
||||
|
||||
export default class ChannelDrawer extends React.PureComponent {
|
||||
static propTypes = {
|
||||
actions: React.PropTypes.shape({
|
||||
selectChannel: React.PropTypes.func.isRequired,
|
||||
viewChannel: React.PropTypes.func.isRequired,
|
||||
closeDMChannel: React.PropTypes.func.isRequired,
|
||||
closeDrawers: React.PropTypes.func.isRequired
|
||||
closeDrawers: React.PropTypes.func.isRequired,
|
||||
selectChannel: React.PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
currentTeam: React.PropTypes.object,
|
||||
currentChannel: React.PropTypes.object,
|
||||
|
|
@ -23,7 +21,7 @@ export default class ChannelDrawer extends React.PureComponent {
|
|||
selectChannel = (id) => {
|
||||
this.props.actions.selectChannel(id);
|
||||
this.props.actions.closeDrawers();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
|
@ -42,8 +40,6 @@ export default class ChannelDrawer extends React.PureComponent {
|
|||
channelMembers={channelMembers}
|
||||
theme={theme}
|
||||
onSelectChannel={this.selectChannel}
|
||||
onViewChannel={this.props.actions.viewChannel}
|
||||
handleCloseDM={this.props.actions.closeDMChannel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
|
|||
import {connect} from 'react-redux';
|
||||
|
||||
import {closeDrawers} from 'app/actions/navigation';
|
||||
import {closeDMChannel} from 'app/actions/views/channel';
|
||||
import {closeDMChannel, leaveChannel, markFavorite, unmarkFavorite} from 'app/actions/views/channel';
|
||||
|
||||
import {selectChannel, viewChannel} from 'service/actions/channels';
|
||||
import {getChannelsByCategory, getCurrentChannel} from 'service/selectors/entities/channels';
|
||||
|
|
@ -30,7 +30,10 @@ function mapDispatchToProps(dispatch) {
|
|||
selectChannel,
|
||||
viewChannel,
|
||||
closeDMChannel,
|
||||
closeDrawers
|
||||
closeDrawers,
|
||||
leaveChannel,
|
||||
markFavorite,
|
||||
unmarkFavorite
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -959,6 +959,8 @@
|
|||
"calling_screen": "Calling",
|
||||
"center_panel.recent": "Click here to jump to recent messages. ",
|
||||
"channel_header.addMembers": "Add Members",
|
||||
"channelHeader.addToFavorites": "Add to Favorites",
|
||||
"channelHeader.removeFromFavorites": "Remove from Favorites",
|
||||
"change_url.close": "Close",
|
||||
"change_url.endWithLetter": "Must end with a letter or number",
|
||||
"change_url.invalidUrl": "Invalid URL",
|
||||
|
|
@ -1480,10 +1482,15 @@
|
|||
"member_list.noUsersAdd": "No users to add.",
|
||||
"members_popover.msg": "Message",
|
||||
"members_popover.title": "Members",
|
||||
"mobile.channel_list.alertTitleCloseDM": "Close Direct Message",
|
||||
"mobile.channel_list.alertMessageCloseDM": "Are you sure you want to close the DM with {username}",
|
||||
"mobile.channel_list.alertTitleLeaveChannel": "Leave {term}",
|
||||
"mobile.channel_list.alertMessageLeaveChannel": "Are you sure you want to leave the {term} with {name}?",
|
||||
"mobile.channel_list.alertNo": "No",
|
||||
"mobile.channel_list.alertYes": "Yes",
|
||||
"mobile.channel_list.open": "Open {term}",
|
||||
"mobile.channel_list.openDM": "Open Direct Message",
|
||||
"mobile.channel_list.dm": "Direct Message",
|
||||
"mobile.channel_list.privateChannel": "Private Channel",
|
||||
"mobile.channel_list.publicChannel": "Public Channel",
|
||||
"mobile.components.channels_list_view.yourChannels": "Your channels:",
|
||||
"mobile.components.select_server_view.enterServerUrl": "Enter Server URL",
|
||||
"mobile.components.select_server_view.continue": "Continue",
|
||||
|
|
|
|||
30
test/app/actions/options_modal.js
Normal file
30
test/app/actions/options_modal.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import * as Actions from 'app/actions/views/options_modal';
|
||||
import configureStore from 'app/store';
|
||||
|
||||
describe('Actions.Views.OptionsModal', () => {
|
||||
let store;
|
||||
beforeEach(() => {
|
||||
store = configureStore();
|
||||
});
|
||||
|
||||
it('openModal', async () => {
|
||||
await Actions.openModal('title', [1, 2, 3])(store.dispatch, store.getState);
|
||||
const {optionsModal} = store.getState().views;
|
||||
assert.equal('title', optionsModal.title);
|
||||
assert.deepEqual([1, 2, 3], optionsModal.options);
|
||||
assert.ok(optionsModal.visible);
|
||||
});
|
||||
|
||||
it('closeModal', async () => {
|
||||
await Actions.closeModal()(store.dispatch, store.getState);
|
||||
const {optionsModal} = store.getState().views;
|
||||
assert.equal('', optionsModal.title);
|
||||
assert.deepEqual([], optionsModal.options);
|
||||
assert.ifError(optionsModal.visible);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue