From da3679d46a9c5f0a3df9cd36817af55f7cd28526 Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Mon, 4 Jun 2018 22:17:02 -0300
Subject: [PATCH] [NEW] Drawer (#322)

---
 app/containers/Sidebar.js           | 320 ++++++++++++++++++++++------
 app/containers/routes/AuthRoutes.js |  40 +++-
 app/containers/status.js            |  13 +-
 app/i18n/locales/en.js              |   3 +
 app/views/ProfileView/index.js      |  18 ++
 app/views/SettingsView/index.js     |  18 ++
 e2e/05-roomslist.spec.js            |   5 +
 e2e/10-changeserver.spec.js         |   5 +
 e2e/11-broadcast.spec.js            |   3 +-
 e2e/helpers/app.js                  |   1 +
 storybook/stories/index.js          |   2 +-
 11 files changed, 357 insertions(+), 71 deletions(-)
 create mode 100644 app/views/ProfileView/index.js
 create mode 100644 app/views/SettingsView/index.js

diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js
index 655338416..e2acce4c7 100644
--- a/app/containers/Sidebar.js
+++ b/app/containers/Sidebar.js
@@ -1,45 +1,84 @@
 import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import { ScrollView, Text, View, StyleSheet, FlatList, TouchableHighlight } from 'react-native';
+import { ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation } from 'react-native';
 import { connect } from 'react-redux';
-import { DrawerActions } from 'react-navigation';
+import { DrawerActions, SafeAreaView } from 'react-navigation';
+import FastImage from 'react-native-fast-image';
+import Icon from 'react-native-vector-icons/MaterialIcons';
 
 import database from '../lib/realm';
 import { setServer } from '../actions/server';
 import { logout } from '../actions/login';
+import Avatar from '../containers/Avatar';
+import Status from '../containers/status';
+import Touch from '../utils/touch';
+import { STATUS_COLORS } from '../constants/colors';
+import RocketChat from '../lib/rocketchat';
+import log from '../utils/log';
 import I18n from '../i18n';
 
 const styles = StyleSheet.create({
-	scrollView: {
-		paddingTop: 20
+	selected: {
+		backgroundColor: 'rgba(0, 0, 0, .04)'
 	},
-	imageContainer: {
-		width: '100%',
+	item: {
+		flexDirection: 'row',
 		alignItems: 'center'
 	},
-	image: {
-		width: 200,
-		height: 200,
-		borderRadius: 100
+	itemLeft: {
+		marginHorizontal: 10,
+		width: 30,
+		alignItems: 'center'
+	},
+	itemLeftOpacity: {
+		opacity: 0.62
 	},
-	serverTitle: {
-		fontSize: 16,
-		color: 'grey',
-		padding: 10,
-		width: '100%'
+	itemText: {
+		marginVertical: 16,
+		fontWeight: 'bold',
+		color: '#292E35'
 	},
-	serverItem: {
-		backgroundColor: 'white',
-		padding: 10,
-		flex: 1
+	separator: {
+		borderBottomWidth: StyleSheet.hairlineWidth,
+		borderColor: '#ddd',
+		marginVertical: 4
 	},
-	selectedServer: {
-		backgroundColor: '#eeeeee'
+	serverImage: {
+		width: 24,
+		height: 24,
+		borderRadius: 4
+	},
+	header: {
+		paddingVertical: 16,
+		flexDirection: 'row',
+		alignItems: 'center'
+	},
+	headerTextContainer: {
+		flex: 1,
+		flexDirection: 'column',
+		alignItems: 'flex-start'
+	},
+	headerUsername: {
+		flexDirection: 'row',
+		alignItems: 'center'
+	},
+	avatar: {
+		marginHorizontal: 10
+	},
+	status: {
+		borderRadius: 12,
+		width: 12,
+		height: 12,
+		marginRight: 5
+	},
+	currentServerText: {
+		fontWeight: 'bold'
 	}
 });
 const keyExtractor = item => item.id;
 @connect(state => ({
-	server: state.server.server
+	server: state.server.server,
+	user: state.login.user
 }), dispatch => ({
 	selectServer: server => dispatch(setServer(server)),
 	logout: () => dispatch(logout())
@@ -54,7 +93,23 @@ export default class Sidebar extends Component {
 
 	constructor(props) {
 		super(props);
-		this.state = { servers: [] };
+		this.state = {
+			servers: [],
+			status: [{
+				id: 'online',
+				name: I18n.t('Online')
+			}, {
+				id: 'busy',
+				name: I18n.t('Busy')
+			}, {
+				id: 'away',
+				name: I18n.t('Away')
+			}, {
+				id: 'offline',
+				name: I18n.t('Invisible')
+			}],
+			showServers: false
+		};
 	}
 
 	componentDidMount() {
@@ -83,54 +138,195 @@ export default class Sidebar extends Component {
 		this.props.navigation.dispatch(DrawerActions.closeDrawer());
 	}
 
-	renderItem = ({ item, separators }) => (
+	toggleServers = () => {
+		LayoutAnimation.easeInEaseOut();
+		this.setState({ showServers: !this.state.showServers });
+	}
+
+	isRouteFocused = (route) => {
+		const { state } = this.props.navigation;
+		const activeItemKey = state.routes[state.index] ? state.routes[state.index].key : null;
+		return activeItemKey === route;
+	}
+
+	sidebarNavigate = (route) => {
+		const { navigate } = this.props.navigation;
+		if (!this.isRouteFocused(route)) {
+			navigate(route);
+		}
+	}
+
+	renderSeparator = key => <View key={key} style={styles.separator} />;
 
-		<TouchableHighlight
-			onShowUnderlay={separators.highlight}
-			onHideUnderlay={separators.unhighlight}
-			onPress={() => { this.onPressItem(item); }}
-			testID={`sidebar-${ item.id }`}
+	renderItem = ({
+		text, left, selected, onPress, testID
+	}) => (
+		<Touch
+			key={text}
+			onPress={onPress}
+			underlayColor='rgba(255, 255, 255, 0.5)'
+			activeOpacity={0.3}
+			testID={testID}
 		>
-			<View style={[styles.serverItem, (item.id === this.props.server ? styles.selectedServer : null)]}>
-				<Text>
-					{item.id}
+			<View style={[styles.item, selected && styles.selected]}>
+				<View style={[styles.itemLeft, !selected && styles.itemLeftOpacity]}>
+					{left}
+				</View>
+				<Text style={styles.itemText}>
+					{text}
 				</Text>
 			</View>
-		</TouchableHighlight>
-	);
+		</Touch>
+	)
+
+	renderStatusItem = ({ item }) => (
+		this.renderItem({
+			text: item.name,
+			left: <View style={[styles.status, { backgroundColor: STATUS_COLORS[item.id] }]} />,
+			selected: this.props.user.status === item.id,
+			onPress: () => {
+				this.closeDrawer();
+				this.toggleServers();
+				if (this.props.user.status !== item.id) {
+					try {
+						RocketChat.setUserPresenceDefaultStatus(item.id);
+					} catch (e) {
+						log('onPressModalButton', e);
+					}
+				}
+			}
+		})
+	)
+
+	renderServer = ({ item }) => (
+		this.renderItem({
+			text: item.id,
+			left: <FastImage
+				style={styles.serverImage}
+				source={{ uri: encodeURI(`${ item.id }/assets/favicon_32.png`) }}
+			/>,
+			selected: this.props.server === item.id,
+			onPress: () => {
+				this.closeDrawer();
+				this.toggleServers();
+				if (this.props.server !== item.id) {
+					this.props.selectServer(item.id);
+				}
+			},
+			testID: `sidebar-${ item.id }`
+		})
+	)
+
+	renderNavigation = () => (
+		[
+			this.renderItem({
+				text: I18n.t('Chats'),
+				left: <Icon name='chat-bubble' size={20} />,
+				onPress: () => this.sidebarNavigate('Chats'),
+				selected: this.isRouteFocused('Chats'),
+				testID: 'sidebar-chats'
+			}),
+			this.renderItem({
+				text: I18n.t('Profile'),
+				left: <Icon name='person' size={20} />,
+				onPress: () => this.sidebarNavigate('ProfileView'),
+				selected: this.isRouteFocused('ProfileView'),
+				testID: 'sidebar-profile'
+			}),
+			this.renderItem({
+				text: I18n.t('Settings'),
+				left: <Icon name='settings' size={20} />,
+				onPress: () => this.sidebarNavigate('SettingsView'),
+				selected: this.isRouteFocused('SettingsView'),
+				testID: 'sidebar-settings'
+			}),
+			this.renderSeparator('separator-logout'),
+			this.renderItem({
+				text: I18n.t('Logout'),
+				left: <Icon
+					name='exit-to-app'
+					size={20}
+				/>,
+				onPress: () => this.props.logout(),
+				testID: 'sidebar-logout'
+			})
+		]
+	)
+
+	renderServers = () => (
+		[
+			<FlatList
+				key='status-list'
+				data={this.state.status}
+				extraData={this.props.user}
+				renderItem={this.renderStatusItem}
+				keyExtractor={keyExtractor}
+			/>,
+			this.renderSeparator('separator-status'),
+			<FlatList
+				key='servers-list'
+				data={this.state.servers}
+				extraData={this.props.server}
+				renderItem={this.renderServer}
+				keyExtractor={keyExtractor}
+			/>,
+			this.renderSeparator('separator-add-server'),
+			this.renderItem({
+				text: I18n.t('Add_Server'),
+				left: <Icon
+					name='add'
+					size={20}
+				/>,
+				onPress: () => {
+					this.closeDrawer();
+					this.toggleServers();
+					this.props.navigation.navigate('AddServer');
+				},
+				testID: 'sidebar-add-server'
+			})
+		]
+	)
 
 	render() {
+		const { user, server } = this.props;
 		return (
-			<ScrollView style={styles.scrollView}>
-				<View style={{ paddingBottom: 20 }} testID='sidebar'>
-					<FlatList
-						data={this.state.servers}
-						renderItem={this.renderItem}
-						keyExtractor={keyExtractor}
-					/>
-					<TouchableHighlight
-						onPress={() => {
-							this.closeDrawer();
-							this.props.logout();
-						}}
-						testID='sidebar-logout'
-					>
-						<View style={styles.serverItem}>
-							<Text>{I18n.t('Logout')}</Text>
-						</View>
-					</TouchableHighlight>
-					<TouchableHighlight
-						onPress={() => {
-							this.closeDrawer();
-							this.props.navigation.navigate({ key: 'AddServer', routeName: 'AddServer' });
-						}}
-						testID='sidebar-add-server'
+			<ScrollView>
+				<SafeAreaView
+					style={styles.container}
+					forceInset={{ top: 'always', horizontal: 'never' }}
+					testID='sidebar'
+				>
+					<Touch
+						onPress={() => this.toggleServers()}
+						underlayColor='rgba(255, 255, 255, 0.5)'
+						activeOpacity={0.3}
+						testID='sidebar-toggle-server'
 					>
-						<View style={styles.serverItem}>
-							<Text>{I18n.t('Add_Server')}</Text>
+						<View style={styles.header}>
+							<Avatar
+								text={user.username}
+								size={30}
+								style={styles.avatar}
+							/>
+							<View style={styles.headerTextContainer}>
+								<View style={styles.headerUsername}>
+									<Status style={styles.status} id={user.id} />
+									<Text>{user.username}</Text>
+								</View>
+								<Text style={styles.currentServerText}>{server}</Text>
+							</View>
+							<Icon
+								name={this.state.showServers ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
+								size={30}
+							/>
 						</View>
-					</TouchableHighlight>
-				</View>
+					</Touch>
+
+					{this.renderSeparator('separator-header')}
+
+					{!this.state.showServers && this.renderNavigation()}
+					{this.state.showServers && this.renderServers()}
+				</SafeAreaView>
 			</ScrollView>
 		);
 	}
diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js
index cd46d1b48..3ded765b3 100644
--- a/app/containers/routes/AuthRoutes.js
+++ b/app/containers/routes/AuthRoutes.js
@@ -1,5 +1,7 @@
+import React from 'react';
 import { Platform } from 'react-native';
 import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
+import Icon from 'react-native-vector-icons/MaterialIcons';
 
 import Sidebar from '../../containers/Sidebar';
 import RoomsListView from '../../views/RoomsListView';
@@ -17,6 +19,8 @@ import RoomFilesView from '../../views/RoomFilesView';
 import RoomMembersView from '../../views/RoomMembersView';
 import RoomInfoView from '../../views/RoomInfoView';
 import RoomInfoEditView from '../../views/RoomInfoEditView';
+import ProfileView from '../../views/ProfileView';
+import SettingsView from '../../views/SettingsView';
 import I18n from '../../i18n';
 
 const headerTintColor = '#292E35';
@@ -130,15 +134,45 @@ const AuthRoutes = createStackNavigator(
 
 const Routes = createDrawerNavigator(
 	{
-		Home: {
-			screen: AuthRoutes
+		Chats: {
+			screen: AuthRoutes,
+			navigationOptions: {
+				drawerLabel: 'Chats',
+				drawerIcon: () => <Icon name='chat-bubble' size={20} />
+			}
+		},
+		ProfileView: {
+			screen: createStackNavigator({
+				ProfileView: {
+					screen: ProfileView,
+					navigationOptions: ({ navigation }) => ({
+						title: 'Profile',
+						headerTintColor: '#292E35',
+						headerLeft: <Icon name='menu' size={30} color='#292E35' onPress={() => navigation.toggleDrawer()} /> // TODO: refactor
+					})
+				}
+			})
+		},
+		SettingsView: {
+			screen: createStackNavigator({
+				SettingsView: {
+					screen: SettingsView,
+					navigationOptions: ({ navigation }) => ({
+						title: 'Settings',
+						headerTintColor: '#292E35',
+						headerLeft: <Icon name='menu' size={30} color='#292E35' onPress={() => navigation.toggleDrawer()} /> // TODO: refactor
+					})
+				}
+			})
 		}
 	},
 	{
 		contentComponent: Sidebar,
 		navigationOptions: {
 			drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked'
-		}
+		},
+		initialRouteName: 'Chats',
+		backBehavior: 'initialRoute'
 	}
 );
 
diff --git a/app/containers/status.js b/app/containers/status.js
index 8a434d0cf..a2797ea5a 100644
--- a/app/containers/status.js
+++ b/app/containers/status.js
@@ -13,7 +13,8 @@ const styles = StyleSheet.create({
 });
 
 @connect(state => ({
-	activeUsers: state.activeUsers
+	activeUsers: state.activeUsers,
+	user: state.login.user
 }))
 
 export default class Status extends React.Component {
@@ -24,12 +25,18 @@ export default class Status extends React.Component {
 	};
 
 	shouldComponentUpdate(nextProps) {
-		const userId = this.props.id;
+		const { id: userId, user } = this.props;
+		if (user.id === userId) {
+			return (nextProps.user && nextProps.user.status !== user.status);
+		}
 		return (nextProps.activeUsers[userId] && nextProps.activeUsers[userId].status) !== this.status;
 	}
 
 	get status() {
-		const userId = this.props.id;
+		const { id: userId, user } = this.props;
+		if (user.id === userId) {
+			return user.status || 'offline';
+		}
 		return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline';
 	}
 
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index e2cf96ecb..c66849ca2 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -31,6 +31,7 @@ export default {
 	Cancel_recording: 'Cancel recording',
 	Cancel: 'Cancel',
 	Channel_Name: 'Channel Name',
+	Chats: 'Chats',
 	Close_emoji_selector: 'Close emoji selector',
 	Code: 'Code',
 	Colaborative: 'Colaborative',
@@ -123,6 +124,7 @@ export default {
 	Privacy_Policy: ' Privacy Policy',
 	Private_Channel: 'Private Channel',
 	Private: 'Private',
+	Profile: 'Profile',
 	Public_Channel: 'Public Channel',
 	Public: 'Public',
 	Quote: 'Quote',
@@ -155,6 +157,7 @@ export default {
 	Send_audio_message: 'Send audio message',
 	Send_message: 'Send message',
 	Servers: 'Servers',
+	Settings: 'Settings',
 	Settings_succesfully_changed: 'Settings succesfully changed!',
 	Share_Message: 'Share Message',
 	Share: 'Share',
diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js
new file mode 100644
index 000000000..d4e11c676
--- /dev/null
+++ b/app/views/ProfileView/index.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Text, View } from 'react-native';
+
+import LoggedView from '../View';
+
+export default class ProfileView extends LoggedView {
+	constructor(props) {
+		super('ProfileView', props);
+	}
+
+	render() {
+		return (
+			<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
+				<Text>ProfileView</Text>
+			</View>
+		);
+	}
+}
diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js
new file mode 100644
index 000000000..c25511c71
--- /dev/null
+++ b/app/views/SettingsView/index.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Text, View } from 'react-native';
+
+import LoggedView from '../View';
+
+export default class SettingsView extends LoggedView {
+	constructor(props) {
+		super('SettingsView', props);
+	}
+
+	render() {
+		return (
+			<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
+				<Text>SettingsView</Text>
+			</View>
+		);
+	}
+}
diff --git a/e2e/05-roomslist.spec.js b/e2e/05-roomslist.spec.js
index 62485b843..ddc2911ad 100644
--- a/e2e/05-roomslist.spec.js
+++ b/e2e/05-roomslist.spec.js
@@ -87,6 +87,9 @@ describe('Rooms list screen', () => {
 			it('should navigate to add server', async() => {
 				await element(by.id('rooms-list-view-sidebar')).tap();
 				await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
+				await element(by.id('sidebar-toggle-server')).tap();
+				await waitFor(element(by.id('sidebar-add-server'))).toBeVisible().withTimeout(2000);
+				await expect(element(by.id('sidebar-add-server'))).toBeVisible();
 				await element(by.id('sidebar-add-server')).tap();
 				await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
 				await expect(element(by.id('new-server-view'))).toBeVisible();
@@ -98,6 +101,8 @@ describe('Rooms list screen', () => {
 			it('should logout', async() => {
 				await element(by.id('rooms-list-view-sidebar')).tap();
 				await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
+				await waitFor(element(by.id('sidebar-logout'))).toBeVisible().withTimeout(2000);
+				await expect(element(by.id('sidebar-logout'))).toBeVisible();
 				await element(by.id('sidebar-logout')).tap();
 				await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
 				await expect(element(by.id('welcome-view'))).toBeVisible();
diff --git a/e2e/10-changeserver.spec.js b/e2e/10-changeserver.spec.js
index c84fe35cb..e59e9289a 100644
--- a/e2e/10-changeserver.spec.js
+++ b/e2e/10-changeserver.spec.js
@@ -13,6 +13,8 @@ describe('Change server', () => {
 		// Navigate to add server
 		await element(by.id('rooms-list-view-sidebar')).tap();
 		await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
+		await element(by.id('sidebar-toggle-server')).tap();
+		await waitFor(element(by.id('sidebar-add-server'))).toBeVisible().withTimeout(2000);
 		await element(by.id('sidebar-add-server')).tap();
 		await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
 		// Add server
@@ -44,6 +46,9 @@ describe('Change server', () => {
 	it('should change server', async() => {
 		await element(by.id('rooms-list-view-sidebar')).tap();
 		await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
+		await element(by.id('sidebar-toggle-server')).tap();
+		await waitFor(element(by.id(`sidebar-${ data.server }`))).toBeVisible().withTimeout(2000);
+		await expect(element(by.id(`sidebar-${ data.server }`))).toBeVisible();
 		await element(by.id(`sidebar-${ data.server }`)).tap();
 		await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
 		await waitFor(element(by.id('rooms-list-view-sidebar').and(by.label(`Connected to ${ data.server }. Tap to view servers list.`)))).toBeVisible().withTimeout(60000);
diff --git a/e2e/11-broadcast.spec.js b/e2e/11-broadcast.spec.js
index 988eb68a6..675b81a24 100644
--- a/e2e/11-broadcast.spec.js
+++ b/e2e/11-broadcast.spec.js
@@ -93,8 +93,7 @@ describe('Broadcast room', () => {
 		await element(by.id('messagebox-input')).typeText(`${ data.random }broadcastreply`);
 		await element(by.id('messagebox-send-message')).tap();
 		await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000);
-		await expect(element(by.text(`${ data.random }message`))).toBeVisible(); // broadcasted message
-		await expect(element(by.text(` ${ data.random }broadcastreply`))).toBeVisible(); // reply
+		await expect(element(by.text(`${ data.random }message`))).toBeVisible();
 	});
 
 	afterEach(async() => {
diff --git a/e2e/helpers/app.js b/e2e/helpers/app.js
index 798a0768f..bde0fad05 100644
--- a/e2e/helpers/app.js
+++ b/e2e/helpers/app.js
@@ -28,6 +28,7 @@ async function login() {
 async function logout() {
     await element(by.id('rooms-list-view-sidebar')).tap();
     await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000);
+	await waitFor(element(by.id('sidebar-logout'))).toBeVisible().withTimeout(2000);
     await element(by.id('sidebar-logout')).tap();
     await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
     await expect(element(by.id('welcome-view'))).toBeVisible();
diff --git a/storybook/stories/index.js b/storybook/stories/index.js
index 0cc2abc02..603598e26 100644
--- a/storybook/stories/index.js
+++ b/storybook/stories/index.js
@@ -13,7 +13,7 @@ import { storiesOf } from '@storybook/react-native';
 import DirectMessage from './Channels/DirectMessage';
 import Avatar from './Avatar';
 
-const reducers = combineReducers({ settings: () => ({}) });
+const reducers = combineReducers({ settings: () => ({}), login: () => ({ user: {} }) });
 const store = createStore(reducers);
 
 storiesOf('Avatar', module).addDecorator(story => <Provider store={store}>{story()}</Provider>).add('avatar', () => Avatar);
-- 
GitLab