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' }, - serverTitle: { - fontSize: 16, - color: 'grey', - padding: 10, - width: '100%' + itemLeftOpacity: { + opacity: 0.62 }, - serverItem: { - backgroundColor: 'white', - padding: 10, - flex: 1 + itemText: { + marginVertical: 16, + fontWeight: 'bold', + color: '#292E35' }, - selectedServer: { - backgroundColor: '#eeeeee' + separator: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: '#ddd', + marginVertical: 4 + }, + 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 }); + } - { this.onPressItem(item); }} - testID={`sidebar-${ item.id }`} + 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 => ; + + renderItem = ({ + text, left, selected, onPress, testID + }) => ( + - - - {item.id} + + + {left} + + + {text} - - ); + + ) + + renderStatusItem = ({ item }) => ( + this.renderItem({ + text: item.name, + left: , + 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: , + 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: , + onPress: () => this.sidebarNavigate('Chats'), + selected: this.isRouteFocused('Chats'), + testID: 'sidebar-chats' + }), + this.renderItem({ + text: I18n.t('Profile'), + left: , + onPress: () => this.sidebarNavigate('ProfileView'), + selected: this.isRouteFocused('ProfileView'), + testID: 'sidebar-profile' + }), + this.renderItem({ + text: I18n.t('Settings'), + left: , + onPress: () => this.sidebarNavigate('SettingsView'), + selected: this.isRouteFocused('SettingsView'), + testID: 'sidebar-settings' + }), + this.renderSeparator('separator-logout'), + this.renderItem({ + text: I18n.t('Logout'), + left: , + onPress: () => this.props.logout(), + testID: 'sidebar-logout' + }) + ] + ) + + renderServers = () => ( + [ + , + this.renderSeparator('separator-status'), + , + this.renderSeparator('separator-add-server'), + this.renderItem({ + text: I18n.t('Add_Server'), + left: , + onPress: () => { + this.closeDrawer(); + this.toggleServers(); + this.props.navigation.navigate('AddServer'); + }, + testID: 'sidebar-add-server' + }) + ] + ) render() { + const { user, server } = this.props; return ( - - - - { - this.closeDrawer(); - this.props.logout(); - }} - testID='sidebar-logout' + + + this.toggleServers()} + underlayColor='rgba(255, 255, 255, 0.5)' + activeOpacity={0.3} + testID='sidebar-toggle-server' > - - {I18n.t('Logout')} + + + + + + {user.username} + + {server} + + - - { - this.closeDrawer(); - this.props.navigation.navigate({ key: 'AddServer', routeName: 'AddServer' }); - }} - testID='sidebar-add-server' - > - - {I18n.t('Add_Server')} - - - + + + {this.renderSeparator('separator-header')} + + {!this.state.showServers && this.renderNavigation()} + {this.state.showServers && this.renderServers()} + ); } 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: () => + } + }, + ProfileView: { + screen: createStackNavigator({ + ProfileView: { + screen: ProfileView, + navigationOptions: ({ navigation }) => ({ + title: 'Profile', + headerTintColor: '#292E35', + headerLeft: navigation.toggleDrawer()} /> // TODO: refactor + }) + } + }) + }, + SettingsView: { + screen: createStackNavigator({ + SettingsView: { + screen: SettingsView, + navigationOptions: ({ navigation }) => ({ + title: 'Settings', + headerTintColor: '#292E35', + headerLeft: 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 ( + + ProfileView + + ); + } +} 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 ( + + SettingsView + + ); + } +} 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 => {story()}).add('avatar', () => Avatar);