[FIX] Rooms swipes (#1034)

* Regression: on press style feedback

* Action button styles

* Fix animations

* Styles changed

* Update subscription without having to wait for socket

* Calculate width on RoomsListView instead
This commit is contained in:
Diego Mello 2019-07-04 13:15:30 -03:00 committed by GitHub
parent d8c557c367
commit b444c425d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 61 deletions

View File

@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { View, Text, Animated } from 'react-native';
View, Text, Dimensions, Animated
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler'; import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler';
@ -18,9 +16,8 @@ import { CustomIcon } from '../../lib/Icons';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
const OPTION_WIDTH = 80; const OPTION_WIDTH = 80;
const SMALL_SWIPE = 80; const SMALL_SWIPE = 40;
const ACTION_OFFSET = 220; const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type', 'width'];
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
@connect(state => ({ @connect(state => ({
userId: state.login.user && state.login.user.id, userId: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username, username: state.login.user && state.login.user.username,
@ -45,6 +42,7 @@ export default class RoomItem extends React.Component {
token: PropTypes.string, token: PropTypes.string,
avatarSize: PropTypes.number, avatarSize: PropTypes.number,
testID: PropTypes.string, testID: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
favorite: PropTypes.bool, favorite: PropTypes.bool,
isRead: PropTypes.bool, isRead: PropTypes.bool,
@ -71,9 +69,7 @@ export default class RoomItem extends React.Component {
this.state = { this.state = {
dragX, dragX,
rowOffSet, rowOffSet,
rowState: 0, // 0: closed, 1: right opened, -1: left opened rowState: 0 // 0: closed, 1: right opened, -1: left opened
leftWidth: undefined,
rightOffset: undefined
}; };
this._onGestureEvent = Animated.event( this._onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: dragX } }] [{ nativeEvent: { translationX: dragX } }]
@ -104,43 +100,30 @@ export default class RoomItem extends React.Component {
this.rowTranslation.removeAllListeners(); this.rowTranslation.removeAllListeners();
} }
close = () => {
this.swipeableRow.close();
};
_onHandlerStateChange = ({ nativeEvent }) => { _onHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) { if (nativeEvent.oldState === State.ACTIVE) {
this._handleRelease(nativeEvent); this._handleRelease(nativeEvent);
} }
}; };
_currentOffset = () => {
const { leftWidth = 0, rowState } = this.state;
const { rightOffset } = this.state;
if (rowState === 1) {
return leftWidth;
} else if (rowState === -1) {
return rightOffset;
}
return 0;
};
_handleRelease = (nativeEvent) => { _handleRelease = (nativeEvent) => {
const { translationX } = nativeEvent; const { translationX } = nativeEvent;
const { rowState } = this.state; const { rowState } = this.state;
const { width } = this.props;
const halfScreen = width / 2;
let toValue = 0; let toValue = 0;
if (rowState === 0) { // if no option is opened if (rowState === 0) { // if no option is opened
if (translationX > 0 && translationX < ACTION_OFFSET) { if (translationX > 0 && translationX < halfScreen) {
toValue = OPTION_WIDTH; // open left option if he swipe right but not enough to trigger action toValue = OPTION_WIDTH; // open left option if he swipe right but not enough to trigger action
this.setState({ rowState: -1 }); this.setState({ rowState: -1 });
} else if (translationX > ACTION_OFFSET) { } else if (translationX >= halfScreen) {
toValue = 0; toValue = 0;
this.toggleRead(); this.toggleRead();
} else if (translationX < 0 && translationX > -ACTION_OFFSET) { } else if (translationX < 0 && translationX > -halfScreen) {
toValue = -2 * OPTION_WIDTH; // open right option if he swipe left toValue = -2 * OPTION_WIDTH; // open right option if he swipe left
this.setState({ rowState: 1 }); this.setState({ rowState: 1 });
} else if (translationX < -ACTION_OFFSET) { } else if (translationX <= -halfScreen) {
toValue = 0; toValue = -width;
this.hideChannel(); this.hideChannel();
} else { } else {
toValue = 0; toValue = 0;
@ -151,7 +134,7 @@ export default class RoomItem extends React.Component {
if (this._value < SMALL_SWIPE) { if (this._value < SMALL_SWIPE) {
toValue = 0; toValue = 0;
this.setState({ rowState: 0 }); this.setState({ rowState: 0 });
} else if (this._value > ACTION_OFFSET) { } else if (this._value > halfScreen) {
toValue = 0; toValue = 0;
this.setState({ rowState: 0 }); this.setState({ rowState: 0 });
this.toggleRead(); this.toggleRead();
@ -164,7 +147,7 @@ export default class RoomItem extends React.Component {
if (this._value > -2 * SMALL_SWIPE) { if (this._value > -2 * SMALL_SWIPE) {
toValue = 0; toValue = 0;
this.setState({ rowState: 0 }); this.setState({ rowState: 0 });
} else if (this._value < -ACTION_OFFSET) { } else if (this._value < -halfScreen) {
toValue = 0; toValue = 0;
this.setState({ rowState: 0 }); this.setState({ rowState: 0 });
this.hideChannel(); this.hideChannel();
@ -181,7 +164,7 @@ export default class RoomItem extends React.Component {
dragX.setValue(0); dragX.setValue(0);
Animated.spring(rowOffSet, { Animated.spring(rowOffSet, {
toValue, toValue,
bounciness: 5 bounciness: 0
}).start(); }).start();
} }
@ -191,6 +174,7 @@ export default class RoomItem extends React.Component {
} }
close = () => { close = () => {
this.setState({ rowState: 0 });
this._animateRow(0); this._animateRow(0);
} }
@ -221,16 +205,28 @@ export default class RoomItem extends React.Component {
} }
} }
onPress = () => {
const { rowState } = this.state;
if (rowState !== 0) {
this.close();
return;
}
const { onPress } = this.props;
if (onPress) {
onPress();
}
}
renderLeftActions = () => { renderLeftActions = () => {
const { isRead } = this.props; const { isRead, width } = this.props;
const { width } = Dimensions.get('window'); const halfWidth = width / 2;
const trans = this.rowTranslation.interpolate({ const trans = this.rowTranslation.interpolate({
inputRange: [0, OPTION_WIDTH], inputRange: [0, OPTION_WIDTH],
outputRange: [-width, -width + OPTION_WIDTH] outputRange: [-width, -width + OPTION_WIDTH]
}); });
const iconTrans = this.rowTranslation.interpolate({ const iconTrans = this.rowTranslation.interpolate({
inputRange: [0, OPTION_WIDTH, ACTION_OFFSET - 20, ACTION_OFFSET, width], inputRange: [0, OPTION_WIDTH, halfWidth - 1, halfWidth, width],
outputRange: [0, 0, -(OPTION_WIDTH + 10), 0, 0] outputRange: [0, 0, -(OPTION_WIDTH + 10), 0, 0]
}); });
return ( return (
@ -246,12 +242,12 @@ export default class RoomItem extends React.Component {
> >
{isRead ? ( {isRead ? (
<View style={styles.actionView}> <View style={styles.actionView}>
<CustomIcon size={15} name='flag' color='white' /> <CustomIcon size={20} name='flag' color='white' />
<Text style={styles.actionText}>{I18n.t('Unread')}</Text> <Text style={styles.actionText}>{I18n.t('Unread')}</Text>
</View> </View>
) : ( ) : (
<View style={styles.actionView}> <View style={styles.actionView}>
<CustomIcon size={15} name='check' color='white' /> <CustomIcon size={20} name='check' color='white' />
<Text style={styles.actionText}>{I18n.t('Read')}</Text> <Text style={styles.actionText}>{I18n.t('Read')}</Text>
</View> </View>
)} )}
@ -262,23 +258,24 @@ export default class RoomItem extends React.Component {
}; };
renderRightActions = () => { renderRightActions = () => {
const { favorite } = this.props; const { favorite, width } = this.props;
const { width } = Dimensions.get('window'); const halfWidth = width / 2;
const trans = this.rowTranslation.interpolate({ const trans = this.rowTranslation.interpolate({
inputRange: [-OPTION_WIDTH, 0], inputRange: [-OPTION_WIDTH, 0],
outputRange: [width - OPTION_WIDTH, width] outputRange: [width - OPTION_WIDTH, width]
}); });
const iconHideTrans = this.rowTranslation.interpolate({ const iconHideTrans = this.rowTranslation.interpolate({
inputRange: [-(ACTION_OFFSET - 20), -2 * OPTION_WIDTH, 0], inputRange: [-(halfWidth - 20), -2 * OPTION_WIDTH, 0],
outputRange: [0, 0, -OPTION_WIDTH] outputRange: [0, 0, -OPTION_WIDTH]
}); });
const iconFavWidth = this.rowTranslation.interpolate({ const iconFavWidth = this.rowTranslation.interpolate({
inputRange: [-(ACTION_OFFSET + 1), -ACTION_OFFSET, -(ACTION_OFFSET - 20), -2 * OPTION_WIDTH, 0], inputRange: [-halfWidth, -2 * OPTION_WIDTH, 0],
outputRange: [0, 0, OPTION_WIDTH + 20, OPTION_WIDTH, OPTION_WIDTH] outputRange: [0, OPTION_WIDTH, OPTION_WIDTH],
extrapolate: 'clamp'
}); });
const iconHideWidth = this.rowTranslation.interpolate({ const iconHideWidth = this.rowTranslation.interpolate({
inputRange: [-(ACTION_OFFSET + 1), -ACTION_OFFSET, -(ACTION_OFFSET - 20), -2 * OPTION_WIDTH, 0], inputRange: [-width, -halfWidth, -2 * OPTION_WIDTH, 0],
outputRange: [(ACTION_OFFSET + 1), ACTION_OFFSET, OPTION_WIDTH + 20, OPTION_WIDTH, OPTION_WIDTH] outputRange: [width, halfWidth, OPTION_WIDTH, OPTION_WIDTH]
}); });
return ( return (
<Animated.View <Animated.View
@ -293,12 +290,12 @@ export default class RoomItem extends React.Component {
<RectButton style={[styles.actionButtonRightFav]} onPress={this.toggleFav}> <RectButton style={[styles.actionButtonRightFav]} onPress={this.toggleFav}>
{favorite ? ( {favorite ? (
<View style={styles.actionView}> <View style={styles.actionView}>
<CustomIcon size={17} name='Star-filled' color='white' /> <CustomIcon size={20} name='Star-filled' color='white' />
<Text style={styles.actionText}>{I18n.t('Unfavorite')}</Text> <Text style={styles.actionText}>{I18n.t('Unfavorite')}</Text>
</View> </View>
) : ( ) : (
<View style={styles.actionView}> <View style={styles.actionView}>
<CustomIcon size={17} name='star' color='white' /> <CustomIcon size={20} name='star' color='white' />
<Text style={styles.actionText}>{I18n.t('Favorite')}</Text> <Text style={styles.actionText}>{I18n.t('Favorite')}</Text>
</View> </View>
)} )}
@ -314,7 +311,7 @@ export default class RoomItem extends React.Component {
onPress={this.handleHideButtonPress} onPress={this.handleHideButtonPress}
> >
<View style={styles.actionView}> <View style={styles.actionView}>
<CustomIcon size={15} name='eye-off' color='white' /> <CustomIcon size={20} name='eye-off' color='white' />
<Text style={styles.actionText}>{I18n.t('Hide')}</Text> <Text style={styles.actionText}>{I18n.t('Hide')}</Text>
</View> </View>
</RectButton> </RectButton>
@ -332,7 +329,7 @@ export default class RoomItem extends React.Component {
render() { render() {
const { const {
unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, onPress, id, prid, showLastMessage, lastMessage unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage
} = this.props; } = this.props;
const date = this.formatDate(_updatedAt); const date = this.formatDate(_updatedAt);
@ -358,7 +355,7 @@ export default class RoomItem extends React.Component {
onGestureEvent={this._onGestureEvent} onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange} onHandlerStateChange={this._onHandlerStateChange}
> >
<Animated.View style={styles.upperContainer}> <View style={styles.upperContainer}>
{this.renderLeftActions()} {this.renderLeftActions()}
{this.renderRightActions()} {this.renderRightActions()}
<Animated.View <Animated.View
@ -369,10 +366,11 @@ export default class RoomItem extends React.Component {
} }
> >
<RectButton <RectButton
onPress={onPress} onPress={this.onPress}
activeOpacity={0.8} activeOpacity={0.8}
underlayColor='#e1e5e8' underlayColor='#e1e5e8'
testID={testID} testID={testID}
style={styles.button}
> >
<View <View
style={[styles.container, height && { height }]} style={[styles.container, height && { height }]}
@ -393,7 +391,7 @@ export default class RoomItem extends React.Component {
</View> </View>
</RectButton> </RectButton>
</Animated.View> </Animated.View>
</Animated.View> </View>
</PanGestureHandler> </PanGestureHandler>
); );
} }

View File

@ -9,12 +9,14 @@ export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
backgroundColor: COLOR_WHITE,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
paddingLeft: 14, paddingLeft: 14,
height: ROW_HEIGHT height: ROW_HEIGHT
}, },
button: {
backgroundColor: COLOR_WHITE
},
centerContainer: { centerContainer: {
flex: 1, flex: 1,
paddingVertical: 10, paddingVertical: 10,
@ -96,10 +98,12 @@ export default StyleSheet.create({
marginRight: 10 marginRight: 10
}, },
actionText: { actionText: {
color: 'white', color: COLOR_WHITE,
fontSize: 14, fontSize: 15,
backgroundColor: 'transparent', backgroundColor: 'transparent',
justifyContent: 'center' justifyContent: 'center',
marginTop: 4,
...sharedStyles.textSemibold
}, },
actionButtonLeft: { actionButtonLeft: {
flex: 1, flex: 1,

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, FlatList, BackHandler, ActivityIndicator, Text, ScrollView, Keyboard, LayoutAnimation, InteractionManager View, FlatList, BackHandler, ActivityIndicator, Text, ScrollView, Keyboard, LayoutAnimation, InteractionManager, Dimensions
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -121,6 +121,7 @@ export default class RoomsListView extends React.Component {
console.time(`${ this.constructor.name } init`); console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`); console.time(`${ this.constructor.name } mount`);
const { width } = Dimensions.get('window');
this.data = []; this.data = [];
this.state = { this.state = {
searching: false, searching: false,
@ -133,7 +134,8 @@ export default class RoomsListView extends React.Component {
channels: [], channels: [],
privateGroup: [], privateGroup: [],
direct: [], direct: [],
livechat: [] livechat: [],
width
}; };
Orientation.unlockAllOrientations(); Orientation.unlockAllOrientations();
this.didFocusListener = props.navigation.addListener('didFocus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress)); this.didFocusListener = props.navigation.addListener('didFocus', () => BackHandler.addEventListener('hardwareBackPress', this.handleBackPress));
@ -148,6 +150,7 @@ export default class RoomsListView extends React.Component {
initSearchingAndroid: this.initSearchingAndroid, initSearchingAndroid: this.initSearchingAndroid,
cancelSearchingAndroid: this.cancelSearchingAndroid cancelSearchingAndroid: this.cancelSearchingAndroid
}); });
Dimensions.addEventListener('change', this.onDimensionsChange);
console.timeEnd(`${ this.constructor.name } mount`); console.timeEnd(`${ this.constructor.name } mount`);
} }
@ -172,7 +175,7 @@ export default class RoomsListView extends React.Component {
return true; return true;
} }
const { loading, searching } = this.state; const { loading, searching, width } = this.state;
if (nextState.loading !== loading) { if (nextState.loading !== loading) {
return true; return true;
} }
@ -180,6 +183,10 @@ export default class RoomsListView extends React.Component {
return true; return true;
} }
if (nextState.width !== width) {
return true;
}
const { search } = this.state; const { search } = this.state;
if (!isEqual(nextState.search, search)) { if (!isEqual(nextState.search, search)) {
return true; return true;
@ -220,9 +227,12 @@ export default class RoomsListView extends React.Component {
if (this.willBlurListener && this.willBlurListener.remove) { if (this.willBlurListener && this.willBlurListener.remove) {
this.willBlurListener.remove(); this.willBlurListener.remove();
} }
Dimensions.removeEventListener('change', this.onDimensionsChange);
console.countReset(`${ this.constructor.name }.render calls`); console.countReset(`${ this.constructor.name }.render calls`);
} }
onDimensionsChange = ({ window: { width } }) => this.setState({ width })
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
internalSetState = (...args) => { internalSetState = (...args) => {
const { navigation } = this.props; const { navigation } = this.props;
@ -393,7 +403,15 @@ export default class RoomsListView extends React.Component {
toggleRead = async(rid, isRead) => { toggleRead = async(rid, isRead) => {
try { try {
await RocketChat.toggleRead(isRead, rid); const result = await RocketChat.toggleRead(isRead, rid);
if (result.success) {
database.write(() => {
const sub = database.objects('subscriptions').filtered('rid == $0', rid)[0];
if (sub) {
sub.alert = isRead;
}
});
}
} catch (e) { } catch (e) {
log('error_toggle_read', e); log('error_toggle_read', e);
} }
@ -401,7 +419,13 @@ export default class RoomsListView extends React.Component {
hideChannel = async(rid, type) => { hideChannel = async(rid, type) => {
try { try {
await RocketChat.hideRoom(rid, type); const result = await RocketChat.hideRoom(rid, type);
if (result.success) {
database.write(() => {
const sub = database.objects('subscriptions').filtered('rid == $0', rid)[0];
database.delete(sub);
});
}
} catch (e) { } catch (e) {
log('error_hide_channel', e); log('error_hide_channel', e);
} }
@ -435,6 +459,7 @@ export default class RoomsListView extends React.Component {
} }
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { width } = this.state;
const { const {
userId, baseUrl, StoreLastMessage userId, baseUrl, StoreLastMessage
} = this.props; } = this.props;
@ -460,6 +485,7 @@ export default class RoomsListView extends React.Component {
showLastMessage={StoreLastMessage} showLastMessage={StoreLastMessage}
onPress={() => this._onPressItem(item)} onPress={() => this._onPressItem(item)}
testID={`rooms-list-view-item-${ item.name }`} testID={`rooms-list-view-item-${ item.name }`}
width={width}
height={ROW_HEIGHT} height={ROW_HEIGHT}
toggleFav={this.toggleFav} toggleFav={this.toggleFav}
toggleRead={this.toggleRead} toggleRead={this.toggleRead}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView } from 'react-native'; import { ScrollView, Dimensions } from 'react-native';
// import moment from 'moment'; // import moment from 'moment';
import RoomItemComponent from '../../app/presentation/RoomItem'; import RoomItemComponent from '../../app/presentation/RoomItem';
@ -7,6 +7,7 @@ import StoriesSeparator from './StoriesSeparator';
const date = '2017-10-10T10:00:00Z'; const date = '2017-10-10T10:00:00Z';
const baseUrl = 'https://open.rocket.chat'; const baseUrl = 'https://open.rocket.chat';
const { width } = Dimensions.get('window');
const RoomItem = props => ( const RoomItem = props => (
<RoomItemComponent <RoomItemComponent
@ -14,6 +15,7 @@ const RoomItem = props => (
name='rocket.cat' name='rocket.cat'
_updatedAt={date} _updatedAt={date}
baseUrl={baseUrl} baseUrl={baseUrl}
width={width}
{...props} {...props}
/> />
); );