Unverified Commit d2544a79 authored by Reinaldo Neto's avatar Reinaldo Neto Committed by GitHub
Browse files

[IMPROVE] Onboarding changes (#3387)



- Change the first screen of the app
- Minor changes on NewServerView and make it the first screen of the app
- Add "Create workspace" to ServerDropdown
Co-authored-by: default avatarDiego Mello <diegolmello@gmail.com>
parent 30aab71d
......@@ -1444,6 +1444,7 @@ Array [
Object {
"fontSize": 12,
},
undefined,
]
}
>
......@@ -1605,6 +1606,7 @@ Array [
Object {
"fontSize": 12,
},
undefined,
]
}
>
......@@ -41153,6 +41155,7 @@ exports[`Storyshots Message Show a button as attachment 1`] = `
"color": "#ffffff",
},
undefined,
undefined,
]
}
>
......@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import Navigation from './lib/Navigation';
import { defaultHeader, getActiveRouteName, navigationTheme } from './utils/navigation';
import { ROOT_INSIDE, ROOT_LOADING, ROOT_NEW_SERVER, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app';
import { ROOT_INSIDE, ROOT_LOADING, ROOT_OUTSIDE, ROOT_SET_USERNAME } from './actions/app';
// Stacks
import AuthLoadingView from './views/AuthLoadingView';
// SetUsername Stack
......@@ -56,9 +56,7 @@ const App = React.memo(({ root, isMasterDetail }: { root: string; isMasterDetail
<Stack.Navigator screenOptions={{ headerShown: false, animationEnabled: false }}>
<>
{root === ROOT_LOADING ? <Stack.Screen name='AuthLoading' component={AuthLoadingView} /> : null}
{root === ROOT_OUTSIDE || root === ROOT_NEW_SERVER ? (
<Stack.Screen name='OutsideStack' component={OutsideStack} />
) : null}
{root === ROOT_OUTSIDE ? <Stack.Screen name='OutsideStack' component={OutsideStack} /> : null}
{root === ROOT_INSIDE && isMasterDetail ? (
<Stack.Screen name='MasterDetailStack' component={MasterDetailStack} />
) : null}
......
......@@ -3,7 +3,6 @@ import { APP } from './actionsTypes';
export const ROOT_OUTSIDE = 'outside';
export const ROOT_INSIDE = 'inside';
export const ROOT_LOADING = 'loading';
export const ROOT_NEW_SERVER = 'newServer';
export const ROOT_SET_USERNAME = 'setUsername';
export function appStart({ root, ...args }) {
......
......@@ -17,6 +17,7 @@ interface IButtonProps {
color: string;
fontSize: any;
style: any;
styleText?: any;
testID: string;
}
......@@ -48,7 +49,8 @@ export default class Button extends React.PureComponent<Partial<IButtonProps>, a
};
render() {
const { title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, ...otherProps } = this.props;
const { title, type, onPress, disabled, backgroundColor, color, loading, style, theme, fontSize, styleText, ...otherProps } =
this.props;
const isPrimary = type === 'primary';
let textColor = isPrimary ? themes[theme!].buttonText : themes[theme!].bodyText;
......@@ -72,7 +74,7 @@ export default class Button extends React.PureComponent<Partial<IButtonProps>, a
{loading ? (
<ActivityIndicator color={textColor} />
) : (
<Text style={[styles.text, { color: textColor }, fontSize && { fontSize }]} accessibilityLabel={title}>
<Text style={[styles.text, { color: textColor }, fontSize && { fontSize }, styleText]} accessibilityLabel={title}>
{title}
</Text>
)}
......
......@@ -7,7 +7,6 @@ const initialState = {
server: '',
version: null,
loading: true,
adding: false,
previousServer: null,
changingServer: false
};
......@@ -58,13 +57,11 @@ export default function server(state = initialState, action) {
case SERVER.INIT_ADD:
return {
...state,
adding: true,
previousServer: action.previousServer
};
case SERVER.FINISH_ADD:
return {
...state,
adding: false,
previousServer: null
};
default:
......
......@@ -8,7 +8,7 @@ import { inviteLinksRequest, inviteLinksSetToken } from '../actions/inviteLinks'
import database from '../lib/database';
import RocketChat from '../lib/rocketchat';
import EventEmitter from '../utils/events';
import { ROOT_INSIDE, ROOT_NEW_SERVER, appInit, appStart } from '../actions/app';
import { ROOT_INSIDE, ROOT_OUTSIDE, appInit, appStart } from '../actions/app';
import { localAuthenticate } from '../utils/localAuthentication';
import { goRoom } from '../utils/goRoom';
import { loginRequest } from '../actions/login';
......@@ -180,7 +180,7 @@ const handleOpen = function* handleOpen({ params }) {
yield fallbackNavigation();
return;
}
yield put(appStart({ root: ROOT_NEW_SERVER }));
yield put(appStart({ root: ROOT_OUTSIDE }));
yield put(serverInitAdd(server));
yield delay(1000);
EventEmitter.emit('NewServer', { server: host });
......
......@@ -118,8 +118,6 @@ const fetchRooms = function* fetchRooms() {
const handleLoginSuccess = function* handleLoginSuccess({ user }) {
try {
const adding = yield select(state => state.server.adding);
RocketChat.getUserPresence(user.id);
const server = yield select(getServer);
......@@ -170,24 +168,10 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
yield put(setUser(user));
EventEmitter.emit('connected');
let currentRoot;
if (adding) {
yield put(serverFinishAdd());
yield put(appStart({ root: ROOT_INSIDE }));
} else {
currentRoot = yield select(state => state.app.root);
if (currentRoot !== ROOT_INSIDE) {
yield put(appStart({ root: ROOT_INSIDE }));
}
}
// after a successful login, check if it's been invited via invite link
currentRoot = yield select(state => state.app.root);
if (currentRoot === ROOT_INSIDE) {
const inviteLinkToken = yield select(state => state.inviteLinks.token);
if (inviteLinkToken) {
yield put(inviteLinksRequest(inviteLinkToken));
}
yield put(appStart({ root: ROOT_INSIDE }));
const inviteLinkToken = yield select(state => state.inviteLinks.token);
if (inviteLinkToken) {
yield put(inviteLinksRequest(inviteLinkToken));
}
} catch (e) {
log(e);
......
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { ThemeContext } from '../theme';
import { ModalAnimation, StackAnimation, defaultHeader, themedHeader } from '../utils/navigation';
// Outside Stack
import OnboardingView from '../views/OnboardingView';
import NewServerView from '../views/NewServerView';
import WorkspaceView from '../views/WorkspaceView';
import LoginView from '../views/LoginView';
......@@ -15,18 +13,14 @@ import ForgotPasswordView from '../views/ForgotPasswordView';
import RegisterView from '../views/RegisterView';
import LegalView from '../views/LegalView';
import AuthenticationWebView from '../views/AuthenticationWebView';
import { ROOT_OUTSIDE } from '../actions/app';
// Outside
const Outside = createStackNavigator();
const _OutsideStack = ({ root }) => {
const _OutsideStack = () => {
const { theme } = React.useContext(ThemeContext);
return (
<Outside.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation }}>
{root === ROOT_OUTSIDE ? (
<Outside.Screen name='OnboardingView' component={OnboardingView} options={OnboardingView.navigationOptions} />
) : null}
<Outside.Screen name='NewServerView' component={NewServerView} options={NewServerView.navigationOptions} />
<Outside.Screen name='WorkspaceView' component={WorkspaceView} options={WorkspaceView.navigationOptions} />
<Outside.Screen name='LoginView' component={LoginView} options={LoginView.navigationOptions} />
......@@ -41,10 +35,6 @@ const mapStateToProps = state => ({
root: state.app.root
});
_OutsideStack.propTypes = {
root: PropTypes.string
};
const OutsideStack = connect(mapStateToProps)(_OutsideStack);
// OutsideStackModal
......
export default {
// ONBOARDING VIEW
ONBOARD_JOIN_A_WORKSPACE: 'onboard_join_a_workspace',
ONBOARD_CREATE_NEW_WORKSPACE: 'onboard_create_new_workspace',
ONBOARD_CREATE_NEW_WORKSPACE_F: 'onboard_create_new_workspace_f',
// NEW SERVER VIEW
NS_CONNECT_TO_WORKSPACE: 'ns_connect_to_workspace',
NS_JOIN_OPEN_WORKSPACE: 'ns_join_open_workspace',
......@@ -78,6 +73,7 @@ export default {
RL_GROUP_CHANNELS_BY_TYPE: 'rl_group_channels_by_type',
RL_GROUP_CHANNELS_BY_FAVORITE: 'rl_group_channels_by_favorite',
RL_GROUP_CHANNELS_BY_UNREAD: 'rl_group_channels_by_unread',
RL_CREATE_NEW_WORKSPACE: 'rl_create_new_workspace',
// QUEUE LIST VIEW
QL_GO_ROOM: 'ql_go_room',
......
import { Dimensions } from 'react-native';
import { isTablet } from './deviceInfo';
const { width, height } = Dimensions.get('window');
const guidelineBaseWidth = isTablet ? 600 : 375;
const guidelineBaseHeight = isTablet ? 800 : 667;
const scale = size => (width / guidelineBaseWidth) * size;
const verticalScale = size => (height / guidelineBaseHeight) * size;
const moderateScale = (size, factor = 0.5) => size + (scale(size) - size) * factor;
// TODO: we need to refactor this
const scale = (size, width) => (width / guidelineBaseWidth) * size;
const verticalScale = (size, height) => (height / guidelineBaseHeight) * size;
const moderateScale = (size, factor = 0.5, width) => size + (scale(size, width) - size) * factor;
export { scale, verticalScale, moderateScale };
......@@ -10,9 +10,7 @@ import Item from './Item';
const styles = StyleSheet.create({
container: {
zIndex: 1,
marginTop: 24,
marginBottom: 32
zIndex: 1
},
inputContainer: {
marginTop: 0,
......
import React from 'react';
import PropTypes from 'prop-types';
import { BackHandler, Keyboard, StyleSheet, Text, View } from 'react-native';
import { Text, Keyboard, StyleSheet, View, BackHandler, Image } from 'react-native';
import { connect } from 'react-redux';
import { Base64 } from 'js-base64';
import parse from 'url-parse';
import { Q } from '@nozbe/watermelondb';
import { TouchableOpacity } from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker';
import UserPreferences from '../../lib/userPreferences';
import EventEmitter from '../../utils/events';
import { selectServerRequest, serverRequest } from '../../actions/server';
import { selectServerRequest, serverRequest, serverFinishAdd as serverFinishAddAction } from '../../actions/server';
import { inviteLinksClear as inviteLinksClearAction } from '../../actions/inviteLinks';
import sharedStyles from '../Styles';
import Button from '../../containers/Button';
......@@ -27,31 +28,38 @@ import database from '../../lib/database';
import { sanitizeLikeString } from '../../lib/database/utils';
import SSLPinning from '../../utils/sslPinning';
import RocketChat from '../../lib/rocketchat';
import { isTablet } from '../../utils/deviceInfo';
import { verticalScale, moderateScale } from '../../utils/scaling';
import { withDimensions } from '../../dimensions';
import ServerInput from './ServerInput';
const styles = StyleSheet.create({
onboardingImage: {
alignSelf: 'center',
resizeMode: 'contain'
},
title: {
...sharedStyles.textBold,
fontSize: 22
letterSpacing: 0,
alignSelf: 'center'
},
subtitle: {
...sharedStyles.textRegular,
alignSelf: 'center'
},
certificatePicker: {
marginBottom: 32,
alignItems: 'center',
justifyContent: 'flex-end'
},
chooseCertificateTitle: {
fontSize: 13,
...sharedStyles.textRegular
},
chooseCertificate: {
fontSize: 13,
...sharedStyles.textSemibold
},
description: {
...sharedStyles.textRegular,
fontSize: 14,
textAlign: 'left',
marginBottom: 24
textAlign: 'center'
},
connectButton: {
marginBottom: 0
......@@ -59,23 +67,22 @@ const styles = StyleSheet.create({
});
class NewServerView extends React.Component {
static navigationOptions = () => ({
title: I18n.t('Workspaces')
});
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string,
connecting: PropTypes.bool.isRequired,
connectServer: PropTypes.func.isRequired,
selectServer: PropTypes.func.isRequired,
adding: PropTypes.bool,
previousServer: PropTypes.string,
inviteLinksClear: PropTypes.func
inviteLinksClear: PropTypes.func,
serverFinishAdd: PropTypes.func
};
constructor(props) {
super(props);
if (!isTablet) {
Orientation.lockToPortrait();
}
this.setHeader();
this.state = {
......@@ -92,25 +99,27 @@ class NewServerView extends React.Component {
this.queryServerHistory();
}
componentDidUpdate(prevProps) {
const { adding } = this.props;
if (prevProps.adding !== adding) {
this.setHeader();
}
}
componentWillUnmount() {
EventEmitter.removeListener('NewServer', this.handleNewServerEvent);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
const { previousServer, serverFinishAdd } = this.props;
if (previousServer) {
serverFinishAdd();
}
}
setHeader = () => {
const { adding, navigation } = this.props;
if (adding) {
navigation.setOptions({
const { previousServer, navigation } = this.props;
if (previousServer) {
return navigation.setOptions({
headerTitle: I18n.t('Workspaces'),
headerLeft: () => <HeaderButton.CloseModal navigation={navigation} onPress={this.close} testID='new-server-view-close' />
});
}
return navigation.setOptions({
headerShown: false
});
};
handleBackPress = () => {
......@@ -273,16 +282,26 @@ class NewServerView extends React.Component {
renderCertificatePicker = () => {
const { certificate } = this.state;
const { theme } = this.props;
const { theme, width, height, previousServer } = this.props;
return (
<View style={styles.certificatePicker}>
<Text style={[styles.chooseCertificateTitle, { color: themes[theme].auxiliaryText }]}>
<View
style={[
styles.certificatePicker,
{
marginBottom: verticalScale(previousServer && !isTablet ? 10 : 30, height)
}
]}>
<Text
style={[
styles.chooseCertificateTitle,
{ color: themes[theme].auxiliaryText, fontSize: moderateScale(13, null, width) }
]}>
{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
</Text>
<TouchableOpacity
onPress={certificate ? this.handleRemove : this.chooseCertificate}
testID='new-server-choose-certificate'>
<Text style={[styles.chooseCertificate, { color: themes[theme].tintColor }]}>
<Text style={[styles.chooseCertificate, { color: themes[theme].tintColor, fontSize: moderateScale(13, null, width) }]}>
{certificate ?? I18n.t('Apply_Your_Certificate')}
</Text>
</TouchableOpacity>
......@@ -291,12 +310,48 @@ class NewServerView extends React.Component {
};
render() {
const { connecting, theme } = this.props;
const { connecting, theme, previousServer, width, height } = this.props;
const { text, connectingOpen, serversHistory } = this.state;
const marginTop = previousServer ? 0 : 35;
return (
<FormContainer theme={theme} testID='new-server-view' keyboardShouldPersistTaps='never'>
<FormContainerInner>
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Join_your_workspace')}</Text>
<Image
style={[
styles.onboardingImage,
{
marginBottom: verticalScale(10, height),
marginTop: isTablet ? 0 : verticalScale(marginTop, height),
width: verticalScale(100, height),
height: verticalScale(100, height)
}
]}
source={require('../../static/images/logo.png')}
fadeDuration={0}
/>
<Text
style={[
styles.title,
{
color: themes[theme].titleText,
fontSize: moderateScale(22, null, width),
marginBottom: verticalScale(8, height)
}
]}>
Rocket.Chat
</Text>
<Text
style={[
styles.subtitle,
{
color: themes[theme].controlText,
fontSize: moderateScale(16, null, width),
marginBottom: verticalScale(30, height)
}
]}>
{I18n.t('Onboarding_subtitle')}
</Text>
<ServerInput
text={text}
theme={theme}
......@@ -312,12 +367,20 @@ class NewServerView extends React.Component {
onPress={this.submit}
disabled={!text || connecting}
loading={!connectingOpen && connecting}
style={styles.connectButton}
style={[styles.connectButton, { marginTop: verticalScale(16, height) }]}
theme={theme}
testID='new-server-view-button'
/>
<OrSeparator theme={theme} />
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>
<Text
style={[
styles.description,
{
color: themes[theme].auxiliaryText,
fontSize: moderateScale(14, null, width),
marginBottom: verticalScale(16, height)
}
]}>
{I18n.t('Onboarding_join_open_description')}
</Text>
<Button
......@@ -339,14 +402,14 @@ class NewServerView extends React.Component {
const mapStateToProps = state => ({
connecting: state.server.connecting,
adding: state.server.adding,
previousServer: state.server.previousServer
});
const mapDispatchToProps = dispatch => ({
connectServer: (...params) => dispatch(serverRequest(...params)),
selectServer: server => dispatch(selectServerRequest(server)),
inviteLinksClear: () => dispatch(inviteLinksClearAction())
inviteLinksClear: () => dispatch(inviteLinksClearAction()),
serverFinishAdd: () => dispatch(serverFinishAddAction())
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewServerView));
export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(NewServerView)));
import React from 'react';
import { Image, Linking, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import Orientation from 'react-native-orientation-locker';
import I18n from '../../i18n';
import Button from '../../containers/Button';
import { isTablet } from '../../utils/deviceInfo';
import { themes } from '../../constants/colors';
import { withTheme } from '../../theme';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
import { events, logEvent } from '../../utils/log';
import styles from './styles';
class OnboardingView extends React.Component {
static navigationOptions = {
headerShown: false
};
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.string
};
constructor(props) {
super(props);
if (!isTablet) {
Orientation.lockToPortrait();
}
}
shouldComponentUpdate(nextProps) {
const { theme } = this.props;
if (theme !== nextProps.theme) {
return true;
}
return false;
}
connectServer = () => {
logEvent(events.ONBOARD_JOIN_A_WORKSPACE);
const { navigation } = this.props;
navigation.navigate('NewServerView');
};
createWorkspace = async () => {
logEvent(events.ONBOARD_CREATE_NEW_WORKSPACE);
try {
await Linking.openURL('https://cloud.rocket.chat/trial');
} catch {
logEvent(events.ONBOARD_CREATE_NEW_WORKSPACE_F);
}
};
render() {
const { theme } = this.props;
return (
<FormContainer theme={theme} testID='onboarding-view'>
<FormContainerInner>
<Image style={styles.onboarding} source={require('../../static/images/logo.png')} fadeDuration={0} />
<Text style={[styles.title, { color: themes[theme].titleText }]}>{I18n.t('Onboarding_title')}</Text>
<Text style={[styles.subtitle, { color: themes[theme].controlText }]}>{I18n.t('Onboarding_subtitle')}</Text>
<Text style={[styles.description, { color: themes[theme].auxiliaryText }]}>{I18n.t('Onboarding_description')}</Text>
<View style={styles.buttonsContainer}>
<Button
title={I18n.t('Onboarding_join_workspace')}
type='primary'
onPress={this.connectServer}
theme={theme}
testID='join-workspace'
/>
<Button
title={I18n.t('Create_a_new_workspace')}
type='secondary'
backgroundColor={themes[theme].chatComponentBackground}
onPress={this.createWorkspace}
theme={theme}
testID='create-workspace-button'
/>
</View>
</FormContainerInner>
</FormContainer>
);
}
}
export default withTheme(OnboardingView);
import { StyleSheet } from 'react-native';
import { moderateScale, verticalScale } from '../../utils/scaling';
import { isTablet } from '../../utils/deviceInfo';