diff --git a/app/index.js b/app/index.js index 289e8fbd9..e916e59d2 100644 --- a/app/index.js +++ b/app/index.js @@ -18,26 +18,17 @@ const startLogged = () => { Navigation.loadView('RoomHeaderView'); Navigation.loadView('SettingsView'); Navigation.loadView('SidebarView'); + Navigation.setRoot({ root: { - sideMenu: { - left: { + stack: { + id: 'AppRoot', + children: [{ component: { - id: 'SidebarView', - name: 'SidebarView' + id: 'RoomsListView', + name: 'RoomsListView' } - }, - center: { - stack: { - id: 'AppRoot', - children: [{ - component: { - id: 'RoomsListView', - name: 'RoomsListView' - } - }] - } - } + }] } } }); diff --git a/app/sagas/login.js b/app/sagas/login.js index 5ccfeeb5d..78a17d9e6 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -6,11 +6,12 @@ import { import Navigation from '../lib/Navigation'; import * as types from '../actions/actionsTypes'; import { appStart } from '../actions'; -import { serverFinishAdd } from '../actions/server'; +import { serverFinishAdd, selectServerRequest } from '../actions/server'; import { loginFailure, loginSuccess } from '../actions/login'; import RocketChat from '../lib/rocketchat'; import log from '../utils/log'; import I18n from '../i18n'; +import database from '../lib/realm'; const getServer = state => state.server.server; const loginWithPasswordCall = args => RocketChat.loginWithPassword(args); @@ -60,8 +61,26 @@ const handleLogout = function* handleLogout() { if (server) { try { yield call(logoutCall, { server }); + const { serversDB } = database.databases; + // all servers + const servers = yield serversDB.objects('servers'); + // filter logging out server and delete it + const serverRecord = servers.filtered('id = $0', server); + serversDB.write(() => { + serversDB.delete(serverRecord); + }); + // see if there's other logged in servers and selects first one + if (servers.length > 0) { + const newServer = servers[0].id; + const token = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ newServer }`); + if (token) { + return yield put(selectServerRequest(newServer)); + } + } + // if there's no servers, go outside yield put(appStart('outside')); } catch (e) { + yield put(appStart('outside')); log('handleLogout', e); } } diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js index 27f00ea84..c9199d9de 100644 --- a/app/views/ProfileView/index.js +++ b/app/views/ProfileView/index.js @@ -1,8 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - View, ScrollView, Keyboard, BackHandler -} from 'react-native'; +import { View, ScrollView, Keyboard } from 'react-native'; import { connect } from 'react-redux'; import Dialog from 'react-native-dialog'; import SHA256 from 'js-sha256'; @@ -12,7 +10,6 @@ import RNPickerSelect from 'react-native-picker-select'; import SafeAreaView from 'react-native-safe-area-view'; import equal from 'deep-equal'; -import Navigation from '../../lib/Navigation'; import LoggedView from '../View'; import KeyboardView from '../../presentation/KeyboardView'; import sharedStyles from '../Styles'; @@ -26,9 +23,7 @@ import I18n from '../../i18n'; import Button from '../../containers/Button'; import Avatar from '../../containers/Avatar'; import Touch from '../../utils/touch'; -import { appStart as appStartAction } from '../../actions'; import { setUser as setUserAction } from '../../actions/login'; -import Icons from '../../lib/Icons'; @connect(state => ({ user: { @@ -42,7 +37,6 @@ import Icons from '../../lib/Icons'; Accounts_CustomFields: state.settings.Accounts_CustomFields, baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' }), dispatch => ({ - appStart: () => dispatch(appStartAction()), setUser: params => dispatch(setUserAction(params)) })) /** @extends React.Component */ @@ -50,32 +44,17 @@ export default class ProfileView extends LoggedView { static options() { return { topBar: { - leftButtons: [{ - id: 'settings', - icon: Icons.getSource('settings'), - testID: 'rooms-list-view-sidebar' - }], title: { text: I18n.t('Profile') } - }, - sideMenu: { - left: { - enabled: true - }, - right: { - enabled: true - } } }; } static propTypes = { baseUrl: PropTypes.string, - componentId: PropTypes.string, user: PropTypes.object, Accounts_CustomFields: PropTypes.string, - appStart: PropTypes.func, setUser: PropTypes.func } @@ -94,8 +73,6 @@ export default class ProfileView extends LoggedView { avatarSuggestions: {}, customFields: {} }; - Navigation.events().bindComponent(this); - BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); } async componentDidMount() { @@ -126,22 +103,6 @@ export default class ProfileView extends LoggedView { return false; } - componentWillUnmount() { - BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); - } - - navigationButtonPressed = ({ buttonId }) => { - if (buttonId === 'settings') { - Navigation.toggleDrawer(); - } - } - - handleBackPress = () => { - const { appStart } = this.props; - appStart('background'); - return false; - } - setAvatar = (avatar) => { this.setState({ avatar }); } diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js index 3f4508819..34db1f007 100644 --- a/app/views/RoomsListView/ServerDropdown.js +++ b/app/views/RoomsListView/ServerDropdown.js @@ -81,6 +81,13 @@ export default class ServerDropdown extends Component { } } + componentWillUnmount() { + if (this.newServerTimeout) { + clearTimeout(this.newServerTimeout); + this.newServerTimeout = false; + } + } + updateState = () => { const { servers } = this; this.setState({ servers }); @@ -137,12 +144,7 @@ export default class ServerDropdown extends Component { const token = await AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`); if (!token) { appStart(); - try { - this.sdk.disconnect(); - } catch (error) { - console.warn(error); - } - setTimeout(() => { + this.newServerTimeout = setTimeout(() => { EventEmitter.emit('NewServer', { server }); }, 1000); } else { diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 8d78aa8b4..182344753 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -270,7 +270,22 @@ export default class RoomsListView extends LoggedView { } }); } else if (buttonId === 'settings') { - Navigation.toggleDrawer(); + Navigation.showModal({ + stack: { + children: [{ + component: { + name: 'SidebarView', + options: { + topBar: { + title: { + text: I18n.t('Settings') + } + } + } + } + }] + } + }); } else if (buttonId === 'search') { this.initSearchingAndroid(); } else if (buttonId === 'back') { diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js index 6ca848e70..a53ae7ab5 100644 --- a/app/views/SettingsView/index.js +++ b/app/views/SettingsView/index.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, ScrollView, BackHandler } from 'react-native'; +import { View, ScrollView } from 'react-native'; import RNPickerSelect from 'react-native-picker-select'; import { connect } from 'react-redux'; import SafeAreaView from 'react-native-safe-area-view'; @@ -18,36 +18,20 @@ import Loading from '../../containers/Loading'; import { showErrorAlert, showToast } from '../../utils/info'; import log from '../../utils/log'; import { setUser as setUserAction } from '../../actions/login'; -import { appStart as appStartAction } from '../../actions'; -import Icons from '../../lib/Icons'; @connect(state => ({ userLanguage: state.login.user && state.login.user.language }), dispatch => ({ - setUser: params => dispatch(setUserAction(params)), - appStart: () => dispatch(appStartAction()) + setUser: params => dispatch(setUserAction(params)) })) /** @extends React.Component */ export default class SettingsView extends LoggedView { static options() { return { topBar: { - leftButtons: [{ - id: 'settings', - icon: Icons.getSource('settings'), - testID: 'rooms-list-view-sidebar' - }], title: { text: I18n.t('Settings') } - }, - sideMenu: { - left: { - enabled: true - }, - right: { - enabled: true - } } }; } @@ -55,8 +39,7 @@ export default class SettingsView extends LoggedView { static propTypes = { componentId: PropTypes.string, userLanguage: PropTypes.string, - setUser: PropTypes.func, - appStart: PropTypes.func + setUser: PropTypes.func } constructor(props) { @@ -85,8 +68,6 @@ export default class SettingsView extends LoggedView { }], saving: false }; - Navigation.events().bindComponent(this); - BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); } shouldComponentUpdate(nextProps, nextState) { @@ -104,22 +85,6 @@ export default class SettingsView extends LoggedView { return false; } - componentWillUnmount() { - BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress); - } - - navigationButtonPressed = ({ buttonId }) => { - if (buttonId === 'settings') { - Navigation.toggleDrawer(); - } - } - - handleBackPress = () => { - const { appStart } = this.props; - appStart('background'); - return false; - } - getLabel = (language) => { const { languages } = this.state; const l = languages.find(i => i.value === language); diff --git a/app/views/SidebarView.js b/app/views/SidebarView.js index 0408c3e76..ed44ddf4c 100644 --- a/app/views/SidebarView.js +++ b/app/views/SidebarView.js @@ -1,14 +1,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { - ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, SafeAreaView + ScrollView, Text, View, StyleSheet, FlatList, LayoutAnimation, SafeAreaView, Image } from 'react-native'; import { connect } from 'react-redux'; import Icon from 'react-native-vector-icons/MaterialIcons'; import equal from 'deep-equal'; import Navigation from '../lib/Navigation'; -import { setStackRoot as setStackRootAction } from '../actions'; import { logout as logoutAction } from '../actions/login'; import Avatar from '../containers/Avatar'; import Status from '../containers/status'; @@ -18,7 +17,8 @@ import RocketChat from '../lib/rocketchat'; import log from '../utils/log'; import I18n from '../i18n'; import scrollPersistTaps from '../utils/scrollPersistTaps'; -import { getReadableVersion } from '../utils/deviceInfo'; +import { getReadableVersion, isIOS, isAndroid } from '../utils/deviceInfo'; +import Icons from '../lib/Icons'; const styles = StyleSheet.create({ container: { @@ -34,14 +34,14 @@ const styles = StyleSheet.create({ width: 30, alignItems: 'center' }, + itemCenter: { + flex: 1 + }, itemText: { marginVertical: 16, fontWeight: 'bold', color: '#292E35' }, - itemSelected: { - backgroundColor: '#F7F8FA' - }, separator: { borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#ddd', @@ -79,13 +79,22 @@ const styles = StyleSheet.create({ fontWeight: '600', color: '#292E35', fontSize: 13 + }, + disclosureContainer: { + marginLeft: 6, + marginRight: 9, + alignItems: 'center', + justifyContent: 'center' + }, + disclosureIndicator: { + width: 20, + height: 20 } }); const keyExtractor = item => item.id; @connect(state => ({ Site_Name: state.settings.Site_Name, - stackRoot: state.app.stackRoot, user: { id: state.login.user && state.login.user.id, language: state.login.user && state.login.user.language, @@ -95,18 +104,27 @@ const keyExtractor = item => item.id; }, baseUrl: state.settings.Site_Url || state.server ? state.server.server : '' }), dispatch => ({ - logout: () => dispatch(logoutAction()), - setStackRoot: stackRoot => dispatch(setStackRootAction(stackRoot)) + logout: () => dispatch(logoutAction()) })) export default class Sidebar extends Component { + static options() { + return { + topBar: { + leftButtons: [{ + id: 'cancel', + icon: isAndroid ? Icons.getSource('close', false) : undefined, + systemItem: 'cancel' + }] + } + }; + } + static propTypes = { baseUrl: PropTypes.string, componentId: PropTypes.string, Site_Name: PropTypes.string.isRequired, - stackRoot: PropTypes.string.isRequired, user: PropTypes.object, - logout: PropTypes.func.isRequired, - setStackRoot: PropTypes.func + logout: PropTypes.func.isRequired } constructor(props) { @@ -131,18 +149,13 @@ export default class Sidebar extends Component { shouldComponentUpdate(nextProps, nextState) { const { status, showStatus } = this.state; - const { - Site_Name, stackRoot, user, baseUrl - } = this.props; + const { Site_Name, user, baseUrl } = this.props; if (nextState.showStatus !== showStatus) { return true; } if (nextProps.Site_Name !== Site_Name) { return true; } - if (nextProps.stackRoot !== stackRoot) { - return true; - } if (nextProps.Site_Name !== Site_Name) { return true; } @@ -166,11 +179,6 @@ export default class Sidebar extends Component { return false; } - handleChangeStack = (event) => { - const { stack } = event; - this.setStack(stack); - } - navigationButtonPressed = ({ buttonId }) => { if (buttonId === 'cancel') { const { componentId } = this.props; @@ -196,37 +204,30 @@ export default class Sidebar extends Component { }); } - setStack = async(stack) => { - const { stackRoot, setStackRoot } = this.props; - if (stackRoot !== stack) { - await Navigation.setStackRoot('AppRoot', { - component: { - id: stack, - name: stack - } - }); - setStackRoot(stack); - } - } - - closeDrawer = () => { - Navigation.toggleDrawer(); - } - toggleStatus = () => { LayoutAnimation.easeInEaseOut(); this.setState(prevState => ({ showStatus: !prevState.showStatus })); } - sidebarNavigate = (stack) => { - this.closeDrawer(); - this.setStack(stack); + sidebarNavigate = (route) => { + const { componentId } = this.props; + Navigation.push(componentId, { + component: { + name: route + } + }); + } + + logout = () => { + const { componentId, logout } = this.props; + Navigation.dismissModal(componentId); + logout(); } renderSeparator = key => ; renderItem = ({ - text, left, onPress, testID, current + text, left, onPress, testID, disclosure }) => ( - + {left} - - {text} - + + + {text} + + + {disclosure ? this.renderDisclosure() : null} ) @@ -254,7 +258,6 @@ export default class Sidebar extends Component { left: , current: user.status === item.id, onPress: () => { - this.closeDrawer(); this.toggleStatus(); if (user.status !== item.id) { try { @@ -268,43 +271,43 @@ export default class Sidebar extends Component { ); } - renderNavigation = () => { - const { stackRoot } = this.props; - const { logout } = this.props; - return ( - [ - this.renderItem({ - text: I18n.t('Chats'), - left: , - onPress: () => this.sidebarNavigate('RoomsListView'), - testID: 'sidebar-chats', - current: stackRoot === 'RoomsListView' - }), - this.renderItem({ - text: I18n.t('Profile'), - left: , - onPress: () => this.sidebarNavigate('ProfileView'), - testID: 'sidebar-profile', - current: stackRoot === 'ProfileView' - }), - this.renderItem({ - text: I18n.t('Settings'), - left: , - onPress: () => this.sidebarNavigate('SettingsView'), - testID: 'sidebar-settings', - current: stackRoot === 'SettingsView' - }), - this.renderSeparator('separator-logout'), - this.renderItem({ - text: I18n.t('Logout'), - left: , - onPress: () => logout(), - testID: 'sidebar-logout' - }) - ] - ); + // Remove it after https://github.com/RocketChat/Rocket.Chat.ReactNative/pull/643 + renderDisclosure = () => { + if (isIOS) { + return ( + + + + ); + } } + renderNavigation = () => ( + [ + this.renderItem({ + text: I18n.t('Profile'), + left: , + onPress: () => this.sidebarNavigate('ProfileView'), + testID: 'sidebar-profile', + disclosure: true + }), + this.renderItem({ + text: I18n.t('Settings'), + left: , + onPress: () => this.sidebarNavigate('SettingsView'), + testID: 'sidebar-settings', + disclosure: true + }), + this.renderSeparator('separator-logout'), + this.renderItem({ + text: I18n.t('Logout'), + left: , + onPress: () => this.logout(), + testID: 'sidebar-logout' + }) + ] + ) + renderStatus = () => { const { status } = this.state; const { user } = this.props; diff --git a/e2e/08-room.spec.js b/e2e/08-room.spec.js index 6c25ee587..02fefdc52 100644 --- a/e2e/08-room.spec.js +++ b/e2e/08-room.spec.js @@ -169,7 +169,7 @@ describe('Room screen', () => { await element(by.text(`${ data.random }message`)).longPress(); await waitFor(element(by.text('Message actions'))).toBeVisible().withTimeout(5000); await expect(element(by.text('Message actions'))).toBeVisible(); - await element(by.text('Copy Permalink')).tap(); + await element(by.text('Permalink')).tap(); await expect(element(by.text('Permalink copied to clipboard!'))).toBeVisible(); await waitFor(element(by.text('Permalink copied to clipboard!'))).toBeNotVisible().withTimeout(5000); @@ -180,7 +180,7 @@ describe('Room screen', () => { await element(by.text(`${ data.random }message`)).longPress(); await waitFor(element(by.text('Message actions'))).toBeVisible().withTimeout(5000); await expect(element(by.text('Message actions'))).toBeVisible(); - await element(by.text('Copy Message')).tap(); + await element(by.text('Copy')).tap(); await expect(element(by.text('Copied to clipboard!'))).toBeVisible(); await waitFor(element(by.text('Copied to clipboard!'))).toBeNotVisible().withTimeout(5000); // TODO: test clipboard