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:
enahum 2017-01-30 10:41:14 -03:00 committed by Harrison Healey
parent 0996644512
commit bfd70f742c
20 changed files with 627 additions and 43 deletions

View file

@ -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);
}
});
};
}

View 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);
};
}

View file

@ -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}

View file

@ -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

View 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);

View 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;

View 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;

View 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;
}
}

View 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);

View file

@ -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;

View file

@ -271,6 +271,11 @@ const state = {
loginId: '',
password: ''
},
optionsModal: {
title: '',
options: [],
visible: false
},
selectServer: {
serverUrl: Config.DefaultServerUrl
}

View file

@ -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
};
}

View file

@ -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
});

View 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
});

View file

@ -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}
/>
);
}

View file

@ -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)
};
}

View file

@ -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",

View 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);
});
});