From b444c425d2092b9299d386888ca7d862afde4aca Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Thu, 4 Jul 2019 13:15:30 -0300
Subject: [PATCH] [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
---
 app/presentation/RoomItem/index.js  | 100 ++++++++++++++--------------
 app/presentation/RoomItem/styles.js |  12 ++--
 app/views/RoomsListView/index.js    |  36 ++++++++--
 storybook/stories/RoomItem.js       |   4 +-
 4 files changed, 91 insertions(+), 61 deletions(-)

diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js
index b93c9f1d5..b9e2639b9 100644
--- a/app/presentation/RoomItem/index.js
+++ b/app/presentation/RoomItem/index.js
@@ -1,9 +1,7 @@
 import React from 'react';
 import moment from 'moment';
 import PropTypes from 'prop-types';
-import {
-	View, Text, Dimensions, Animated
-} from 'react-native';
+import { View, Text, Animated } from 'react-native';
 import { connect } from 'react-redux';
 import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler';
 
@@ -18,9 +16,8 @@ import { CustomIcon } from '../../lib/Icons';
 export { ROW_HEIGHT };
 
 const OPTION_WIDTH = 80;
-const SMALL_SWIPE = 80;
-const ACTION_OFFSET = 220;
-const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
+const SMALL_SWIPE = 40;
+const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type', 'width'];
 @connect(state => ({
 	userId: state.login.user && state.login.user.id,
 	username: state.login.user && state.login.user.username,
@@ -45,6 +42,7 @@ export default class RoomItem extends React.Component {
 		token: PropTypes.string,
 		avatarSize: PropTypes.number,
 		testID: PropTypes.string,
+		width: PropTypes.number,
 		height: PropTypes.number,
 		favorite: PropTypes.bool,
 		isRead: PropTypes.bool,
@@ -71,9 +69,7 @@ export default class RoomItem extends React.Component {
 		this.state = {
 			dragX,
 			rowOffSet,
-			rowState: 0, // 0: closed, 1: right opened, -1: left opened
-			leftWidth: undefined,
-			rightOffset: undefined
+			rowState: 0 // 0: closed, 1: right opened, -1: left opened
 		};
 		this._onGestureEvent = Animated.event(
 			[{ nativeEvent: { translationX: dragX } }]
@@ -104,43 +100,30 @@ export default class RoomItem extends React.Component {
 		this.rowTranslation.removeAllListeners();
 	}
 
-	close = () => {
-		this.swipeableRow.close();
-	};
-
 	_onHandlerStateChange = ({ nativeEvent }) => {
 		if (nativeEvent.oldState === State.ACTIVE) {
 			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) => {
 		const { translationX } = nativeEvent;
 		const { rowState } = this.state;
+		const { width } = this.props;
+		const halfScreen = width / 2;
 		let toValue = 0;
 		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
 				this.setState({ rowState: -1 });
-			} else if (translationX > ACTION_OFFSET) {
+			} else if (translationX >= halfScreen) {
 				toValue = 0;
 				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
 				this.setState({ rowState: 1 });
-			} else if (translationX < -ACTION_OFFSET) {
-				toValue = 0;
+			} else if (translationX <= -halfScreen) {
+				toValue = -width;
 				this.hideChannel();
 			} else {
 				toValue = 0;
@@ -151,7 +134,7 @@ export default class RoomItem extends React.Component {
 			if (this._value < SMALL_SWIPE) {
 				toValue = 0;
 				this.setState({ rowState: 0 });
-			} else if (this._value > ACTION_OFFSET) {
+			} else if (this._value > halfScreen) {
 				toValue = 0;
 				this.setState({ rowState: 0 });
 				this.toggleRead();
@@ -164,7 +147,7 @@ export default class RoomItem extends React.Component {
 			if (this._value > -2 * SMALL_SWIPE) {
 				toValue = 0;
 				this.setState({ rowState: 0 });
-			} else if (this._value < -ACTION_OFFSET) {
+			} else if (this._value < -halfScreen) {
 				toValue = 0;
 				this.setState({ rowState: 0 });
 				this.hideChannel();
@@ -181,7 +164,7 @@ export default class RoomItem extends React.Component {
 		dragX.setValue(0);
 		Animated.spring(rowOffSet, {
 			toValue,
-			bounciness: 5
+			bounciness: 0
 		}).start();
 	}
 
@@ -191,6 +174,7 @@ export default class RoomItem extends React.Component {
 	}
 
 	close = () => {
+		this.setState({ rowState: 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 = () => {
-		const { isRead } = this.props;
-		const { width } = Dimensions.get('window');
+		const { isRead, width } = this.props;
+		const halfWidth = width / 2;
 		const trans = this.rowTranslation.interpolate({
 			inputRange: [0, OPTION_WIDTH],
 			outputRange: [-width, -width + OPTION_WIDTH]
 		});
 
 		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]
 		});
 		return (
@@ -246,12 +242,12 @@ export default class RoomItem extends React.Component {
 					>
 						{isRead ? (
 							<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>
 							</View>
 						) : (
 							<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>
 							</View>
 						)}
@@ -262,23 +258,24 @@ export default class RoomItem extends React.Component {
 	};
 
 	renderRightActions = () => {
-		const { favorite } = this.props;
-		const { width } = Dimensions.get('window');
+		const { favorite, width } = this.props;
+		const halfWidth = width / 2;
 		const trans = this.rowTranslation.interpolate({
 			inputRange: [-OPTION_WIDTH, 0],
 			outputRange: [width - OPTION_WIDTH, width]
 		});
 		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]
 		});
 		const iconFavWidth = this.rowTranslation.interpolate({
-			inputRange: [-(ACTION_OFFSET + 1), -ACTION_OFFSET, -(ACTION_OFFSET - 20), -2 * OPTION_WIDTH, 0],
-			outputRange: [0, 0, OPTION_WIDTH + 20, OPTION_WIDTH, OPTION_WIDTH]
+			inputRange: [-halfWidth, -2 * OPTION_WIDTH, 0],
+			outputRange: [0, OPTION_WIDTH, OPTION_WIDTH],
+			extrapolate: 'clamp'
 		});
 		const iconHideWidth = this.rowTranslation.interpolate({
-			inputRange: [-(ACTION_OFFSET + 1), -ACTION_OFFSET, -(ACTION_OFFSET - 20), -2 * OPTION_WIDTH, 0],
-			outputRange: [(ACTION_OFFSET + 1), ACTION_OFFSET, OPTION_WIDTH + 20, OPTION_WIDTH, OPTION_WIDTH]
+			inputRange: [-width, -halfWidth, -2 * OPTION_WIDTH, 0],
+			outputRange: [width, halfWidth, OPTION_WIDTH, OPTION_WIDTH]
 		});
 		return (
 			<Animated.View
@@ -293,12 +290,12 @@ export default class RoomItem extends React.Component {
 					<RectButton style={[styles.actionButtonRightFav]} onPress={this.toggleFav}>
 						{favorite ? (
 							<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>
 							</View>
 						) : (
 							<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>
 							</View>
 						)}
@@ -314,7 +311,7 @@ export default class RoomItem extends React.Component {
 						onPress={this.handleHideButtonPress}
 					>
 						<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>
 						</View>
 					</RectButton>
@@ -332,7 +329,7 @@ export default class RoomItem extends React.Component {
 
 	render() {
 		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;
 
 		const date = this.formatDate(_updatedAt);
@@ -358,7 +355,7 @@ export default class RoomItem extends React.Component {
 				onGestureEvent={this._onGestureEvent}
 				onHandlerStateChange={this._onHandlerStateChange}
 			>
-				<Animated.View style={styles.upperContainer}>
+				<View style={styles.upperContainer}>
 					{this.renderLeftActions()}
 					{this.renderRightActions()}
 					<Animated.View
@@ -369,10 +366,11 @@ export default class RoomItem extends React.Component {
 						}
 					>
 						<RectButton
-							onPress={onPress}
+							onPress={this.onPress}
 							activeOpacity={0.8}
 							underlayColor='#e1e5e8'
 							testID={testID}
+							style={styles.button}
 						>
 							<View
 								style={[styles.container, height && { height }]}
@@ -393,7 +391,7 @@ export default class RoomItem extends React.Component {
 							</View>
 						</RectButton>
 					</Animated.View>
-				</Animated.View>
+				</View>
 			</PanGestureHandler>
 		);
 	}
diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js
index fba962e7e..a39a1037f 100644
--- a/app/presentation/RoomItem/styles.js
+++ b/app/presentation/RoomItem/styles.js
@@ -9,12 +9,14 @@ export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
 
 export default StyleSheet.create({
 	container: {
-		backgroundColor: COLOR_WHITE,
 		flexDirection: 'row',
 		alignItems: 'center',
 		paddingLeft: 14,
 		height: ROW_HEIGHT
 	},
+	button: {
+		backgroundColor: COLOR_WHITE
+	},
 	centerContainer: {
 		flex: 1,
 		paddingVertical: 10,
@@ -96,10 +98,12 @@ export default StyleSheet.create({
 		marginRight: 10
 	},
 	actionText: {
-		color: 'white',
-		fontSize: 14,
+		color: COLOR_WHITE,
+		fontSize: 15,
 		backgroundColor: 'transparent',
-		justifyContent: 'center'
+		justifyContent: 'center',
+		marginTop: 4,
+		...sharedStyles.textSemibold
 	},
 	actionButtonLeft: {
 		flex: 1,
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index f8901afce..fbb9c169b 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import {
-	View, FlatList, BackHandler, ActivityIndicator, Text, ScrollView, Keyboard, LayoutAnimation, InteractionManager
+	View, FlatList, BackHandler, ActivityIndicator, Text, ScrollView, Keyboard, LayoutAnimation, InteractionManager, Dimensions
 } from 'react-native';
 import { connect } from 'react-redux';
 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 } mount`);
 
+		const { width } = Dimensions.get('window');
 		this.data = [];
 		this.state = {
 			searching: false,
@@ -133,7 +134,8 @@ export default class RoomsListView extends React.Component {
 			channels: [],
 			privateGroup: [],
 			direct: [],
-			livechat: []
+			livechat: [],
+			width
 		};
 		Orientation.unlockAllOrientations();
 		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,
 			cancelSearchingAndroid: this.cancelSearchingAndroid
 		});
+		Dimensions.addEventListener('change', this.onDimensionsChange);
 		console.timeEnd(`${ this.constructor.name } mount`);
 	}
 
@@ -172,7 +175,7 @@ export default class RoomsListView extends React.Component {
 			return true;
 		}
 
-		const { loading, searching } = this.state;
+		const { loading, searching, width } = this.state;
 		if (nextState.loading !== loading) {
 			return true;
 		}
@@ -180,6 +183,10 @@ export default class RoomsListView extends React.Component {
 			return true;
 		}
 
+		if (nextState.width !== width) {
+			return true;
+		}
+
 		const { search } = this.state;
 		if (!isEqual(nextState.search, search)) {
 			return true;
@@ -220,9 +227,12 @@ export default class RoomsListView extends React.Component {
 		if (this.willBlurListener && this.willBlurListener.remove) {
 			this.willBlurListener.remove();
 		}
+		Dimensions.removeEventListener('change', this.onDimensionsChange);
 		console.countReset(`${ this.constructor.name }.render calls`);
 	}
 
+	onDimensionsChange = ({ window: { width } }) => this.setState({ width })
+
 	// eslint-disable-next-line react/sort-comp
 	internalSetState = (...args) => {
 		const { navigation } = this.props;
@@ -393,7 +403,15 @@ export default class RoomsListView extends React.Component {
 
 	toggleRead = async(rid, isRead) => {
 		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) {
 			log('error_toggle_read', e);
 		}
@@ -401,7 +419,13 @@ export default class RoomsListView extends React.Component {
 
 	hideChannel = async(rid, type) => {
 		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) {
 			log('error_hide_channel', e);
 		}
@@ -435,6 +459,7 @@ export default class RoomsListView extends React.Component {
 	}
 
 	renderItem = ({ item }) => {
+		const { width } = this.state;
 		const {
 			userId, baseUrl, StoreLastMessage
 		} = this.props;
@@ -460,6 +485,7 @@ export default class RoomsListView extends React.Component {
 					showLastMessage={StoreLastMessage}
 					onPress={() => this._onPressItem(item)}
 					testID={`rooms-list-view-item-${ item.name }`}
+					width={width}
 					height={ROW_HEIGHT}
 					toggleFav={this.toggleFav}
 					toggleRead={this.toggleRead}
diff --git a/storybook/stories/RoomItem.js b/storybook/stories/RoomItem.js
index 48fddcb19..dd22e393e 100644
--- a/storybook/stories/RoomItem.js
+++ b/storybook/stories/RoomItem.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { ScrollView } from 'react-native';
+import { ScrollView, Dimensions } from 'react-native';
 // import moment from 'moment';
 
 import RoomItemComponent from '../../app/presentation/RoomItem';
@@ -7,6 +7,7 @@ import StoriesSeparator from './StoriesSeparator';
 
 const date = '2017-10-10T10:00:00Z';
 const baseUrl = 'https://open.rocket.chat';
+const { width } = Dimensions.get('window');
 
 const RoomItem = props => (
 	<RoomItemComponent
@@ -14,6 +15,7 @@ const RoomItem = props => (
 		name='rocket.cat'
 		_updatedAt={date}
 		baseUrl={baseUrl}
+		width={width}
 		{...props}
 	/>
 );