[NEW] Drawer (#322)

This commit is contained in:
Diego Mello 2018-06-04 22:17:02 -03:00 committed by Guilherme Gazzo
parent 12f8b26701
commit da3679d46a
11 changed files with 357 additions and 71 deletions

View File

@ -1,45 +1,84 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; 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 { 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 database from '../lib/realm';
import { setServer } from '../actions/server'; import { setServer } from '../actions/server';
import { logout } from '../actions/login'; 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'; import I18n from '../i18n';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
scrollView: { selected: {
paddingTop: 20 backgroundColor: 'rgba(0, 0, 0, .04)'
}, },
imageContainer: { item: {
width: '100%', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
image: { itemLeft: {
width: 200, marginHorizontal: 10,
height: 200, width: 30,
borderRadius: 100 alignItems: 'center'
}, },
serverTitle: { itemLeftOpacity: {
fontSize: 16, opacity: 0.62
color: 'grey',
padding: 10,
width: '100%'
}, },
serverItem: { itemText: {
backgroundColor: 'white', marginVertical: 16,
padding: 10, fontWeight: 'bold',
flex: 1 color: '#292E35'
}, },
selectedServer: { separator: {
backgroundColor: '#eeeeee' 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; const keyExtractor = item => item.id;
@connect(state => ({ @connect(state => ({
server: state.server.server server: state.server.server,
user: state.login.user
}), dispatch => ({ }), dispatch => ({
selectServer: server => dispatch(setServer(server)), selectServer: server => dispatch(setServer(server)),
logout: () => dispatch(logout()) logout: () => dispatch(logout())
@ -54,7 +93,23 @@ export default class Sidebar extends Component {
constructor(props) { constructor(props) {
super(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() { componentDidMount() {
@ -83,54 +138,195 @@ export default class Sidebar extends Component {
this.props.navigation.dispatch(DrawerActions.closeDrawer()); this.props.navigation.dispatch(DrawerActions.closeDrawer());
} }
renderItem = ({ item, separators }) => ( toggleServers = () => {
LayoutAnimation.easeInEaseOut();
this.setState({ showServers: !this.state.showServers });
}
<TouchableHighlight isRouteFocused = (route) => {
onShowUnderlay={separators.highlight} const { state } = this.props.navigation;
onHideUnderlay={separators.unhighlight} const activeItemKey = state.routes[state.index] ? state.routes[state.index].key : null;
onPress={() => { this.onPressItem(item); }} return activeItemKey === route;
testID={`sidebar-${ item.id }`} }
sidebarNavigate = (route) => {
const { navigate } = this.props.navigation;
if (!this.isRouteFocused(route)) {
navigate(route);
}
}
renderSeparator = key => <View key={key} style={styles.separator} />;
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)]}> <View style={[styles.item, selected && styles.selected]}>
<Text> <View style={[styles.itemLeft, !selected && styles.itemLeftOpacity]}>
{item.id} {left}
</View>
<Text style={styles.itemText}>
{text}
</Text> </Text>
</View> </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() { render() {
const { user, server } = this.props;
return ( return (
<ScrollView style={styles.scrollView}> <ScrollView>
<View style={{ paddingBottom: 20 }} testID='sidebar'> <SafeAreaView
<FlatList style={styles.container}
data={this.state.servers} forceInset={{ top: 'always', horizontal: 'never' }}
renderItem={this.renderItem} testID='sidebar'
keyExtractor={keyExtractor} >
/> <Touch
<TouchableHighlight onPress={() => this.toggleServers()}
onPress={() => { underlayColor='rgba(255, 255, 255, 0.5)'
this.closeDrawer(); activeOpacity={0.3}
this.props.logout(); testID='sidebar-toggle-server'
}}
testID='sidebar-logout'
> >
<View style={styles.serverItem}> <View style={styles.header}>
<Text>{I18n.t('Logout')}</Text> <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> </View>
</TouchableHighlight> </Touch>
<TouchableHighlight
onPress={() => { {this.renderSeparator('separator-header')}
this.closeDrawer();
this.props.navigation.navigate({ key: 'AddServer', routeName: 'AddServer' }); {!this.state.showServers && this.renderNavigation()}
}} {this.state.showServers && this.renderServers()}
testID='sidebar-add-server' </SafeAreaView>
>
<View style={styles.serverItem}>
<Text>{I18n.t('Add_Server')}</Text>
</View>
</TouchableHighlight>
</View>
</ScrollView> </ScrollView>
); );
} }

View File

@ -1,5 +1,7 @@
import React from 'react';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { createStackNavigator, createDrawerNavigator } from 'react-navigation'; import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
import Icon from 'react-native-vector-icons/MaterialIcons';
import Sidebar from '../../containers/Sidebar'; import Sidebar from '../../containers/Sidebar';
import RoomsListView from '../../views/RoomsListView'; import RoomsListView from '../../views/RoomsListView';
@ -17,6 +19,8 @@ import RoomFilesView from '../../views/RoomFilesView';
import RoomMembersView from '../../views/RoomMembersView'; import RoomMembersView from '../../views/RoomMembersView';
import RoomInfoView from '../../views/RoomInfoView'; import RoomInfoView from '../../views/RoomInfoView';
import RoomInfoEditView from '../../views/RoomInfoEditView'; import RoomInfoEditView from '../../views/RoomInfoEditView';
import ProfileView from '../../views/ProfileView';
import SettingsView from '../../views/SettingsView';
import I18n from '../../i18n'; import I18n from '../../i18n';
const headerTintColor = '#292E35'; const headerTintColor = '#292E35';
@ -130,15 +134,45 @@ const AuthRoutes = createStackNavigator(
const Routes = createDrawerNavigator( const Routes = createDrawerNavigator(
{ {
Home: { Chats: {
screen: AuthRoutes 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, contentComponent: Sidebar,
navigationOptions: { navigationOptions: {
drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked' drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked'
} },
initialRouteName: 'Chats',
backBehavior: 'initialRoute'
} }
); );

View File

@ -13,7 +13,8 @@ const styles = StyleSheet.create({
}); });
@connect(state => ({ @connect(state => ({
activeUsers: state.activeUsers activeUsers: state.activeUsers,
user: state.login.user
})) }))
export default class Status extends React.Component { export default class Status extends React.Component {
@ -24,12 +25,18 @@ export default class Status extends React.Component {
}; };
shouldComponentUpdate(nextProps) { 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; return (nextProps.activeUsers[userId] && nextProps.activeUsers[userId].status) !== this.status;
} }
get 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'; return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline';
} }

View File

@ -31,6 +31,7 @@ export default {
Cancel_recording: 'Cancel recording', Cancel_recording: 'Cancel recording',
Cancel: 'Cancel', Cancel: 'Cancel',
Channel_Name: 'Channel Name', Channel_Name: 'Channel Name',
Chats: 'Chats',
Close_emoji_selector: 'Close emoji selector', Close_emoji_selector: 'Close emoji selector',
Code: 'Code', Code: 'Code',
Colaborative: 'Colaborative', Colaborative: 'Colaborative',
@ -123,6 +124,7 @@ export default {
Privacy_Policy: ' Privacy Policy', Privacy_Policy: ' Privacy Policy',
Private_Channel: 'Private Channel', Private_Channel: 'Private Channel',
Private: 'Private', Private: 'Private',
Profile: 'Profile',
Public_Channel: 'Public Channel', Public_Channel: 'Public Channel',
Public: 'Public', Public: 'Public',
Quote: 'Quote', Quote: 'Quote',
@ -155,6 +157,7 @@ export default {
Send_audio_message: 'Send audio message', Send_audio_message: 'Send audio message',
Send_message: 'Send message', Send_message: 'Send message',
Servers: 'Servers', Servers: 'Servers',
Settings: 'Settings',
Settings_succesfully_changed: 'Settings succesfully changed!', Settings_succesfully_changed: 'Settings succesfully changed!',
Share_Message: 'Share Message', Share_Message: 'Share Message',
Share: 'Share', Share: 'Share',

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -87,6 +87,9 @@ describe('Rooms list screen', () => {
it('should navigate to add server', async() => { it('should navigate to add server', async() => {
await element(by.id('rooms-list-view-sidebar')).tap(); await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000); 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 element(by.id('sidebar-add-server')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('new-server-view'))).toBeVisible(); await expect(element(by.id('new-server-view'))).toBeVisible();
@ -98,6 +101,8 @@ describe('Rooms list screen', () => {
it('should logout', async() => { it('should logout', async() => {
await element(by.id('rooms-list-view-sidebar')).tap(); await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000); 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 element(by.id('sidebar-logout')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000); await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('welcome-view'))).toBeVisible(); await expect(element(by.id('welcome-view'))).toBeVisible();

View File

@ -13,6 +13,8 @@ describe('Change server', () => {
// Navigate to add server // Navigate to add server
await element(by.id('rooms-list-view-sidebar')).tap(); await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000); 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 element(by.id('sidebar-add-server')).tap();
await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('new-server-view'))).toBeVisible().withTimeout(2000);
// Add server // Add server
@ -44,6 +46,9 @@ describe('Change server', () => {
it('should change server', async() => { it('should change server', async() => {
await element(by.id('rooms-list-view-sidebar')).tap(); await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000); 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 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'))).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); await waitFor(element(by.id('rooms-list-view-sidebar').and(by.label(`Connected to ${ data.server }. Tap to view servers list.`)))).toBeVisible().withTimeout(60000);

View File

@ -93,8 +93,7 @@ describe('Broadcast room', () => {
await element(by.id('messagebox-input')).typeText(`${ data.random }broadcastreply`); await element(by.id('messagebox-input')).typeText(`${ data.random }broadcastreply`);
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000); 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 }message`))).toBeVisible();
await expect(element(by.text(` ${ data.random }broadcastreply`))).toBeVisible(); // reply
}); });
afterEach(async() => { afterEach(async() => {

View File

@ -28,6 +28,7 @@ async function login() {
async function logout() { async function logout() {
await element(by.id('rooms-list-view-sidebar')).tap(); await element(by.id('rooms-list-view-sidebar')).tap();
await waitFor(element(by.id('sidebar'))).toBeVisible().withTimeout(2000); 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 element(by.id('sidebar-logout')).tap();
await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000); await waitFor(element(by.id('welcome-view'))).toBeVisible().withTimeout(2000);
await expect(element(by.id('welcome-view'))).toBeVisible(); await expect(element(by.id('welcome-view'))).toBeVisible();

View File

@ -13,7 +13,7 @@ import { storiesOf } from '@storybook/react-native';
import DirectMessage from './Channels/DirectMessage'; import DirectMessage from './Channels/DirectMessage';
import Avatar from './Avatar'; import Avatar from './Avatar';
const reducers = combineReducers({ settings: () => ({}) }); const reducers = combineReducers({ settings: () => ({}), login: () => ({ user: {} }) });
const store = createStore(reducers); const store = createStore(reducers);
storiesOf('Avatar', module).addDecorator(story => <Provider store={store}>{story()}</Provider>).add('avatar', () => Avatar); storiesOf('Avatar', module).addDecorator(story => <Provider store={store}>{story()}</Provider>).add('avatar', () => Avatar);