From c2497145fc63672147e63af73b995610ced96650 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 15 Jul 2019 13:54:28 -0300 Subject: [PATCH] [FIX] Swipe animations (#1044) * Comment removeClippedSubviews * Comment width animation * Remove redux from RoomItem * Fix wrong re-render comparison * Remove listener * Raise minDeltaX * memo actions * Spring with native driver * Refactor functions * Fix props issues * Remove RoomItem.height * Long swipe * Refactor animations * this.rowTranslation -> this.transX * Moved state to this * Fix favorite button --- app/presentation/RoomItem/Actions.js | 129 +++++++++++++++ app/presentation/RoomItem/index.js | 232 ++++++++------------------- app/presentation/RoomItem/styles.js | 55 ++++--- app/views/RoomsListView/index.js | 22 ++- 4 files changed, 237 insertions(+), 201 deletions(-) create mode 100644 app/presentation/RoomItem/Actions.js diff --git a/app/presentation/RoomItem/Actions.js b/app/presentation/RoomItem/Actions.js new file mode 100644 index 000000000..8793dc791 --- /dev/null +++ b/app/presentation/RoomItem/Actions.js @@ -0,0 +1,129 @@ +import React from 'react'; +import { Animated, View, Text } from 'react-native'; +import { RectButton } from 'react-native-gesture-handler'; +import PropTypes from 'prop-types'; + +import I18n from '../../i18n'; +import styles, { ACTION_WIDTH, LONG_SWIPE } from './styles'; +import { CustomIcon } from '../../lib/Icons'; + +export const LeftActions = React.memo(({ + transX, isRead, width, onToggleReadPress +}) => { + const translateX = transX.interpolate({ + inputRange: [0, ACTION_WIDTH], + outputRange: [-ACTION_WIDTH, 0] + }); + const translateXIcon = transX.interpolate({ + inputRange: [0, ACTION_WIDTH, LONG_SWIPE - 2, LONG_SWIPE], + outputRange: [0, 0, -LONG_SWIPE + ACTION_WIDTH + 2, 0], + extrapolate: 'clamp' + }); + return ( + + + + + + + {I18n.t(isRead ? 'Unread' : 'Read')} + + + + + + ); +}); + +export const RightActions = React.memo(({ + transX, favorite, width, toggleFav, onHidePress +}) => { + const translateXFav = transX.interpolate({ + inputRange: [-width / 2, -ACTION_WIDTH * 2, 0], + outputRange: [width / 2, width - ACTION_WIDTH * 2, width] + }); + const translateXHide = transX.interpolate({ + inputRange: [-width, -LONG_SWIPE, -ACTION_WIDTH * 2, 0], + outputRange: [0, width - LONG_SWIPE, width - ACTION_WIDTH, width] + }); + return ( + + + + + + {I18n.t(favorite ? 'Unfavorite' : 'Favorite')} + + + + + + + + {I18n.t('Hide')} + + + + + ); +}); + +LeftActions.propTypes = { + transX: PropTypes.object, + isRead: PropTypes.bool, + width: PropTypes.number, + onToggleReadPress: PropTypes.func +}; + +RightActions.propTypes = { + transX: PropTypes.object, + favorite: PropTypes.bool, + width: PropTypes.number, + toggleFav: PropTypes.func, + onHidePress: PropTypes.func +}; diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js index b9e2639b9..1b5105f62 100644 --- a/app/presentation/RoomItem/index.js +++ b/app/presentation/RoomItem/index.js @@ -2,27 +2,22 @@ import React from 'react'; import moment from 'moment'; import PropTypes from 'prop-types'; import { View, Text, Animated } from 'react-native'; -import { connect } from 'react-redux'; import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler'; import Avatar from '../../containers/Avatar'; import I18n from '../../i18n'; -import styles, { ROW_HEIGHT } from './styles'; +import styles, { + ROW_HEIGHT, ACTION_WIDTH, SMALL_SWIPE, LONG_SWIPE +} from './styles'; import UnreadBadge from './UnreadBadge'; import TypeIcon from './TypeIcon'; import LastMessage from './LastMessage'; -import { CustomIcon } from '../../lib/Icons'; +import { LeftActions, RightActions } from './Actions'; export { ROW_HEIGHT }; -const OPTION_WIDTH = 80; -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, - token: state.login.user && state.login.user.token -})) +const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type', 'width', 'isRead', 'favorite']; + export default class RoomItem extends React.Component { static propTypes = { type: PropTypes.string.isRequired, @@ -43,7 +38,6 @@ export default class RoomItem extends React.Component { avatarSize: PropTypes.number, testID: PropTypes.string, width: PropTypes.number, - height: PropTypes.number, favorite: PropTypes.bool, isRead: PropTypes.bool, rid: PropTypes.string, @@ -60,46 +54,36 @@ export default class RoomItem extends React.Component { // eslint-disable-next-line no-useless-constructor constructor(props) { super(props); - const dragX = new Animated.Value(0); - const rowOffSet = new Animated.Value(0); - this.rowTranslation = Animated.add( - rowOffSet, - dragX + this.dragX = new Animated.Value(0); + this.rowOffSet = new Animated.Value(0); + this.transX = Animated.add( + this.rowOffSet, + this.dragX ); this.state = { - dragX, - rowOffSet, rowState: 0 // 0: closed, 1: right opened, -1: left opened }; this._onGestureEvent = Animated.event( - [{ nativeEvent: { translationX: dragX } }] + [{ nativeEvent: { translationX: this.dragX } }] ); this._value = 0; - this.rowTranslation.addListener(({ value }) => { this._value = value; }); } shouldComponentUpdate(nextProps) { - const { lastMessage, _updatedAt, isRead } = this.props; + const { lastMessage, _updatedAt } = this.props; const oldlastMessage = lastMessage; const newLastmessage = nextProps.lastMessage; if (oldlastMessage && newLastmessage && oldlastMessage.ts !== newLastmessage.ts) { return true; } - if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt !== _updatedAt) { - return true; - } - if (isRead !== nextProps.isRead) { + if (_updatedAt && nextProps._updatedAt && nextProps._updatedAt.toISOString() !== _updatedAt.toISOString()) { return true; } // eslint-disable-next-line react/destructuring-assignment return attrs.some(key => nextProps[key] !== this.props[key]); } - componentWillUnmount() { - this.rowTranslation.removeAllListeners(); - } - _onHandlerStateChange = ({ nativeEvent }) => { if (nativeEvent.oldState === State.ACTIVE) { this._handleRelease(nativeEvent); @@ -109,21 +93,22 @@ export default class RoomItem extends React.Component { _handleRelease = (nativeEvent) => { const { translationX } = nativeEvent; const { rowState } = this.state; - const { width } = this.props; - const halfScreen = width / 2; + this._value = this._value + translationX; + let toValue = 0; if (rowState === 0) { // if no option is opened - if (translationX > 0 && translationX < halfScreen) { - toValue = OPTION_WIDTH; // open left option if he swipe right but not enough to trigger action + if (translationX > 0 && translationX < LONG_SWIPE) { + toValue = ACTION_WIDTH; // open left option if he swipe right but not enough to trigger action this.setState({ rowState: -1 }); - } else if (translationX >= halfScreen) { + } else if (translationX >= LONG_SWIPE) { toValue = 0; this.toggleRead(); - } else if (translationX < 0 && translationX > -halfScreen) { - toValue = -2 * OPTION_WIDTH; // open right option if he swipe left + } else if (translationX < 0 && translationX > -LONG_SWIPE) { + toValue = -2 * ACTION_WIDTH; // open right option if he swipe left this.setState({ rowState: 1 }); - } else if (translationX <= -halfScreen) { - toValue = -width; + } else if (translationX <= -LONG_SWIPE) { + toValue = 0; + this.setState({ rowState: 0 }); this.hideChannel(); } else { toValue = 0; @@ -134,12 +119,12 @@ export default class RoomItem extends React.Component { if (this._value < SMALL_SWIPE) { toValue = 0; this.setState({ rowState: 0 }); - } else if (this._value > halfScreen) { + } else if (this._value > LONG_SWIPE) { toValue = 0; this.setState({ rowState: 0 }); this.toggleRead(); } else { - toValue = OPTION_WIDTH; + toValue = ACTION_WIDTH; } } @@ -147,32 +132,28 @@ export default class RoomItem extends React.Component { if (this._value > -2 * SMALL_SWIPE) { toValue = 0; this.setState({ rowState: 0 }); - } else if (this._value < -halfScreen) { + } else if (this._value < -LONG_SWIPE) { toValue = 0; this.setState({ rowState: 0 }); this.hideChannel(); } else { - toValue = -2 * OPTION_WIDTH; + toValue = -2 * ACTION_WIDTH; } } this._animateRow(toValue); } _animateRow = (toValue) => { - const { dragX, rowOffSet } = this.state; - rowOffSet.setValue(this._value); - dragX.setValue(0); - Animated.spring(rowOffSet, { + this.rowOffSet.setValue(this._value); + this._value = toValue; + this.dragX.setValue(0); + Animated.spring(this.rowOffSet, { toValue, - bounciness: 0 + bounciness: 0, + useNativeDriver: true }).start(); } - handleLeftButtonPress = () => { - this.toggleRead(); - this.close(); - } - close = () => { this.setState({ rowState: 0 }); this._animateRow(0); @@ -193,11 +174,6 @@ export default class RoomItem extends React.Component { } } - handleHideButtonPress = () => { - this.hideChannel(); - this.close(); - } - hideChannel = () => { const { hideChannel, rid, type } = this.props; if (hideChannel) { @@ -205,6 +181,16 @@ export default class RoomItem extends React.Component { } } + onToggleReadPress = () => { + this.toggleRead(); + this.close(); + } + + onHidePress = () => { + this.hideChannel(); + this.close(); + } + onPress = () => { const { rowState } = this.state; if (rowState !== 0) { @@ -217,109 +203,6 @@ export default class RoomItem extends React.Component { } } - renderLeftActions = () => { - 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, halfWidth - 1, halfWidth, width], - outputRange: [0, 0, -(OPTION_WIDTH + 10), 0, 0] - }); - return ( - - - - {isRead ? ( - - - {I18n.t('Unread')} - - ) : ( - - - {I18n.t('Read')} - - )} - - - - ); - }; - - renderRightActions = () => { - 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: [-(halfWidth - 20), -2 * OPTION_WIDTH, 0], - outputRange: [0, 0, -OPTION_WIDTH] - }); - const iconFavWidth = this.rowTranslation.interpolate({ - inputRange: [-halfWidth, -2 * OPTION_WIDTH, 0], - outputRange: [0, OPTION_WIDTH, OPTION_WIDTH], - extrapolate: 'clamp' - }); - const iconHideWidth = this.rowTranslation.interpolate({ - inputRange: [-width, -halfWidth, -2 * OPTION_WIDTH, 0], - outputRange: [width, halfWidth, OPTION_WIDTH, OPTION_WIDTH] - }); - return ( - - - - {favorite ? ( - - - {I18n.t('Unfavorite')} - - ) : ( - - - {I18n.t('Favorite')} - - )} - - - - - - - {I18n.t('Hide')} - - - - - ); - } - formatDate = date => moment(date).calendar(null, { lastDay: `[${ I18n.t('Yesterday') }]`, sameDay: 'h:mm A', @@ -329,7 +212,7 @@ export default class RoomItem extends React.Component { render() { const { - unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage + unread, userMentions, name, _updatedAt, alert, testID, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage, isRead, width, favorite } = this.props; const date = this.formatDate(_updatedAt); @@ -351,17 +234,28 @@ export default class RoomItem extends React.Component { return ( - - {this.renderLeftActions()} - {this.renderRightActions()} + + + @@ -373,7 +267,7 @@ export default class RoomItem extends React.Component { style={styles.button} > @@ -391,7 +285,7 @@ export default class RoomItem extends React.Component { - + ); } diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js index a39a1037f..6ad29fd6d 100644 --- a/app/presentation/RoomItem/styles.js +++ b/app/presentation/RoomItem/styles.js @@ -6,6 +6,9 @@ import { } from '../../constants/colors'; export const ROW_HEIGHT = 75 * PixelRatio.getFontScale(); +export const ACTION_WIDTH = 80; +export const SMALL_SWIPE = ACTION_WIDTH / 2; +export const LONG_SWIPE = ACTION_WIDTH * 3; export default StyleSheet.create({ container: { @@ -97,6 +100,15 @@ export default StyleSheet.create({ avatar: { marginRight: 10 }, + upperContainer: { + overflow: 'hidden' + }, + actionsContainer: { + position: 'absolute', + left: 0, + right: 0, + height: ROW_HEIGHT + }, actionText: { color: COLOR_WHITE, fontSize: 15, @@ -105,37 +117,24 @@ export default StyleSheet.create({ marginTop: 4, ...sharedStyles.textSemibold }, - actionButtonLeft: { - flex: 1, - backgroundColor: '#497AFC', + actionLeftButtonContainer: { + position: 'absolute', + height: ROW_HEIGHT, + backgroundColor: COLOR_PRIMARY, justifyContent: 'center', - alignItems: 'flex-end' + top: 0 }, - actionButtonRightFav: { - flex: 1, + actionRightButtonContainer: { + position: 'absolute', + height: ROW_HEIGHT, justifyContent: 'center', - alignItems: 'flex-start', - backgroundColor: '#F4BD3E' + top: 0, + backgroundColor: '#54585e' }, - actionButtonRightHide: { - flex: 1, - justifyContent: 'center', - alignItems: 'flex-start', - backgroundColor: '#55585D' - }, - actionView: { - width: 80, - alignItems: 'center' - }, - leftAction: { - ...StyleSheet.absoluteFill, - flexDirection: 'row-reverse' - }, - rightAction: { - ...StyleSheet.absoluteFill, - flexDirection: 'row' - }, - upperContainer: { - overflow: 'hidden' + actionButton: { + width: ACTION_WIDTH, + height: '100%', + alignItems: 'center', + justifyContent: 'center' } }); diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index fbb9c169b..c2dc0a8a8 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -39,6 +39,8 @@ const keyExtractor = item => item.rid; @connect(state => ({ userId: state.login.user && state.login.user.id, + username: state.login.user && state.login.user.username, + token: state.login.user && state.login.user.token, isAuthenticated: state.login.isAuthenticated, server: state.server.server, baseUrl: state.settings.baseUrl || state.server ? state.server.server : '', @@ -95,6 +97,8 @@ export default class RoomsListView extends React.Component { static propTypes = { navigation: PropTypes.object, userId: PropTypes.string, + username: PropTypes.string, + token: PropTypes.string, baseUrl: PropTypes.string, server: PropTypes.string, searchText: PropTypes.string, @@ -395,7 +399,15 @@ export default class RoomsListView extends React.Component { toggleFav = async(rid, favorite) => { try { - await RocketChat.toggleFavorite(rid, !favorite); + const result = await RocketChat.toggleFavorite(rid, !favorite); + if (result.success) { + database.write(() => { + const sub = database.objects('subscriptions').filtered('rid == $0', rid)[0]; + if (sub) { + sub.f = !favorite; + } + }); + } } catch (e) { log('error_toggle_favorite', e); } @@ -461,7 +473,7 @@ export default class RoomsListView extends React.Component { renderItem = ({ item }) => { const { width } = this.state; const { - userId, baseUrl, StoreLastMessage + userId, username, token, baseUrl, StoreLastMessage } = this.props; const id = item.rid.replace(userId, '').trim(); @@ -478,6 +490,9 @@ export default class RoomsListView extends React.Component { _updatedAt={item.roomUpdatedAt} key={item._id} id={id} + userId={userId} + username={username} + token={token} rid={item.rid} type={item.t} baseUrl={baseUrl} @@ -486,7 +501,6 @@ export default class RoomsListView extends React.Component { onPress={() => this._onPressItem(item)} testID={`rooms-list-view-item-${ item.name }`} width={width} - height={ROW_HEIGHT} toggleFav={this.toggleFav} toggleRead={this.toggleRead} hideChannel={this.hideChannel} @@ -591,7 +605,7 @@ export default class RoomsListView extends React.Component { renderItem={this.renderItem} ListHeaderComponent={this.renderListHeader} getItemLayout={getItemLayout} - removeClippedSubviews + // removeClippedSubviews keyboardShouldPersistTaps='always' initialNumToRender={9} windowSize={9}