diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index 655338416291847376944ee9e82315ab05885e2a..e2acce4c7e40ed642924e1604d25eeb90445927e 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 cd46d1b4862e85a5224a7f3b5eb43b4591b0075e..3ded765b322430ca6af6a079e714080c1a839e55 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 8a434d0cfda4e0d3b9099656fca313ff6ac080ad..a2797ea5a530812fdb13540b53039ba494dd3c2b 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 e2cf96ecb69f93fde892c3773157523e49467423..c66849ca2c7023bfa383666926ee774051da5c9a 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 0000000000000000000000000000000000000000..d4e11c6767f26c7c753c1a93321de9ddcbb77a97 --- /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 0000000000000000000000000000000000000000..c25511c71a83f478383ed3aaa1a93ca4d2c7e758 --- /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 62485b843174b253faf0c7fd9ec6a106a8e2e462..ddc2911adee09426e65226be44643ada104cc5eb 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 c84fe35cbd10418b52c7316e3109bb737aa620e5..e59e9289a2f0c4efcf8576cc7402f1e87767d5a7 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 988eb68a62f3cb9a341bcce70a16fe65073beff9..675b81a2471d727d34109905c7e9caf46ae5285a 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 798a0768f8f431a621c5cdbe7da0cb6d319fb3bd..bde0fad0577501b46dfbfdd52283db9e34f22f65 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 0cc2abc02b1a671770d0e1d3aa4cab864a82ae3a..603598e268ab1823aea86240b401df5a638112fd 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);