PLT-5840 Prevent double tapping (#464)

* PLT-5840 Prevent double tapping

* handle double tapping on channel header

* Review feedback
This commit is contained in:
enahum 2017-04-09 22:43:18 -03:00 committed by GitHub
parent 15ae765f52
commit c675b17e3c
26 changed files with 287 additions and 211 deletions

View file

@ -10,32 +10,27 @@ import {
} from 'react-native';
import ChanneIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelDrawerItem extends PureComponent {
static propTypes = {
channel: PropTypes.object.isRequired,
onSelectChannel: PropTypes.func.isRequired,
onLongPress: PropTypes.func,
isActive: PropTypes.bool.isRequired,
hasUnread: PropTypes.bool.isRequired,
mentions: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
handleLongPress = (channel) => {
if (this.props.onLongPress) {
this.props.onLongPress(channel);
}
};
render() {
const {
channel,
theme,
mentions,
hasUnread,
isActive
isActive,
onSelectChannel
} = this.props;
const style = getStyleSheet(theme);
@ -84,11 +79,7 @@ export default class ChannelDrawerItem extends PureComponent {
return (
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={() => this.props.onSelectChannel(channel)}
delayLongPress={1000}
onLongPress={() => {
this.handleLongPress(channel);
}}
onPress={() => preventDoubleTap(onSelectChannel, this, channel)}
>
<View style={style.container}>
{activeBorder}

View file

@ -16,6 +16,7 @@ import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {Constants} from 'mattermost-redux/constants';
@ -235,7 +236,7 @@ class ChannelDrawerList extends Component {
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={action}
onPress={() => preventDoubleTap(action, this)}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
>
<MaterialIcon
@ -292,7 +293,7 @@ class ChannelDrawerList extends Component {
const settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => this.props.actions.openSettingsModal()}
onPress={() => preventDoubleTap(this.props.actions.openSettingsModal)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon

View file

@ -12,58 +12,6 @@ import {
import Icon from 'react-native-vector-icons/FontAwesome';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
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: {
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
}
});
});
function createTouchableComponent(children, action) {
return (
<TouchableOpacity onPress={action}>
@ -136,4 +84,56 @@ ChannelListRow.propTypes = {
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: {
fontSize: 13,
color: changeOpacity(theme.centerChannelColor, 0.5)
},
selector: {
height: 28,
width: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: '#888',
alignItems: 'center',
justifyContent: 'center'
},
selectorContainer: {
height: 50,
paddingRight: 15,
alignItems: 'center',
justifyContent: 'center'
},
selectorFilled: {
backgroundColor: '#378FD2',
borderWidth: 0
}
});
});
export default ChannelListRow;

View file

@ -68,7 +68,7 @@ function MemberListRow(props) {
if (typeof onPress === 'function') {
return createTouchableComponent(RowComponent, () => onPress(id));
} else if (typeof props.onRowSelect === 'function') {
return createTouchableComponent(RowComponent, props.onRowSelect);
return createTouchableComponent(RowComponent, disableSelect ? () => false : props.onRowSelect);
}
return RowComponent;

View file

@ -11,6 +11,7 @@ import {
TouchableOpacity
} from 'react-native';
import {preventDoubleTap} from 'app/utils/tap';
import FileAttachment from './file_attachment';
export default class FileAttachmentList extends Component {
@ -40,7 +41,7 @@ export default class FileAttachmentList extends Component {
<TouchableOpacity
key={file.id}
onLongPress={this.props.onLongPress}
onPress={() => this.handleOnPress(file)}
onPress={() => preventDoubleTap(this.handleOnPress, this, file)}
onPressIn={() => this.props.toggleSelected(true)}
onPressOut={() => this.props.toggleSelected(false)}
>

View file

@ -14,15 +14,16 @@ import {
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import FileAttachmentList from 'app/components/file_attachment_list/file_attachment_list_container';
import FormattedText from 'app/components/formatted_text';
import FormattedTime from 'app/components/formatted_time';
import MattermostIcon from 'app/components/mattermost_icon';
import Markdown from 'app/components/markdown/markdown';
import OptionsContext from 'app/components/options_context';
import ProfilePicture from 'app/components/profile_picture';
import FileAttachmentList from 'app/components/file_attachment_list/file_attachment_list_container';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ReplyIcon from 'app/components/reply_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import webhookIcon from 'assets/images/icons/webhook.jpg';
@ -119,7 +120,7 @@ class Post extends PureComponent {
handlePress = () => {
const {post, onPress} = this.props;
if (onPress && post.state !== Constants.POST_DELETED && !isSystemMessage(post)) {
onPress(post);
preventDoubleTap(onPress, null, post);
}
};
@ -307,7 +308,7 @@ class Post extends PureComponent {
};
viewUserProfile = () => {
this.props.actions.goToUserProfile(this.props.user.id);
preventDoubleTap(this.props.actions.goToUserProfile, null, this.props.user.id);
};
toggleSelected = (selected) => {

View file

@ -20,33 +20,6 @@ import Section from './section';
import SectionItem from './section_item';
import SaveNotificationsButton from './save_notifications_button';
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
height: 1,
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
marginHorizontal: 15
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
scrollViewContent: {
paddingBottom: 30
},
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});
const SAVE_NOTIFY_PROPS = 'save_notify_props';
const SAVING_NOTIFY_PROPS = 'saving_notify_props';
@ -548,3 +521,30 @@ export default class AccountNotifications extends PureComponent {
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
input: {
color: theme.centerChannelColor,
fontSize: 12,
height: 40
},
separator: {
height: 1,
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
marginHorizontal: 15
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
scrollViewContent: {
paddingBottom: 30
},
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});

View file

@ -10,6 +10,7 @@ import {
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AccountSettings extends PureComponent {
@ -18,7 +19,7 @@ export default class AccountSettings extends PureComponent {
goToAccountNotifications: PropTypes.func.isRequired
}),
theme: PropTypes.object.isRequired
}
};
static navigationProps = {
renderLeftComponent: (props, emitter, theme) => {
@ -53,7 +54,7 @@ export default class AccountSettings extends PureComponent {
>
<TouchableOpacity
style={style.item}
onPress={action}
onPress={() => preventDoubleTap(action)}
>
<View style={style.itemLeftIconContainer}>
<Icon
@ -76,7 +77,7 @@ export default class AccountSettings extends PureComponent {
{separator && <View style={style.separator}/>}
</View>
);
}
};
renderItems = () => {
return [
@ -86,7 +87,7 @@ export default class AccountSettings extends PureComponent {
this.buildItemRow('mobile', 'user.settings.modal.display', 'Display', () => true, true, false),
this.buildItemRow('wrench', 'user.settings.modal.advanced', 'Advanced', () => true, false, false)
];
}
};
render() {
const {theme} = this.props;

View file

@ -11,6 +11,7 @@ import {
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import PostTextbox from 'app/components/post_textbox';
import {preventDoubleTap} from 'app/utils/tap';
import {Constants} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -53,9 +54,11 @@ export default class Channel extends React.PureComponent {
}
};
canPress = true;
componentWillMount() {
this.props.subscribeToHeaderEvent('open_channel_drawer', this.openChannelDrawer);
this.props.subscribeToHeaderEvent('show_channel_info', this.props.actions.goToChannelInfo);
this.props.subscribeToHeaderEvent('open_channel_drawer', () => preventDoubleTap(this.openChannelDrawer, this));
this.props.subscribeToHeaderEvent('show_channel_info', () => preventDoubleTap(this.props.actions.goToChannelInfo));
EventEmitter.on('leave_team', this.handleLeaveTeam);
this.props.actions.initWebSocket(Platform.OS);
if (this.props.currentTeam) {

View file

@ -84,7 +84,7 @@ export default class ChannelPostList extends PureComponent {
toValue: 0,
duration: 500
}).start();
}
};
didPostsLoad(nextProps, postsRequest) {
const nextGetPostsStatus = nextProps.postsRequests[postsRequest].status;

View file

@ -62,6 +62,7 @@ class ChannelAddMembers extends PureComponent {
};
state = {
canSelect: true,
profiles: [],
selectedMembers: {}
};
@ -168,6 +169,7 @@ class ChannelAddMembers extends PureComponent {
};
emitAdding = (loading) => {
this.setState({canSelect: false});
EventEmitter.emit('adding_members', loading);
};
@ -239,7 +241,7 @@ class ChannelAddMembers extends PureComponent {
listScrollRenderAheadDistance={50}
loading={isLoading}
loadingText={loadingText}
selectable={true}
selectable={this.state.canSelect}
onRowSelect={this.handleRowSelect}
renderRow={renderMemberRow}
createSections={createMembersSections}

View file

@ -10,6 +10,7 @@ import {
View
} from 'react-native';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {Constants} from 'mattermost-redux/constants';
@ -64,14 +65,6 @@ class ChannelInfo extends PureComponent {
}
}
handleFavorite = () => {
const {isFavorite, actions, currentChannel} = this.props;
const {markFavorite, unmarkFavorite} = actions;
const toggleFavorite = isFavorite ? unmarkFavorite : markFavorite;
this.setState({isFavorite: !isFavorite});
toggleFavorite(currentChannel.id);
};
handleDeleteOrLeave(eventType) {
const {formatMessage} = this.props.intl;
const channel = this.props.currentChannel;
@ -134,6 +127,14 @@ class ChannelInfo extends PureComponent {
}
};
handleFavorite = () => {
const {isFavorite, actions, currentChannel} = this.props;
const {markFavorite, unmarkFavorite} = actions;
const toggleFavorite = isFavorite ? unmarkFavorite : markFavorite;
this.setState({isFavorite: !isFavorite});
toggleFavorite(currentChannel.id);
};
renderLeaveOrDeleteChannelRow() {
const channel = this.props.currentChannel;
const isDefaultChannel = channel.name === Constants.DEFAULT_CHANNEL;
@ -218,7 +219,7 @@ class ChannelInfo extends PureComponent {
**/
}
<ChannelInfoRow
action={this.props.actions.goToChannelMembers}
action={() => preventDoubleTap(this.props.actions.goToChannelMembers)}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
@ -229,7 +230,7 @@ class ChannelInfo extends PureComponent {
<View>
<View style={style.separator}/>
<ChannelInfoRow
action={this.props.actions.goToChannelAddMembers}
action={() => preventDoubleTap(this.props.actions.goToChannelAddMembers)}
defaultMessage='Add Members'
icon='user-plus'
textId='channel_header.addMembers'
@ -239,7 +240,7 @@ class ChannelInfo extends PureComponent {
</View>
}
<ChannelInfoRow
action={() => this.handleDeleteOrLeave('leave')}
action={() => preventDoubleTap(this.handleDeleteOrLeave, this, 'leave')}
defaultMessage='Leave Channel'
icon='sign-out'
textId='navbar.leave'
@ -250,7 +251,7 @@ class ChannelInfo extends PureComponent {
{this.renderLeaveOrDeleteChannelRow() && canDeleteChannel &&
<View style={style.footer}>
<ChannelInfoRow
action={() => this.handleDeleteOrLeave('delete')}
action={() => preventDoubleTap(this.handleDeleteOrLeave, this, 'delete')}
defaultMessage='Delete Channel'
icon='trash'
iconColor='#CA3B27'
@ -263,7 +264,7 @@ class ChannelInfo extends PureComponent {
{this.renderCloseDirect() &&
<View style={style.footer}>
<ChannelInfoRow
action={this.handleClose}
action={() => preventDoubleTap(this.handleClose, this)}
defaultMessage={defaultMessage}
icon='times'
iconColor='#CA3B27'

View file

@ -59,6 +59,7 @@ class ChannelMembers extends PureComponent {
};
state = {
canSelect: true,
profiles: [],
selectedMembers: {}
};
@ -191,6 +192,7 @@ class ChannelMembers extends PureComponent {
};
emitRemoving = (loading) => {
this.setState({canSelect: false});
EventEmitter.emit('removing_members', loading);
};
@ -288,7 +290,7 @@ class ChannelMembers extends PureComponent {
listScrollRenderAheadDistance={50}
loading={isLoading}
loadingText={loadingText}
selectable={canManageUsers}
selectable={canManageUsers && this.state.canSelect}
onRowSelect={this.handleRowSelect}
renderRow={this.renderMemberRow}
createSections={createMembersSections}

View file

@ -4,6 +4,7 @@
import React, {Component, PropTypes} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
ActivityIndicator,
Image,
Keyboard,
KeyboardAvoidingView,
@ -226,6 +227,31 @@ class Login extends Component {
};
render() {
const isLoading = this.props.loginRequest.status === RequestStatus.STARTED;
let proceed;
if (isLoading) {
proceed = (
<ActivityIndicator
animating={true}
size='small'
/>
);
} else {
proceed = (
<Button
onPress={this.preSignIn}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
id='login.signIn'
defaultMessage='Sign in'
style={GlobalStyles.signupButtonText}
/>
</Button>
);
}
return (
<KeyboardAvoidingView
behavior='padding'
@ -274,16 +300,7 @@ class Login extends Component {
returnKeyType='go'
onSubmitEditing={this.preSignIn}
/>
<Button
onPress={this.preSignIn}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
id='login.signIn'
defaultMessage='Sign in'
style={GlobalStyles.signupButtonText}
/>
</Button>
{proceed}
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>

View file

@ -11,6 +11,7 @@ import {
import Button from 'react-native-button';
import FormattedText from 'app/components/formatted_text';
import {GlobalStyles} from 'app/styles';
import {preventDoubleTap} from 'app/utils/tap';
import logo from 'assets/images/logo.png';
@ -30,7 +31,7 @@ export default class LoginOptions extends PureComponent {
return (
<Button
key='email'
onPress={this.props.actions.goToLogin}
onPress={() => preventDoubleTap(this.props.actions.goToLogin)}
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#2389d7'}]}
>
<FormattedText
@ -51,7 +52,7 @@ export default class LoginOptions extends PureComponent {
return (
<Button
key='saml'
onPress={this.props.actions.goToSaml}
onPress={() => preventDoubleTap(this.props.actions.goToSaml)}
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#34a28b'}]}
>
<Text

View file

@ -3,6 +3,7 @@
import React, {Component} from 'react';
import {
ActivityIndicator,
Image,
Keyboard,
KeyboardAvoidingView,
@ -96,6 +97,32 @@ export default class Mfa extends Component {
};
render() {
const isLoading = this.props.loginRequest.status === RequestStatus.STARTED;
let proceed;
if (isLoading) {
proceed = (
<ActivityIndicator
animating={true}
size='small'
/>
);
} else {
proceed = (
<Button
onPress={this.submit}
loading={false}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
style={GlobalStyles.signupButtonText}
id='mobile.components.select_server_view.proceed'
defaultMessage='Proceed'
/>
</Button>
);
}
return (
<KeyboardAvoidingView
behavior='padding'
@ -128,17 +155,7 @@ export default class Mfa extends Component {
returnKeyType='go'
underlineColorAndroid='transparent'
/>
<Button
onPress={this.submit}
loading={false}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
style={GlobalStyles.signupButtonText}
id='mobile.components.select_server_view.proceed'
defaultMessage='Proceed'
/>
</Button>
{proceed}
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>

View file

@ -3,12 +3,9 @@
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {
TouchableOpacity,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import ActionButton from 'app/components/action_button';
import {getTheme} from 'app/selectors/preferences';
import {Constants} from 'mattermost-redux/constants';
@ -22,18 +19,15 @@ function CreateButton(props) {
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity
onPress={() => props.emitter('new_channel')}
style={{paddingHorizontal: 15}}
>
<FormattedText
id='mobile.create_channel'
defaultMessage='Create'
style={{color: props.theme.sidebarHeaderTextColor}}
/>
</TouchableOpacity>
</View>
<ActionButton
actionEventName='create_channel'
emitter={props.emitter}
enabled={true}
enableEventName='can_create_channel'
labelDefaultMessage='Create'
labelId='mobile.create_channel'
loadingEventName='creating_channel'
/>
);
}

View file

@ -17,9 +17,10 @@ import ChannelListRow from 'app/components/custom_list/channel_list_row';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import SearchBar from 'app/components/search_bar';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {Constants, RequestStatus} from 'mattermost-redux/constants';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import CreateButton from './create_button';
@ -81,7 +82,7 @@ class MoreChannels extends PureComponent {
componentWillMount() {
this.props.subscribeToHeaderEvent('close', this.props.actions.goBack);
this.props.subscribeToHeaderEvent('new_channel', this.onCreateChannel);
this.props.subscribeToHeaderEvent('create_channel', this.onCreateChannel);
}
componentWillReceiveProps(nextProps) {
@ -112,13 +113,17 @@ class MoreChannels extends PureComponent {
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent('close');
this.props.unsubscribeFromHeaderEvent('new_channel');
this.props.unsubscribeFromHeaderEvent('create_channel');
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
}
emitCanCreateChannel = (enabled) => {
EventEmitter.emit('can_create_channel', enabled);
};
filterChannels = (channels, term) => {
return channels.filter((c) => {
return (c.name.toLowerCase().indexOf(term) !== -1 || c.display_name.toLowerCase().indexOf(term) !== -1);
@ -204,6 +209,7 @@ class MoreChannels extends PureComponent {
};
onSelectChannel = async (id) => {
this.emitCanCreateChannel(false);
this.setState({adding: true});
this.searchBar.blur();
await this.props.actions.joinChannel(

View file

@ -28,20 +28,20 @@ export default class OptionsModal extends PureComponent {
PropTypes.string,
PropTypes.object
])
}
};
static defaultProps = {
onCancelPress: () => false
}
};
static navigationProps = {
hideNavBar: true,
modalAnimationType: 'fade'
}
};
state = {
top: new Animated.Value(deviceHeight)
}
};
componentDidMount() {
Animated.timing(this.state.top, {
@ -64,7 +64,7 @@ export default class OptionsModal extends PureComponent {
this.props.actions.closeModal();
this.props.onCancelPress();
});
}
};
render() {
const {

View file

@ -8,15 +8,16 @@ import {
TouchableOpacity,
View
} from 'react-native';
import Font from 'react-native-vector-icons/FontAwesome';
import IconFont from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
export default class OptionsModalList extends PureComponent {
static propTypes = {
items: PropTypes.array.isRequired,
onCancelPress: PropTypes.func
}
};
renderOptions = () => {
const {items, onCancelPress} = this.props;
@ -37,12 +38,12 @@ export default class OptionsModalList extends PureComponent {
return (
<TouchableOpacity
key={index}
onPress={item.action}
onPress={() => preventDoubleTap(item.action, this)}
style={[style.option, style.optionBorder]}
>
{textComponent}
{item.icon &&
<Font
<IconFont
name={item.icon}
size={18}
style={style.optionIcon}
@ -55,7 +56,7 @@ export default class OptionsModalList extends PureComponent {
const cancel = (
<TouchableOpacity
key={items.length}
onPress={onCancelPress}
onPress={() => preventDoubleTap(onCancelPress, this)}
style={style.option}
>
<Text style={style.optionText}>{'Cancel'}</Text>
@ -66,7 +67,7 @@ export default class OptionsModalList extends PureComponent {
...options,
cancel
];
}
};
render() {
return (

View file

@ -8,9 +8,10 @@ import {
TouchableOpacity,
View
} from 'react-native';
import Font from 'react-native-vector-icons/FontAwesome';
import IconFont from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
export default class OptionsModalList extends PureComponent {
static propTypes = {
@ -20,7 +21,7 @@ export default class OptionsModalList extends PureComponent {
PropTypes.string,
PropTypes.object
])
}
};
renderOptions = () => {
const {items} = this.props;
@ -41,12 +42,12 @@ export default class OptionsModalList extends PureComponent {
return (
<TouchableOpacity
key={index}
onPress={item.action}
onPress={() => preventDoubleTap(item.action, this)}
style={[style.option, (index < items.length - 1 && style.optionBorder)]}
>
{textComponent}
{item.icon &&
<Font
<IconFont
name={item.icon}
size={18}
style={style.optionIcon}
@ -84,7 +85,7 @@ export default class OptionsModalList extends PureComponent {
title,
...options
];
}
};
render() {
const {onCancelPress} = this.props;
@ -96,7 +97,7 @@ export default class OptionsModalList extends PureComponent {
</View>
<View style={style.optionContainer}>
<TouchableOpacity
onPress={onCancelPress}
onPress={() => preventDoubleTap(onCancelPress, this)}
style={style.option}
>
<Text style={style.optionCancelText}>{'Cancel'}</Text>

View file

@ -3,6 +3,7 @@
import React, {PropTypes, PureComponent} from 'react';
import {
ActivityIndicator,
Image,
Keyboard,
KeyboardAvoidingView,
@ -112,8 +113,27 @@ export default class SelectServer extends PureComponent {
configRequest.status === RequestStatus.STARTED ||
licenseRequest.status === RequestStatus.STARTED;
let proceed;
if (isLoading) {
return null;
proceed = (
<ActivityIndicator
animating={true}
size='small'
/>
);
} else {
proceed = (
<Button
onPress={this.onClick}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
style={GlobalStyles.signupButtonText}
id='mobile.components.select_server_view.proceed'
defaultMessage='Proceed'
/>
</Button>
);
}
const error = pingRequest.error || configRequest.error || licenseRequest.error;
@ -150,16 +170,7 @@ export default class SelectServer extends PureComponent {
returnKeyType='go'
underlineColorAndroid='transparent'
/>
<Button
onPress={this.onClick}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
style={GlobalStyles.signupButtonText}
id='mobile.components.select_server_view.proceed'
defaultMessage='Proceed'
/>
</Button>
{proceed}
<ErrorText error={this.state.error || error}/>
</View>
</TouchableWithoutFeedback>

View file

@ -49,6 +49,10 @@ export default class SelectTeam extends PureComponent {
}
};
state = {
disableButtons: false
};
componentWillMount() {
this.props.subscribeToHeaderEvent('close', this.props.actions.goBack);
}
@ -58,15 +62,21 @@ export default class SelectTeam extends PureComponent {
}
onSelectTeam = async (team) => {
const {
closeDrawers,
closeModal,
handleTeamChange
} = this.props.actions;
if (!this.state.disableButtons) {
this.setState({disableButtons: true});
const {
closeDrawers,
closeModal,
handleTeamChange
} = this.props.actions;
handleTeamChange(team);
closeDrawers();
InteractionManager.runAfterInteractions(closeModal);
handleTeamChange(team);
closeDrawers();
InteractionManager.runAfterInteractions(closeModal);
setTimeout(() => {
this.setState({disableButtons: false});
}, 300);
}
};
render() {
@ -77,6 +87,7 @@ export default class SelectTeam extends PureComponent {
onPress={() => this.onSelectTeam(team)}
style={GlobalStyles.buttonListItemText}
containerStyle={GlobalStyles.buttonListItem}
disabled={this.state.disableButtons}
>
{team.display_name}
<Icon

View file

@ -10,15 +10,6 @@ import PostList from 'app/components/post_list';
import PostTextbox from 'app/components/post_textbox';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const getStyle = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});
export default class Thread extends PureComponent {
static propTypes = {
actions: React.PropTypes.shape({
@ -85,3 +76,12 @@ export default class Thread extends PureComponent {
);
}
}
const getStyle = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});

14
app/utils/tap.js Normal file
View file

@ -0,0 +1,14 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
let canPress = true;
export function preventDoubleTap(action, thisArg, ...args) {
if (canPress) {
canPress = false;
Reflect.apply(action, thisArg || null, [...args]);
setTimeout(() => {
canPress = true;
}, 300);
}
}

View file

@ -1648,7 +1648,7 @@
"mobile.account_notifications.threads_mentions": "Mentions in threads",
"mobile.account_notifications.threads_start": "Threads that I start",
"mobile.account_notifications.threads_start_participate": "Threads that I start or participate in",
"mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to delete the {term} with {name}?",
"mobile.channel_info.alertMessageDeleteChannel": "Are you sure you want to delete the {term} {name}?",
"mobile.channel_info.alertMessageLeaveChannel": "Are you sure you want to leave the {term} {name}?",
"mobile.channel_info.alertNo": "No",
"mobile.channel_info.alertTitleDeleteChannel": "Delete {term}",