From 47676c2286c6e4dd9814cd72b19eddb96d1ddadc Mon Sep 17 00:00:00 2001
From: pranavpandey1998official
<44601530+pranavpandey1998official@users.noreply.github.com>
Date: Mon, 1 Jul 2019 19:50:38 +0530
Subject: [PATCH] [NEW] Room swipe actions: mark as read/unread, hide, fav
(#976)
* added unread and fav feature
* changed the layout
* fix jest
* done requested changes
* added requested changes
---
__mocks__/react-native-gesture-handler.js | 1 +
app/i18n/locales/en.js | 4 +
app/lib/rocketchat.js | 9 +
app/presentation/RoomItem/index.js | 328 ++++++++++++++++++++--
app/presentation/RoomItem/styles.js | 42 ++-
app/views/RoomsListView/index.js | 35 +++
6 files changed, 391 insertions(+), 28 deletions(-)
diff --git a/__mocks__/react-native-gesture-handler.js b/__mocks__/react-native-gesture-handler.js
index da7b586dd..2f9960f4a 100644
--- a/__mocks__/react-native-gesture-handler.js
+++ b/__mocks__/react-native-gesture-handler.js
@@ -2,3 +2,4 @@ export const RectButton = () => 'View';
export const State = () => 'View';
export const LongPressGestureHandler = () => 'View';
export const BorderlessButton = () => 'View';
+export const PanGestureHandler = () => 'View';
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index e00b2fd3b..e390793f9 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -162,6 +162,7 @@ export default {
Everyone_can_access_this_channel: 'Everyone can access this channel',
erasing_room: 'erasing room',
Error_uploading: 'Error uploading',
+ Favorite: 'Favorite',
Favorites: 'Favorites',
Files: 'Files',
File_description: 'File description',
@@ -175,6 +176,7 @@ export default {
Forgot_Password: 'Forgot Password',
Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type',
+ Hide: 'Hide',
Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel',
@@ -268,6 +270,7 @@ export default {
Reactions_are_disabled: 'Reactions are disabled',
Reactions_are_enabled: 'Reactions are enabled',
Reactions: 'Reactions',
+ Read: 'Read',
Read_Only_Channel: 'Read Only Channel',
Read_Only: 'Read Only',
Read_Receipt: 'Read Receipt',
@@ -352,6 +355,7 @@ export default {
unarchive: 'unarchive',
UNARCHIVE: 'UNARCHIVE',
Unblock_user: 'Unblock user',
+ Unfavorite: 'Unfavorite',
Unfollowed_thread: 'Unfollowed thread',
Unmute: 'Unmute',
unmuted: 'unmuted',
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 7efcc717a..364a43816 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -582,6 +582,12 @@ const RocketChat = {
// RC 0.64.0
return this.sdk.post('rooms.favorite', { roomId, favorite });
},
+ toggleRead(read, roomId) {
+ if (read) {
+ return this.sdk.post('subscriptions.unread', { roomId });
+ }
+ return this.sdk.post('subscriptions.read', { rid: roomId });
+ },
getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
// RC 0.42.0
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
@@ -640,6 +646,9 @@ const RocketChat = {
// RC 0.48.0
return this.sdk.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
},
+ hideRoom(roomId, t) {
+ return this.sdk.post(`${ this.roomTypeToApiType(t) }.close`, { roomId });
+ },
saveRoomSettings(rid, params) {
// RC 0.55.0
return this.sdk.methodCall('saveRoomSettings', rid, params);
diff --git a/app/presentation/RoomItem/index.js b/app/presentation/RoomItem/index.js
index 28a296dc7..b93c9f1d5 100644
--- a/app/presentation/RoomItem/index.js
+++ b/app/presentation/RoomItem/index.js
@@ -1,9 +1,11 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
-import { View, Text } from 'react-native';
+import {
+ View, Text, Dimensions, Animated
+} from 'react-native';
import { connect } from 'react-redux';
-import { RectButton } from 'react-native-gesture-handler';
+import { RectButton, PanGestureHandler, State } from 'react-native-gesture-handler';
import Avatar from '../../containers/Avatar';
import I18n from '../../i18n';
@@ -11,9 +13,13 @@ 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 = 80;
+const ACTION_OFFSET = 220;
const attrs = ['name', 'unread', 'userMentions', 'showLastMessage', 'alert', 'type'];
@connect(state => ({
userId: state.login.user && state.login.user.id,
@@ -39,7 +45,13 @@ export default class RoomItem extends React.Component {
token: PropTypes.string,
avatarSize: PropTypes.number,
testID: PropTypes.string,
- height: 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 = {
@@ -50,10 +62,28 @@ 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.state = {
+ dragX,
+ rowOffSet,
+ rowState: 0, // 0: closed, 1: right opened, -1: left opened
+ leftWidth: undefined,
+ rightOffset: undefined
+ };
+ this._onGestureEvent = Animated.event(
+ [{ nativeEvent: { translationX: dragX } }]
+ );
+ this._value = 0;
+ this.rowTranslation.addListener(({ value }) => { this._value = value; });
}
shouldComponentUpdate(nextProps) {
- const { lastMessage, _updatedAt } = this.props;
+ const { lastMessage, _updatedAt, isRead } = this.props;
const oldlastMessage = lastMessage;
const newLastmessage = nextProps.lastMessage;
@@ -63,10 +93,236 @@ export default class RoomItem extends React.Component {
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();
+ }
+
+ 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;
+ let toValue = 0;
+ if (rowState === 0) { // if no option is opened
+ if (translationX > 0 && translationX < ACTION_OFFSET) {
+ 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) {
+ toValue = 0;
+ this.toggleRead();
+ } else if (translationX < 0 && translationX > -ACTION_OFFSET) {
+ toValue = -2 * OPTION_WIDTH; // open right option if he swipe left
+ this.setState({ rowState: 1 });
+ } else if (translationX < -ACTION_OFFSET) {
+ toValue = 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 > ACTION_OFFSET) {
+ 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 < -ACTION_OFFSET) {
+ 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: 5
+ }).start();
+ }
+
+ handleLeftButtonPress = () => {
+ this.toggleRead();
+ this.close();
+ }
+
+ close = () => {
+ 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);
+ }
+ }
+
+ renderLeftActions = () => {
+ const { isRead } = this.props;
+ const { width } = Dimensions.get('window');
+ 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],
+ outputRange: [0, 0, -(OPTION_WIDTH + 10), 0, 0]
+ });
+ return (
+
+
+
+ {isRead ? (
+
+
+ {I18n.t('Unread')}
+
+ ) : (
+
+
+ {I18n.t('Read')}
+
+ )}
+
+
+
+ );
+ };
+
+ renderRightActions = () => {
+ const { favorite } = this.props;
+ const { width } = Dimensions.get('window');
+ 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],
+ 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]
+ });
+ 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]
+ });
+ 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',
@@ -97,30 +353,48 @@ export default class RoomItem extends React.Component {
}
return (
-
-
-
-
-
-
- { name }
- {_updatedAt ? { date } : null}
-
-
-
-
-
-
-
-
+
+ {this.renderLeftActions()}
+ {this.renderRightActions()}
+
+
+
+
+
+
+
+ { name }
+ {_updatedAt ? { date } : null}
+
+
+
+
+
+
+
+
+
+
+
);
}
}
diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js
index 87fb92c94..fba962e7e 100644
--- a/app/presentation/RoomItem/styles.js
+++ b/app/presentation/RoomItem/styles.js
@@ -9,9 +9,10 @@ export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
export default StyleSheet.create({
container: {
+ backgroundColor: COLOR_WHITE,
flexDirection: 'row',
alignItems: 'center',
- marginLeft: 14,
+ paddingLeft: 14,
height: ROW_HEIGHT
},
centerContainer: {
@@ -93,5 +94,44 @@ export default StyleSheet.create({
},
avatar: {
marginRight: 10
+ },
+ actionText: {
+ color: 'white',
+ fontSize: 14,
+ backgroundColor: 'transparent',
+ justifyContent: 'center'
+ },
+ actionButtonLeft: {
+ flex: 1,
+ backgroundColor: '#497AFC',
+ justifyContent: 'center',
+ alignItems: 'flex-end'
+ },
+ actionButtonRightFav: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'flex-start',
+ backgroundColor: '#F4BD3E'
+ },
+ 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'
}
});
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index ee5aa0fcf..f8901afce 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -383,6 +383,30 @@ export default class RoomsListView extends React.Component {
}, 100);
}
+ toggleFav = async(rid, favorite) => {
+ try {
+ await RocketChat.toggleFavorite(rid, !favorite);
+ } catch (e) {
+ log('error_toggle_favorite', e);
+ }
+ }
+
+ toggleRead = async(rid, isRead) => {
+ try {
+ await RocketChat.toggleRead(isRead, rid);
+ } catch (e) {
+ log('error_toggle_read', e);
+ }
+ }
+
+ hideChannel = async(rid, type) => {
+ try {
+ await RocketChat.hideRoom(rid, type);
+ } catch (e) {
+ log('error_hide_channel', e);
+ }
+ }
+
goDirectory = () => {
const { navigation } = this.props;
navigation.navigate('DirectoryView');
@@ -404,6 +428,12 @@ export default class RoomsListView extends React.Component {
);
}
+ getIsRead = (item) => {
+ let isUnread = (item.archived !== true && item.open === true); // item is not archived and not opened
+ isUnread = isUnread && (item.unread > 0 || item.alert === true); // either its unread count > 0 or its alert
+ return !isUnread;
+ }
+
renderItem = ({ item }) => {
const {
userId, baseUrl, StoreLastMessage
@@ -416,12 +446,14 @@ export default class RoomsListView extends React.Component {
alert={item.alert}
unread={item.unread}
userMentions={item.userMentions}
+ isRead={this.getIsRead(item)}
favorite={item.f}
lastMessage={item.lastMessage ? JSON.parse(JSON.stringify(item.lastMessage)) : null}
name={this.getRoomTitle(item)}
_updatedAt={item.roomUpdatedAt}
key={item._id}
id={id}
+ rid={item.rid}
type={item.t}
baseUrl={baseUrl}
prid={item.prid}
@@ -429,6 +461,9 @@ export default class RoomsListView extends React.Component {
onPress={() => this._onPressItem(item)}
testID={`rooms-list-view-item-${ item.name }`}
height={ROW_HEIGHT}
+ toggleFav={this.toggleFav}
+ toggleRead={this.toggleRead}
+ hideChannel={this.hideChannel}
/>
);
}