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 UnreadBadge from './UnreadBadge'; import TypeIcon from './TypeIcon'; import LastMessage from './LastMessage'; import { CustomIcon } from '../../lib/Icons'; 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 })) export default class RoomItem extends React.Component { static propTypes = { type: PropTypes.string.isRequired, name: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired, showLastMessage: PropTypes.bool, _updatedAt: PropTypes.string, lastMessage: PropTypes.object, alert: PropTypes.bool, unread: PropTypes.number, userMentions: PropTypes.number, id: PropTypes.string, prid: PropTypes.string, onPress: PropTypes.func, userId: PropTypes.string, username: PropTypes.string, token: PropTypes.string, avatarSize: PropTypes.number, testID: PropTypes.string, width: PropTypes.number, height: PropTypes.number, favorite: PropTypes.bool, isRead: PropTypes.bool, rid: PropTypes.string, toggleFav: PropTypes.func, toggleRead: PropTypes.func, hideChannel: PropTypes.func } static defaultProps = { avatarSize: 48 } // Making jest happy: https://github.com/facebook/react-native/issues/22175 // 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.state = { dragX, rowOffSet, rowState: 0 // 0: closed, 1: right opened, -1: left opened }; this._onGestureEvent = Animated.event( [{ nativeEvent: { translationX: dragX } }] ); this._value = 0; this.rowTranslation.addListener(({ value }) => { this._value = value; }); } shouldComponentUpdate(nextProps) { const { lastMessage, _updatedAt, isRead } = 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) { 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); } }; _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 < halfScreen) { toValue = OPTION_WIDTH; // open left option if he swipe right but not enough to trigger action this.setState({ rowState: -1 }); } else if (translationX >= halfScreen) { toValue = 0; this.toggleRead(); } else if (translationX < 0 && translationX > -halfScreen) { toValue = -2 * OPTION_WIDTH; // open right option if he swipe left this.setState({ rowState: 1 }); } else if (translationX <= -halfScreen) { toValue = -width; 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 > halfScreen) { toValue = 0; this.setState({ rowState: 0 }); this.toggleRead(); } else { toValue = OPTION_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 < -halfScreen) { toValue = 0; this.setState({ rowState: 0 }); this.hideChannel(); } else { toValue = -2 * OPTION_WIDTH; } } this._animateRow(toValue); } _animateRow = (toValue) => { const { dragX, rowOffSet } = this.state; rowOffSet.setValue(this._value); dragX.setValue(0); Animated.spring(rowOffSet, { toValue, bounciness: 0 }).start(); } handleLeftButtonPress = () => { this.toggleRead(); this.close(); } 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); } } handleHideButtonPress = () => { this.hideChannel(); this.close(); } hideChannel = () => { const { hideChannel, rid, type } = this.props; if (hideChannel) { hideChannel(rid, type); } } onPress = () => { const { rowState } = this.state; if (rowState !== 0) { this.close(); return; } const { onPress } = this.props; if (onPress) { onPress(); } } 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', lastWeek: 'dddd', sameElse: 'MMM D' }) render() { const { unread, userMentions, name, _updatedAt, alert, testID, height, type, avatarSize, baseUrl, userId, username, token, id, prid, showLastMessage, lastMessage } = this.props; const date = this.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 ( {this.renderLeftActions()} {this.renderRightActions()} { name } {_updatedAt ? { date } : null} ); } }