[CHORE] Refactor RoomItem touchable (#1331)

This commit is contained in:
Prateek Jain 2019-10-29 19:23:58 +05:30 committed by Diego Mello
parent d61fc62e6d
commit d15a5612db
2 changed files with 349 additions and 305 deletions

View File

@ -0,0 +1,216 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Animated } from 'react-native';
import {
RectButton,
PanGestureHandler,
State
} from 'react-native-gesture-handler';
import styles, {
ACTION_WIDTH,
SMALL_SWIPE,
LONG_SWIPE
} from './styles';
import { LeftActions, RightActions } from './Actions';
class Touchable extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
onPress: PropTypes.func,
testID: PropTypes.string,
width: PropTypes.number,
favorite: PropTypes.bool,
isRead: PropTypes.bool,
rid: PropTypes.string,
toggleFav: PropTypes.func,
toggleRead: PropTypes.func,
hideChannel: PropTypes.func,
children: PropTypes.element
}
constructor(props) {
super(props);
this.dragX = new Animated.Value(0);
this.rowOffSet = new Animated.Value(0);
this.transX = Animated.add(
this.rowOffSet,
this.dragX
);
this.state = {
rowState: 0 // 0: closed, 1: right opened, -1: left opened
};
this._onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: this.dragX } }]
);
this._value = 0;
}
_onHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.oldState === State.ACTIVE) {
this._handleRelease(nativeEvent);
}
}
_handleRelease = (nativeEvent) => {
const { translationX } = nativeEvent;
const { rowState } = this.state;
this._value = this._value + translationX;
let toValue = 0;
if (rowState === 0) { // if no option is opened
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 >= LONG_SWIPE) {
toValue = 0;
this.toggleRead();
} 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 <= -LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
this.hideChannel();
} else {
toValue = 0;
}
}
if (rowState === -1) { // if left option is opened
if (this._value < SMALL_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
} else if (this._value > LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
this.toggleRead();
} else {
toValue = ACTION_WIDTH;
}
}
if (rowState === 1) { // if right option is opened
if (this._value > -2 * SMALL_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
} else if (this._value < -LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
this.hideChannel();
} else {
toValue = -2 * ACTION_WIDTH;
}
}
this._animateRow(toValue);
}
_animateRow = (toValue) => {
this.rowOffSet.setValue(this._value);
this._value = toValue;
this.dragX.setValue(0);
Animated.spring(this.rowOffSet, {
toValue,
bounciness: 0,
useNativeDriver: true
}).start();
}
close = () => {
this.setState({ rowState: 0 });
this._animateRow(0);
}
toggleFav = () => {
const { toggleFav, rid, favorite } = this.props;
if (toggleFav) {
toggleFav(rid, favorite);
}
this.close();
};
toggleRead = () => {
const { toggleRead, rid, isRead } = this.props;
if (toggleRead) {
toggleRead(rid, isRead);
}
};
hideChannel = () => {
const { hideChannel, rid, type } = this.props;
if (hideChannel) {
hideChannel(rid, type);
}
};
onToggleReadPress = () => {
this.toggleRead();
this.close();
};
onHidePress = () => {
this.hideChannel();
this.close();
};
onPress = () => {
const { rowState } = this.state;
if (rowState !== 0) {
this.close();
return;
}
const { onPress } = this.props;
if (onPress) {
onPress();
}
};
render() {
const {
testID, isRead, width, favorite, children
} = this.props;
return (
<PanGestureHandler
minDeltaX={20}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}
>
<Animated.View>
<LeftActions
transX={this.transX}
isRead={isRead}
width={width}
onToggleReadPress={this.onToggleReadPress}
/>
<RightActions
transX={this.transX}
favorite={favorite}
width={width}
toggleFav={this.toggleFav}
onHidePress={this.onHidePress}
/>
<Animated.View
style={{
transform: [{ translateX: this.transX }]
}}
>
<RectButton
onPress={this.onPress}
activeOpacity={0.8}
underlayColor='#e1e5e8'
testID={testID}
style={styles.button}
>
{children}
</RectButton>
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
}
}
export default Touchable;

View File

@ -1,26 +1,16 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View, Text, Animated } from 'react-native'; import { View, Text } from 'react-native';
import {
RectButton,
PanGestureHandler,
State
} from 'react-native-gesture-handler';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles, { import styles, { ROW_HEIGHT } from './styles';
ROW_HEIGHT,
ACTION_WIDTH,
SMALL_SWIPE,
LONG_SWIPE
} from './styles';
import UnreadBadge from './UnreadBadge'; import UnreadBadge from './UnreadBadge';
import TypeIcon from './TypeIcon'; import TypeIcon from './TypeIcon';
import LastMessage from './LastMessage'; import LastMessage from './LastMessage';
import { capitalize, formatDate } from '../../utils/room'; import { capitalize, formatDate } from '../../utils/room';
import { LeftActions, RightActions } from './Actions'; import Touchable from './Touchable';
export { ROW_HEIGHT }; export { ROW_HEIGHT };
@ -37,307 +27,145 @@ const attrs = [
'status' 'status'
]; ];
class RoomItem extends React.Component { const arePropsEqual = (oldProps, newProps) => {
static propTypes = { const { _updatedAt: _updatedAtOld } = oldProps;
type: PropTypes.string.isRequired, const { _updatedAt: _updatedAtNew } = newProps;
name: PropTypes.string.isRequired, if (_updatedAtOld && _updatedAtNew && _updatedAtOld.toISOString() !== _updatedAtNew.toISOString()) {
baseUrl: PropTypes.string.isRequired, return false;
showLastMessage: PropTypes.bool, }
_updatedAt: PropTypes.string, return attrs.every(key => oldProps[key] === newProps[key]);
lastMessage: PropTypes.object, };
alert: PropTypes.bool,
unread: PropTypes.number, const RoomItem = React.memo(({
userMentions: PropTypes.number, onPress, width, favorite, toggleFav, isRead, rid, toggleRead, hideChannel, testID, unread, userMentions, name, _updatedAt, alert, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, hideUnreadStatus, lastMessage, status, avatar
id: PropTypes.string, }) => {
prid: PropTypes.string, const date = formatDate(_updatedAt);
onPress: PropTypes.func,
userId: PropTypes.string, let accessibilityLabel = name;
username: PropTypes.string, if (unread === 1) {
token: PropTypes.string, accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
avatarSize: PropTypes.number, } else if (unread > 1) {
testID: PropTypes.string, accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
width: PropTypes.number,
favorite: PropTypes.bool,
isRead: PropTypes.bool,
rid: PropTypes.string,
status: PropTypes.string,
toggleFav: PropTypes.func,
toggleRead: PropTypes.func,
hideChannel: PropTypes.func,
avatar: PropTypes.bool,
hideUnreadStatus: PropTypes.bool
} }
static defaultProps = { if (userMentions > 0) {
avatarSize: 48 accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
};
constructor(props) {
super(props);
this.dragX = new Animated.Value(0);
this.rowOffSet = new Animated.Value(0);
this.transX = Animated.add(
this.rowOffSet,
this.dragX
);
this.state = {
rowState: 0 // 0: closed, 1: right opened, -1: left opened
};
this._onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: this.dragX } }]
);
this._value = 0;
} }
shouldComponentUpdate(nextProps) { if (date) {
const { _updatedAt } = this.props; accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
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]);
} }
_onHandlerStateChange = ({ nativeEvent }) => { return (
if (nativeEvent.oldState === State.ACTIVE) { <Touchable
this._handleRelease(nativeEvent); onPress={onPress}
} width={width}
}; favorite={favorite}
toggleFav={toggleFav}
_handleRelease = (nativeEvent) => { isRead={isRead}
const { translationX } = nativeEvent; rid={rid}
const { rowState } = this.state; toggleRead={toggleRead}
this._value = this._value + translationX; hideChannel={hideChannel}
testID={testID}
let toValue = 0; type={type}
if (rowState === 0) { // if no option is opened >
if (translationX > 0 && translationX < LONG_SWIPE) { <View
toValue = ACTION_WIDTH; // open left option if he swipe right but not enough to trigger action style={styles.container}
this.setState({ rowState: -1 }); accessibilityLabel={accessibilityLabel}
} else if (translationX >= LONG_SWIPE) {
toValue = 0;
this.toggleRead();
} 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 <= -LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
this.hideChannel();
} else {
toValue = 0;
}
}
if (rowState === -1) { // if left option is opened
if (this._value < SMALL_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
} else if (this._value > LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
this.toggleRead();
} else {
toValue = ACTION_WIDTH;
}
}
if (rowState === 1) { // if right option is opened
if (this._value > -2 * SMALL_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
} else if (this._value < -LONG_SWIPE) {
toValue = 0;
this.setState({ rowState: 0 });
this.hideChannel();
} else {
toValue = -2 * ACTION_WIDTH;
}
}
this._animateRow(toValue);
}
_animateRow = (toValue) => {
this.rowOffSet.setValue(this._value);
this._value = toValue;
this.dragX.setValue(0);
Animated.spring(this.rowOffSet, {
toValue,
bounciness: 0,
useNativeDriver: true
}).start();
}
close = () => {
this.setState({ rowState: 0 });
this._animateRow(0);
}
toggleFav = () => {
const { toggleFav, rid, favorite } = this.props;
if (toggleFav) {
toggleFav(rid, favorite);
}
this.close();
};
toggleRead = () => {
const { toggleRead, rid, isRead } = this.props;
if (toggleRead) {
toggleRead(rid, isRead);
}
};
hideChannel = () => {
const { hideChannel, rid, type } = this.props;
if (hideChannel) {
hideChannel(rid, type);
}
};
onToggleReadPress = () => {
this.toggleRead();
this.close();
};
onHidePress = () => {
this.hideChannel();
this.close();
};
onPress = () => {
const { rowState } = this.state;
if (rowState !== 0) {
this.close();
return;
}
const { onPress } = this.props;
if (onPress) {
onPress();
}
};
render() {
const {
unread, userMentions, name, _updatedAt, alert, testID, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage, isRead, width, favorite, status, avatar, hideUnreadStatus
} = this.props;
const date = formatDate(_updatedAt);
let accessibilityLabel = name;
if (unread === 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alert') }`;
} else if (unread > 1) {
accessibilityLabel += `, ${ unread } ${ I18n.t('alerts') }`;
}
if (userMentions > 0) {
accessibilityLabel += `, ${ I18n.t('you_were_mentioned') }`;
}
if (date) {
accessibilityLabel += `, ${ I18n.t('last_message') } ${ date }`;
}
return (
<PanGestureHandler
minDeltaX={20}
onGestureEvent={this._onGestureEvent}
onHandlerStateChange={this._onHandlerStateChange}
> >
<Animated.View> <Avatar
<LeftActions text={avatar}
transX={this.transX} size={avatarSize}
isRead={isRead} type={type}
width={width} baseUrl={baseUrl}
onToggleReadPress={this.onToggleReadPress} style={styles.avatar}
/> userId={userId}
<RightActions token={token}
transX={this.transX} />
favorite={favorite} <View style={styles.centerContainer}>
width={width} <View style={styles.titleContainer}>
toggleFav={this.toggleFav} <TypeIcon
onHidePress={this.onHidePress} type={type}
/> id={id}
<Animated.View prid={prid}
style={{ status={status}
transform: [{ translateX: this.transX }] />
}} <Text
> style={[
<RectButton styles.title,
onPress={this.onPress} alert && !hideUnreadStatus && styles.alert
activeOpacity={0.8} ]}
underlayColor='#e1e5e8' ellipsizeMode='tail'
testID={testID} numberOfLines={1}
style={styles.button}
> >
<View {name}
style={styles.container} </Text>
accessibilityLabel={accessibilityLabel} {_updatedAt ? (
<Text
style={[
styles.date,
alert && !hideUnreadStatus && styles.updateAlert
]}
ellipsizeMode='tail'
numberOfLines={1}
> >
<Avatar {capitalize(date)}
text={avatar} </Text>
size={avatarSize} ) : null}
type={type} </View>
baseUrl={baseUrl} <View style={styles.row}>
style={styles.avatar} <LastMessage
userId={userId} lastMessage={lastMessage}
token={token} type={type}
/> showLastMessage={showLastMessage}
<View style={styles.centerContainer}> username={username}
<View style={styles.titleContainer}> alert={alert && !hideUnreadStatus}
<TypeIcon />
type={type} <UnreadBadge
id={id} unread={unread}
prid={prid} userMentions={userMentions}
status={status} type={type}
/> />
<Text </View>
style={[ </View>
styles.title, </View>
alert && !hideUnreadStatus && styles.alert </Touchable>
]} );
ellipsizeMode='tail' }, arePropsEqual);
numberOfLines={1}
> RoomItem.propTypes = {
{name} type: PropTypes.string.isRequired,
</Text> name: PropTypes.string.isRequired,
{_updatedAt ? ( baseUrl: PropTypes.string.isRequired,
<Text showLastMessage: PropTypes.bool,
style={[ _updatedAt: PropTypes.string,
styles.date, lastMessage: PropTypes.object,
alert && !hideUnreadStatus && styles.updateAlert alert: PropTypes.bool,
]} unread: PropTypes.number,
ellipsizeMode='tail' userMentions: PropTypes.number,
numberOfLines={1} id: PropTypes.string,
> prid: PropTypes.string,
{capitalize(date)} onPress: PropTypes.func,
</Text> userId: PropTypes.string,
) : null} username: PropTypes.string,
</View> token: PropTypes.string,
<View style={styles.row}> avatarSize: PropTypes.number,
<LastMessage testID: PropTypes.string,
lastMessage={lastMessage} width: PropTypes.number,
type={type} favorite: PropTypes.bool,
showLastMessage={showLastMessage} isRead: PropTypes.bool,
username={username} rid: PropTypes.string,
alert={alert && !hideUnreadStatus} status: PropTypes.string,
/> toggleFav: PropTypes.func,
<UnreadBadge toggleRead: PropTypes.func,
unread={unread} hideChannel: PropTypes.func,
userMentions={userMentions} avatar: PropTypes.bool,
type={type} hideUnreadStatus: PropTypes.bool
/> };
</View>
</View> RoomItem.defaultProps = {
</View> avatarSize: 48
</RectButton> };
</Animated.View>
</Animated.View>
</PanGestureHandler>
);
}
}
const mapStateToProps = (state, ownProps) => ({ const mapStateToProps = (state, ownProps) => ({
status: state.meteor.connected && ownProps.type === 'd' ? state.activeUsers[ownProps.id] : 'offline' status: state.meteor.connected && ownProps.type === 'd' ? state.activeUsers[ownProps.id] : 'offline'