From da173275cef2ed2473c7b3eba13f6759000f8fe4 Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Tue, 12 Jun 2018 22:33:00 -0300
Subject: [PATCH] [NEW] User Profile (#323)

* Drawer layout

* Drawer changes

* Profile

* Profile avatar

* Set language

* Tests

* Custom fields

* Readme updated

* fix invalid user muted value

* Fix for "Cannot add a child that doesn't have a YogaNode to a parent without a measure function! (Trying to add a 'RCTVirtualText' to a 'RCTView')"
---
 README.md                                     |   6 +
 __tests__/__snapshots__/RoomItem.js.snap      |   6 +-
 .../__snapshots__/Storyshots.test.js.snap     |  30 +-
 app/actions/login.js                          |   6 +-
 app/containers/Avatar.js                      |  81 +++-
 app/containers/MessageBox/index.js            |  11 +-
 app/containers/Sidebar.js                     |  49 +-
 app/containers/TextInput.js                   |   8 +-
 app/containers/message/Reply.js               |   7 +-
 app/containers/message/index.js               |   3 +-
 app/containers/routes/AuthRoutes.js           |  29 +-
 app/i18n/locales/en.js                        |  92 +++-
 app/lib/ddp.js                                |   6 +-
 app/lib/realm.js                              |   6 +-
 app/lib/rocketchat.js                         |  60 ++-
 app/sagas/init.js                             |   4 +-
 app/sagas/login.js                            |  11 +
 app/views/ForgotPasswordView.js               |  40 +-
 app/views/LoginSignupView.js                  |  21 +-
 app/views/LoginView.js                        |   2 +-
 app/views/MentionedMessagesView/index.js      |   4 +-
 app/views/PinnedMessagesView/index.js         |   4 +-
 app/views/ProfileView/index.js                | 426 +++++++++++++++++-
 app/views/ProfileView/styles.js               |  24 +
 app/views/RegisterView.js                     |   3 +-
 app/views/RoomActionsView/index.js            |   2 +-
 app/views/RoomActionsView/styles.js           |   7 -
 app/views/RoomFilesView/index.js              |   4 +-
 app/views/RoomInfoEditView/index.js           | 230 +++++-----
 app/views/RoomInfoView/index.js               |  35 +-
 app/views/RoomInfoView/styles.js              |   7 -
 app/views/RoomMembersView/index.js            |  16 +-
 app/views/RoomMembersView/styles.js           |  16 +-
 app/views/RoomView/Header/index.js            |   7 +-
 app/views/RoomView/Header/styles.js           |   7 -
 app/views/RoomView/ListView.js                |   4 +-
 app/views/RoomView/index.js                   |   8 +-
 app/views/RoomsListView/Header/index.js       |   9 +-
 app/views/RoomsListView/Header/styles.js      |   7 -
 app/views/RoomsListView/index.js              |   2 +-
 app/views/SearchMessagesView/index.js         |   4 +-
 app/views/SettingsView/index.js               | 124 ++++-
 app/views/SnippetedMessagesView/index.js      |   4 +-
 app/views/StarredMessagesView/index.js        |   4 +-
 app/views/Styles.js                           |   7 +
 e2e/11-broadcast.spec.js                      |  11 +-
 e2e/12-profile.spec.js                        | 115 +++++
 package-lock.json                             |  38 ++
 package.json                                  |   3 +
 49 files changed, 1279 insertions(+), 331 deletions(-)
 create mode 100644 app/views/ProfileView/styles.js
 create mode 100644 e2e/12-profile.spec.js

diff --git a/README.md b/README.md
index 62d11f3b9..1ee828a97 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,12 @@
 
 **Supported Server Versions:** 0.58.0+ (We are working to support earlier versions)
 
+# Download
+[![Rocket.Chat.ReactNative on Google Play](https://play.google.com/intl/en_us/badges/images/badge_new.png)](https://play.google.com/store/apps/details?id=chat.rocket.reactnative)
+
+Note: If you want to try iOS version, send us an email to testflight@rocket.chat and we'll add you to TestFlight users.
+
+
 # Installing dependencies
 
 Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
diff --git a/__tests__/__snapshots__/RoomItem.js.snap b/__tests__/__snapshots__/RoomItem.js.snap
index 8b1bf18ed..988f57af1 100644
--- a/__tests__/__snapshots__/RoomItem.js.snap
+++ b/__tests__/__snapshots__/RoomItem.js.snap
@@ -585,7 +585,7 @@ exports[`render unread +999 1`] = `
             source={
               Object {
                 "priority": "high",
-                "uri": "/avatar/name",
+                "uri": "/avatar/name?random=0",
               }
             }
             style={
@@ -835,7 +835,7 @@ exports[`render unread 1`] = `
             source={
               Object {
                 "priority": "high",
-                "uri": "/avatar/name",
+                "uri": "/avatar/name?random=0",
               }
             }
             style={
@@ -1085,7 +1085,7 @@ exports[`renders correctly 1`] = `
             source={
               Object {
                 "priority": "high",
-                "uri": "/avatar/name",
+                "uri": "/avatar/name?random=0",
               }
             }
             style={
diff --git a/__tests__/__snapshots__/Storyshots.test.js.snap b/__tests__/__snapshots__/Storyshots.test.js.snap
index 7dc94d89e..86fba1e32 100644
--- a/__tests__/__snapshots__/Storyshots.test.js.snap
+++ b/__tests__/__snapshots__/Storyshots.test.js.snap
@@ -62,7 +62,7 @@ exports[`Storyshots Avatar avatar 1`] = `
           source={
             Object {
               "priority": "high",
-              "uri": "/avatar/test",
+              "uri": "/avatar/test?random=0",
             }
           }
           style={
@@ -136,7 +136,7 @@ exports[`Storyshots Avatar avatar 1`] = `
           source={
             Object {
               "priority": "high",
-              "uri": "/avatar/aa",
+              "uri": "/avatar/aa?random=0",
             }
           }
           style={
@@ -210,7 +210,7 @@ exports[`Storyshots Avatar avatar 1`] = `
           source={
             Object {
               "priority": "high",
-              "uri": "/avatar/bb",
+              "uri": "/avatar/bb?random=0",
             }
           }
           style={
@@ -284,7 +284,7 @@ exports[`Storyshots Avatar avatar 1`] = `
           source={
             Object {
               "priority": "high",
-              "uri": "/avatar/test",
+              "uri": "/avatar/test?random=0",
             }
           }
           style={
@@ -393,7 +393,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/rocket.cat",
+                  "uri": "/avatar/rocket.cat?random=0",
                 }
               }
               style={
@@ -615,7 +615,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/rocket.cat",
+                  "uri": "/avatar/rocket.cat?random=0",
                 }
               }
               style={
@@ -841,7 +841,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/rocket.cat",
+                  "uri": "/avatar/rocket.cat?random=0",
                 }
               }
               style={
@@ -1086,7 +1086,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
                 }
               }
               style={
@@ -1335,7 +1335,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
                 }
               }
               style={
@@ -1580,7 +1580,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
                 }
               }
               style={
@@ -1825,7 +1825,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
                 }
               }
               style={
@@ -2070,7 +2070,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries",
+                  "uri": "/avatar/Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries?random=0",
                 }
               }
               style={
@@ -2315,7 +2315,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/W",
+                  "uri": "/avatar/W?random=0",
                 }
               }
               style={
@@ -2537,7 +2537,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/WW",
+                  "uri": "/avatar/WW?random=0",
                 }
               }
               style={
@@ -2759,7 +2759,7 @@ exports[`Storyshots Channel Cell Direct Messages 1`] = `
               source={
                 Object {
                   "priority": "high",
-                  "uri": "/avatar/",
+                  "uri": "/avatar/?random=0",
                 }
               }
               style={
diff --git a/app/actions/login.js b/app/actions/login.js
index 875c46706..e9e332b36 100644
--- a/app/actions/login.js
+++ b/app/actions/login.js
@@ -120,8 +120,10 @@ export function forgotPasswordFailure(err) {
 
 export function setUser(action) {
 	return {
-		type: types.USER.SET,
-		...action
+		// do not change this params order
+		// since we use spread operator, sometimes `type` is overriden
+		...action,
+		type: types.USER.SET
 	};
 }
 
diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js
index 3e49a100e..42b52c794 100644
--- a/app/containers/Avatar.js
+++ b/app/containers/Avatar.js
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
 import { StyleSheet, Text, View, ViewPropTypes } from 'react-native';
 import FastImage from 'react-native-fast-image';
 import avatarInitialsAndColor from '../utils/avatarInitialsAndColor';
+import database from '../lib/realm';
 
 const styles = StyleSheet.create({
 	iconContainer: {
@@ -26,17 +27,78 @@ export default class Avatar extends React.PureComponent {
 	static propTypes = {
 		style: ViewPropTypes.style,
 		baseUrl: PropTypes.string,
-		text: PropTypes.string.isRequired,
+		text: PropTypes.string,
 		avatar: PropTypes.string,
 		size: PropTypes.number,
 		borderRadius: PropTypes.number,
 		type: PropTypes.string,
-		children: PropTypes.object
+		children: PropTypes.object,
+		forceInitials: PropTypes.bool
 	};
-	state = { showInitials: true };
+	static defaultProps = {
+		text: '',
+		size: 25,
+		type: 'd',
+		borderRadius: 2,
+		forceInitials: false
+	};
+	state = { showInitials: true, user: {} };
+
+	componentDidMount() {
+		const { text, type } = this.props;
+		if (type === 'd') {
+			this.users = this.userQuery(text);
+			this.users.addListener(this.update);
+			this.update();
+		}
+	}
+
+	componentWillReceiveProps(nextProps) {
+		if (nextProps.text !== this.props.text && nextProps.type === 'd') {
+			if (this.users) {
+				this.users.removeAllListeners();
+			}
+			this.users = this.userQuery(nextProps.text);
+			this.users.addListener(this.update);
+			this.update();
+		}
+	}
+
+	componentWillUnmount() {
+		if (this.users) {
+			this.users.removeAllListeners();
+		}
+	}
+
+	get avatarVersion() {
+		return (this.state.user && this.state.user.avatarVersion) || 0;
+	}
+
+	/** FIXME: Workaround
+	 * While we don't have containers/components structure, this is breaking tests.
+	 * In that case, avatar would be a component, it would receive an `avatarVersion` param
+	 * and we would have a avatar container in charge of making queries.
+	 * Also, it would make possible to write unit tests like these.
+	*/
+	userQuery = (username) => {
+		if (database && database.databases && database.databases.activeDB) {
+			return database.objects('users').filtered('username = $0', username);
+		}
+		return {
+			addListener: () => {},
+			removeAllListeners: () => {}
+		};
+	}
+
+	update = () => {
+		if (this.users.length) {
+			this.setState({ user: this.users[0] });
+		}
+	}
+
 	render() {
 		const {
-			text = '', size = 25, baseUrl, borderRadius = 2, style, avatar, type = 'd'
+			text, size, baseUrl, borderRadius, style, avatar, type, forceInitials
 		} = this.props;
 		const { initials, color } = avatarInitialsAndColor(`${ text }`);
 
@@ -60,9 +122,9 @@ export default class Avatar extends React.PureComponent {
 
 		let image;
 
-		if (type === 'd') {
-			const uri = avatar || `${ baseUrl }/avatar/${ text }`;
-			image = uri && (
+		if (type === 'd' && !forceInitials) {
+			const uri = avatar || `${ baseUrl }/avatar/${ text }?random=${ this.avatarVersion }`;
+			image = uri ? (
 				<FastImage
 					style={[styles.avatar, avatarStyle]}
 					source={{
@@ -70,18 +132,19 @@ export default class Avatar extends React.PureComponent {
 						priority: FastImage.priority.high
 					}}
 				/>
-			);
+			) : null;
 		}
 
 		return (
 			<View style={[styles.iconContainer, iconContainerStyle, style]}>
-				{this.state.showInitials &&
+				{this.state.showInitials ?
 					<Text
 						style={[styles.avatarInitials, avatarInitialsStyle]}
 						allowFontScaling={false}
 					>
 						{initials}
 					</Text>
+					: null
 				}
 				{image}
 				{this.props.children}
diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js
index fea1c26b6..25925ad5f 100644
--- a/app/containers/MessageBox/index.js
+++ b/app/containers/MessageBox/index.js
@@ -172,7 +172,7 @@ export default class MessageBox extends React.PureComponent {
 			maxWidth: 1960,
 			quality: 0.8
 		};
-		ImagePicker.showImagePicker(options, (response) => {
+		ImagePicker.showImagePicker(options, async(response) => {
 			if (response.didCancel) {
 				console.warn('User cancelled image picker');
 			} else if (response.error) {
@@ -185,7 +185,11 @@ export default class MessageBox extends React.PureComponent {
 					// description: '',
 					store: 'Uploads'
 				};
-				RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
+				try {
+					await RocketChat.sendFileMessage(this.props.rid, fileInfo, response.data);
+				} catch (e) {
+					log('addFile', e);
+				}
 			}
 		});
 	}
@@ -459,6 +463,7 @@ export default class MessageBox extends React.PureComponent {
 							style={{ margin: 8 }}
 							text={item.username || item.name}
 							size={30}
+							type={item.username ? 'd' : 'c'}
 						/>,
 						<Text key='mention-item-name'>{ item.username || item.name }</Text>
 					]
@@ -477,7 +482,7 @@ export default class MessageBox extends React.PureComponent {
 					style={styles.mentionList}
 					data={mentions}
 					renderItem={({ item }) => this.renderMentionItem(item)}
-					keyExtractor={item => item._id || item}
+					keyExtractor={item => item._id || item.username || item}
 					keyboardShouldPersistTaps='always'
 				/>
 			</View>
diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js
index e2acce4c7..ea6cc2f9d 100644
--- a/app/containers/Sidebar.js
+++ b/app/containers/Sidebar.js
@@ -95,26 +95,20 @@ export default class Sidebar extends Component {
 		super(props);
 		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() {
-		database.databases.serversDB.addListener('change', this.updateState);
 		this.setState(this.getState());
+		this.setStatus();
+		database.databases.serversDB.addListener('change', this.updateState);
+	}
+
+	componentWillReceiveProps(nextProps) {
+		if (nextProps.user && this.props.user && this.props.user.language !== nextProps.user.language) {
+			this.setStatus();
+		}
 	}
 
 	componentWillUnmount() {
@@ -126,6 +120,26 @@ export default class Sidebar extends Component {
 		this.closeDrawer();
 	}
 
+	setStatus = () => {
+		setTimeout(() => {
+			this.setState({
+				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')
+				}]
+			});
+		});
+	}
+
 	getState = () => ({
 		servers: database.databases.serversDB.objects('servers')
 	})
@@ -153,6 +167,8 @@ export default class Sidebar extends Component {
 		const { navigate } = this.props.navigation;
 		if (!this.isRouteFocused(route)) {
 			navigate(route);
+		} else {
+			this.closeDrawer();
 		}
 	}
 
@@ -211,6 +227,7 @@ export default class Sidebar extends Component {
 				this.toggleServers();
 				if (this.props.server !== item.id) {
 					this.props.selectServer(item.id);
+					this.props.navigation.navigate('RoomsList');
 				}
 			},
 			testID: `sidebar-${ item.id }`
@@ -324,8 +341,8 @@ export default class Sidebar extends Component {
 
 					{this.renderSeparator('separator-header')}
 
-					{!this.state.showServers && this.renderNavigation()}
-					{this.state.showServers && this.renderServers()}
+					{!this.state.showServers ? this.renderNavigation() : null}
+					{this.state.showServers ? this.renderServers() : null}
 				</SafeAreaView>
 			</ScrollView>
 		);
diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js
index 885b93022..7e85e553c 100644
--- a/app/containers/TextInput.js
+++ b/app/containers/TextInput.js
@@ -105,7 +105,7 @@ export default class RCTextInput extends React.PureComponent {
 		const { showPassword } = this.state;
 		return (
 			<View style={[styles.inputContainer, containerStyle]}>
-				{label && <Text contentDescription={null} accessibilityLabel={null} style={[styles.label, error.error && styles.labelError]}>{label}</Text> }
+				{label ? <Text contentDescription={null} accessibilityLabel={null} style={[styles.label, error.error && styles.labelError]}>{label}</Text> : null }
 				<View style={styles.wrap}>
 					<TextInput
 						style={[
@@ -126,10 +126,10 @@ export default class RCTextInput extends React.PureComponent {
 						contentDescription={placeholder}
 						{...inputProps}
 					/>
-					{iconLeft && this.iconLeft(iconLeft)}
-					{secureTextEntry && this.iconPassword(showPassword ? 'eye-off' : 'eye')}
+					{iconLeft ? this.iconLeft(iconLeft) : null}
+					{secureTextEntry ? this.iconPassword(showPassword ? 'eye-off' : 'eye') : null}
 				</View>
-				{error.error && <Text style={sharedStyles.error}>{error.reason}</Text>}
+				{error.error ? <Text style={sharedStyles.error}>{error.reason}</Text> : null}
 			</View>
 		);
 	}
diff --git a/app/containers/message/Reply.js b/app/containers/message/Reply.js
index 592482bb6..1b42c73c3 100644
--- a/app/containers/message/Reply.js
+++ b/app/containers/message/Reply.js
@@ -78,7 +78,6 @@ const Reply = ({ attachment, timeFormat }) => {
 			<Avatar
 				text={attachment.author_name}
 				size={16}
-				avatar={attachment.author_icon}
 			/>
 		);
 	};
@@ -136,7 +135,11 @@ const Reply = ({ attachment, timeFormat }) => {
 				{renderTitle()}
 				{renderText()}
 				{renderFields()}
-				{attachment.attachments && attachment.attachments.map(attach => <Reply key={attach.text} attachment={attach} timeFormat={timeFormat} />)}
+				{attachment.attachments ?
+					attachment.attachments
+						.map(attach => <Reply key={attach.text} attachment={attach} timeFormat={timeFormat} />)
+					: null
+				}
 			</View>
 		</TouchableOpacity>
 	);
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 54c31aa49..46152b204 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -358,13 +358,14 @@ export default class Message extends React.Component {
 							{this.renderBroadcastReply()}
 						</View>
 					</View>
-					{this.state.reactionsModal &&
+					{this.state.reactionsModal ?
 						<ReactionsModal
 							isVisible={this.state.reactionsModal}
 							onClose={this.onClose}
 							reactions={item.reactions}
 							user={this.props.user}
 						/>
+						: null
 					}
 				</View>
 			</Touch>
diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js
index 3ded765b3..5361dd2e3 100644
--- a/app/containers/routes/AuthRoutes.js
+++ b/app/containers/routes/AuthRoutes.js
@@ -1,5 +1,5 @@
 import React from 'react';
-import { Platform } from 'react-native';
+import { Platform, TouchableOpacity } from 'react-native';
 import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
 import Icon from 'react-native-vector-icons/MaterialIcons';
 
@@ -22,6 +22,7 @@ import RoomInfoEditView from '../../views/RoomInfoEditView';
 import ProfileView from '../../views/ProfileView';
 import SettingsView from '../../views/SettingsView';
 import I18n from '../../i18n';
+import sharedStyles from '../../views/Styles';
 
 const headerTintColor = '#292E35';
 
@@ -132,12 +133,24 @@ const AuthRoutes = createStackNavigator(
 	}
 );
 
+const MenuButton = ({ navigation, testID }) => (
+	<TouchableOpacity
+		style={sharedStyles.headerButton}
+		onPress={navigation.toggleDrawer}
+		accessibilityLabel={I18n.t('Toggle_Drawer')}
+		accessibilityTraits='button'
+		testID={testID}
+	>
+		<Icon name='menu' size={30} color='#292E35' />
+	</TouchableOpacity>
+);
+
 const Routes = createDrawerNavigator(
 	{
 		Chats: {
 			screen: AuthRoutes,
 			navigationOptions: {
-				drawerLabel: 'Chats',
+				drawerLabel: I18n.t('Chats'),
 				drawerIcon: () => <Icon name='chat-bubble' size={20} />
 			}
 		},
@@ -146,9 +159,9 @@ const Routes = createDrawerNavigator(
 				ProfileView: {
 					screen: ProfileView,
 					navigationOptions: ({ navigation }) => ({
-						title: 'Profile',
+						title: I18n.t('Profile'),
 						headerTintColor: '#292E35',
-						headerLeft: <Icon name='menu' size={30} color='#292E35' onPress={() => navigation.toggleDrawer()} /> // TODO: refactor
+						headerLeft: <MenuButton navigation={navigation} testID='profile-view-sidebar' />
 					})
 				}
 			})
@@ -158,9 +171,9 @@ const Routes = createDrawerNavigator(
 				SettingsView: {
 					screen: SettingsView,
 					navigationOptions: ({ navigation }) => ({
-						title: 'Settings',
+						title: I18n.t('Settings'),
 						headerTintColor: '#292E35',
-						headerLeft: <Icon name='menu' size={30} color='#292E35' onPress={() => navigation.toggleDrawer()} /> // TODO: refactor
+						headerLeft: <MenuButton navigation={navigation} testID='settings-view-sidebar' />
 					})
 				}
 			})
@@ -168,9 +181,7 @@ const Routes = createDrawerNavigator(
 	},
 	{
 		contentComponent: Sidebar,
-		navigationOptions: {
-			drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked'
-		},
+		drawerLockMode: Platform.OS === 'ios' ? 'locked-closed' : 'unlocked',
 		initialRouteName: 'Chats',
 		backBehavior: 'initialRoute'
 	}
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index c66849ca2..a54dcdceb 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -1,6 +1,80 @@
 export default {
 	'1_online_member': '1 online member',
 	'1_person_reacted': '1 person reacted',
+	'error-action-not-allowed': '{{action}} is not allowed',
+	'error-application-not-found': 'Application not found',
+	'error-archived-duplicate-name': 'There\'s an archived channel with name {{room_name}}',
+	'error-avatar-invalid-url': 'Invalid avatar URL: {{url}}',
+	'error-avatar-url-handling': 'Error while handling avatar setting from a URL ({{url}}) for {{username}}',
+	'error-cant-invite-for-direct-room': 'Can\'t invite user to direct rooms',
+	'error-could-not-change-email': 'Could not change email',
+	'error-could-not-change-name': 'Could not change name',
+	'error-could-not-change-username': 'Could not change username',
+	'error-delete-protected-role': 'Cannot delete a protected role',
+	'error-department-not-found': 'Department not found',
+	'error-direct-message-file-upload-not-allowed': 'File sharing not allowed in direct messages',
+	'error-duplicate-channel-name': 'A channel with name {{channel_name}} exists',
+	'error-email-domain-blacklisted': 'The email domain is blacklisted',
+	'error-email-send-failed': 'Error trying to send email: {{message}}',
+	'error-field-unavailable': '{{field}} is already in use :(',
+	'error-file-too-large': 'File is too large',
+	'error-importer-not-defined': 'The importer was not defined correctly, it is missing the Import class.',
+	'error-input-is-not-a-valid-field': '{{input}} is not a valid {{field}}',
+	'error-invalid-actionlink': 'Invalid action link',
+	'error-invalid-arguments': 'Invalid arguments',
+	'error-invalid-asset': 'Invalid asset',
+	'error-invalid-channel': 'Invalid channel.',
+	'error-invalid-channel-start-with-chars': 'Invalid channel. Start with @ or #',
+	'error-invalid-custom-field': 'Invalid custom field',
+	'error-invalid-custom-field-name': 'Invalid custom field name. Use only letters, numbers, hyphens and underscores.',
+	'error-invalid-date': 'Invalid date provided.',
+	'error-invalid-description': 'Invalid description',
+	'error-invalid-domain': 'Invalid domain',
+	'error-invalid-email': 'Invalid email {{emai}}',
+	'error-invalid-email-address': 'Invalid email address',
+	'error-invalid-file-height': 'Invalid file height',
+	'error-invalid-file-type': 'Invalid file type',
+	'error-invalid-file-width': 'Invalid file width',
+	'error-invalid-from-address': 'You informed an invalid FROM address.',
+	'error-invalid-integration': 'Invalid integration',
+	'error-invalid-message': 'Invalid message',
+	'error-invalid-method': 'Invalid method',
+	'error-invalid-name': 'Invalid name',
+	'error-invalid-password': 'Invalid password',
+	'error-invalid-redirectUri': 'Invalid redirectUri',
+	'error-invalid-role': 'Invalid role',
+	'error-invalid-room': 'Invalid room',
+	'error-invalid-room-name': '{{room_name}} is not a valid room name',
+	'error-invalid-room-type': '{{type}} is not a valid room type.',
+	'error-invalid-settings': 'Invalid settings provided',
+	'error-invalid-subscription': 'Invalid subscription',
+	'error-invalid-token': 'Invalid token',
+	'error-invalid-triggerWords': 'Invalid triggerWords',
+	'error-invalid-urls': 'Invalid URLs',
+	'error-invalid-user': 'Invalid user',
+	'error-invalid-username': 'Invalid username',
+	'error-invalid-webhook-response': 'The webhook URL responded with a status other than 200',
+	'error-message-deleting-blocked': 'Message deleting is blocked',
+	'error-message-editing-blocked': 'Message editing is blocked',
+	'error-message-size-exceeded': 'Message size exceeds Message_MaxAllowedSize',
+	'error-missing-unsubscribe-link': 'You must provide the [unsubscribe] link.',
+	'error-no-tokens-for-this-user': 'There are no tokens for this user',
+	'error-not-allowed': 'Not allowed',
+	'error-not-authorized': 'Not authorized',
+	'error-push-disabled': 'Push is disabled',
+	'error-remove-last-owner': 'This is the last owner. Please set a new owner before removing this one.',
+	'error-role-in-use': 'Cannot delete role because it\'s in use',
+	'error-role-name-required': 'Role name is required',
+	'error-the-field-is-required': 'The field {{field}} is required.',
+	'error-too-many-requests': 'Error, too many requests. Please slow down. You must wait {{seconds}} seconds before trying again.',
+	'error-user-is-not-activated': 'User is not activated',
+	'error-user-has-no-roles': 'User has no roles',
+	'error-user-limit-exceeded': 'The number of users you are trying to invite to #channel_name exceeds the limit set by the administrator',
+	'error-user-not-in-room': 'User is not in this room',
+	'error-user-registration-custom-field': 'error-user-registration-custom-field',
+	'error-user-registration-disabled': 'User registration is disabled',
+	'error-user-registration-secret': 'User registration is only allowed via Secret URL',
+	'error-you-are-last-owner': 'You are the last owner. Please set new owner before leaving the room.',
 	Actions: 'Actions',
 	Add_Reaction: 'Add Reaction',
 	Add_Server: 'Add Server',
@@ -21,6 +95,8 @@ export default {
 	Are_you_sure_question_mark: 'Are you sure?',
 	Are_you_sure_you_want_to_leave_the_room: 'Are you sure you want to leave the room {{room}}?',
 	Authenticating: 'Authenticating',
+	Avatar_changed_successfully: 'Avatar changed successfully!',
+	Avatar_Url: 'Avatar URL',
 	Away: 'Away',
 	Block_user: 'Block user',
 	Broadcast_channel_Description: 'Only authorized users can write new messages, but the other users will be able to reply',
@@ -30,6 +106,7 @@ export default {
 	Cancel_editing: 'Cancel editing',
 	Cancel_recording: 'Cancel recording',
 	Cancel: 'Cancel',
+	changing_avatar: 'changing avatar',
 	Channel_Name: 'Channel Name',
 	Chats: 'Chats',
 	Close_emoji_selector: 'Close emoji selector',
@@ -60,6 +137,7 @@ export default {
 	Everyone_can_access_this_channel: 'Everyone can access this channel',
 	Files: 'Files',
 	Finish_recording: 'Finish recording',
+	For_your_security_you_must_enter_your_current_password_to_continue: 'For your security, you must enter your current password to continue',
 	Forgot_my_password: 'Forgot my password',
 	Forgot_password_If_this_email_is_registered: 'If this email is registered, we\'ll send instructions on how to reset your password. If you do not receive an email shortly, please come back and try again.',
 	Forgot_password: 'Forgot password',
@@ -71,6 +149,7 @@ export default {
 	is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
 	is_typing: 'is typing',
 	Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
+	Language: 'Language',
 	last_message: 'last message',
 	Leave_channel: 'Leave channel',
 	leave: 'leave',
@@ -95,6 +174,7 @@ export default {
 	Name: 'Name',
 	New_in_RocketChat_question_mark: 'New in Rocket.Chat?',
 	New_Message: 'New Message',
+	New_Password: 'New Password',
 	New_Server: 'New Server',
 	No_files: 'No files',
 	No_mentioned_messages: 'No mentioned messages',
@@ -121,9 +201,12 @@ export default {
 	Pinned_Messages: 'Pinned Messages',
 	pinned: 'pinned',
 	Pinned: 'Pinned',
+	Please_enter_your_password: 'Please enter your password',
+	Preferences_saved: 'Preferences saved!',
 	Privacy_Policy: ' Privacy Policy',
 	Private_Channel: 'Private Channel',
 	Private: 'Private',
+	Profile_saved_successfully: 'Profile saved successfully!',
 	Profile: 'Profile',
 	Public_Channel: 'Public Channel',
 	Public: 'Public',
@@ -151,8 +234,14 @@ export default {
 	Room_Members: 'Room Members',
 	Room_name_changed: 'Room name changed to: {{name}} by {{userBy}}',
 	SAVE: 'SAVE',
+	Save_Changes: 'Save Changes',
+	Save: 'Save',
+	saving_preferences: 'saving preferences',
+	saving_profile: 'saving profile',
+	saving_settings: 'saving settings',
 	Search_Messages: 'Search Messages',
 	Search: 'Search',
+	Select_Avatar: 'Select Avatar',
 	Select_Users: 'Select Users',
 	Send_audio_message: 'Send audio message',
 	Send_message: 'Send message',
@@ -177,10 +266,11 @@ export default {
 	tap_to_change_status: 'tap to change status',
 	Tap_to_view_servers_list: 'Tap to view servers list',
 	Terms_of_Service: ' Terms of Service ',
-	There_was_an_error_while_saving_settings: 'There was an error while saving settings!',
+	There_was_an_error_while_action: 'There was an error while {{action}}!',
 	This_room_is_blocked: 'This room is blocked',
 	This_room_is_read_only: 'This room is read only',
 	Timezone: 'Timezone',
+	Toggle_Drawer: 'Toggle_Drawer',
 	topic: 'topic',
 	Topic: 'Topic',
 	Type_the_channel_name_here: 'Type the channel name here',
diff --git a/app/lib/ddp.js b/app/lib/ddp.js
index 33e856085..bf4dba72c 100644
--- a/app/lib/ddp.js
+++ b/app/lib/ddp.js
@@ -144,9 +144,11 @@ export default class Socket extends EventEmitter {
 		try {
 			this.emit('login', params);
 			const result = await this.call('login', params);
-			this._login = { resume: result.token, ...result };
+			// this._login = { resume: result.token, ...result };
+			this._login = { resume: result.token, ...result, ...params };
 			this._logged = true;
-			this.emit('logged', result);
+			// this.emit('logged', result);
+			this.emit('logged', this._login);
 			return result;
 		} catch (err) {
 			const error = { ...err };
diff --git a/app/lib/realm.js b/app/lib/realm.js
index 1d8aedde8..ee7b84640 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -106,11 +106,11 @@ const subscriptionSchema = {
 
 const usersSchema = {
 	name: 'users',
-	primaryKey: '_id',
+	primaryKey: 'username',
 	properties: {
-		_id: 'string',
 		username: 'string',
-		name: { type: 'string', optional: true }
+		name: { type: 'string', optional: true },
+		avatarVersion: { type: 'int', optional: true }
 	}
 };
 
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index b01774a00..420768755 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -91,10 +91,7 @@ const RocketChat = {
 		this.activeUsers = this.activeUsers || {};
 		const { user } = reduxStore.getState().login;
 
-		if (user && user.id === ddpMessage.id) {
-			if (!ddpMessage.fields) {
-				reduxStore.dispatch(setUser({ status: 'offline' }));
-			}
+		if (ddpMessage.fields && user && user.id === ddpMessage.id) {
 			reduxStore.dispatch(setUser(ddpMessage.fields));
 		}
 
@@ -107,9 +104,14 @@ const RocketChat = {
 			reduxStore.dispatch(setActiveUser(this.activeUsers));
 			this._setUserTimer = null;
 			return this.activeUsers = {};
-		}, 1000);
+		}, 3000);
 
-		this.activeUsers[ddpMessage.id] = ddpMessage.fields;
+		const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
+		if (!ddpMessage.fields) {
+			this.activeUsers[ddpMessage.id] = {};
+		} else {
+			this.activeUsers[ddpMessage.id] = { ...this.activeUsers[ddpMessage.id], ...activeUser, ...ddpMessage.fields };
+		}
 	},
 	async loginSuccess(user) {
 		try {
@@ -122,15 +124,11 @@ const RocketChat = {
 			// call /me only one time
 			if (!user.username) {
 				const me = await this.me({ token: user.token, userId: user.id });
-				// eslint-disable-next-line
-				user.username = me.username;
+				user = { ...user, ...me };
 			}
 			if (user.username) {
 				const userInfo = await this.userInfo({ token: user.token, userId: user.id });
-				user.username = userInfo.user.username;
-				if (userInfo.user.roles) {
-					user.roles = userInfo.user.roles;
-				}
+				user = { ...user, ...userInfo.user };
 			}
 			return reduxStore.dispatch(loginSuccess(user));
 		} catch (e) {
@@ -163,7 +161,10 @@ const RocketChat = {
 				this.getRooms().catch(e => log('logged getRooms', e));
 				this.loginSuccess(user);
 			}));
-			this.ddp.once('logged', protectedFunction(({ id }) => { this.subscribeRooms(id); }));
+			this.ddp.once('logged', protectedFunction(({ id }) => {
+				this.subscribeRooms(id);
+				this.ddp.subscribe('stream-notify-logged', 'updateAvatar', false);
+			}));
 
 			this.ddp.on('disconnected', protectedFunction(() => {
 				reduxStore.dispatch(disconnect());
@@ -184,6 +185,24 @@ const RocketChat = {
 				return reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
 			}));
 
+			this.ddp.on('stream-notify-logged', (ddpMessage) => {
+				// this entire logic needs a better solution
+				// we're using it only because our image cache lib doesn't support clear cache
+				if (ddpMessage.fields && ddpMessage.fields.eventName === 'updateAvatar') {
+					const { args } = ddpMessage.fields;
+					database.write(() => {
+						args.forEach((arg) => {
+							const user = database.objects('users').filtered('username = $0', arg.username);
+							if (!user.length) {
+								database.create('users', { username: arg.username, avatarVersion: 0 });
+							} else {
+								user[0].avatarVersion += 1;
+							}
+						});
+					});
+				}
+			});
+
 			// this.ddp.on('stream-notify-user', protectedFunction((ddpMessage) => {
 			// 	console.warn('rc.stream-notify-user')
 			// 	const [type, data] = ddpMessage.fields.args;
@@ -804,6 +823,12 @@ const RocketChat = {
 	saveRoomSettings(rid, params) {
 		return call('saveRoomSettings', rid, params);
 	},
+	saveUserProfile(params, customFields) {
+		return call('saveUserProfile', params, customFields);
+	},
+	saveUserPreferences(params) {
+		return call('saveUserPreferences', params);
+	},
 	saveNotificationSettings(rid, param, value) {
 		return call('saveNotificationSettings', rid, param, value);
 	},
@@ -836,6 +861,15 @@ const RocketChat = {
 				.some(item => mergedRoles.indexOf(item) !== -1);
 			return result;
 		}, {});
+	},
+	getAvatarSuggestion() {
+		return call('getAvatarSuggestion');
+	},
+	resetAvatar() {
+		return call('resetAvatar');
+	},
+	setAvatarFromService({ data, contentType = '', service = null }) {
+		return call('setAvatarFromService', data, contentType, service);
 	}
 };
 
diff --git a/app/sagas/init.js b/app/sagas/init.js
index b7227e3fd..d911923af 100644
--- a/app/sagas/init.js
+++ b/app/sagas/init.js
@@ -20,8 +20,8 @@ const restore = function* restore() {
 			yield put(setServer(currentServer));
 
 			const login = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`);
-			if (login && login.user) {
-				yield put(setUser(login.user));
+			if (login) {
+				yield put(setUser(JSON.parse(login)));
 			}
 		}
 
diff --git a/app/sagas/login.js b/app/sagas/login.js
index 92602eb41..495b4ed5b 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -20,6 +20,7 @@ import {
 import RocketChat from '../lib/rocketchat';
 import * as NavigationService from '../containers/routes/NavigationService';
 import log from '../utils/log';
+import I18n from '../i18n';
 
 const getUser = state => state.login;
 const getServer = state => state.server.server;
@@ -170,6 +171,15 @@ const watchLoginOpen = function* watchLoginOpen() {
 	}
 };
 
+// eslint-disable-next-line require-yield
+const handleSetUser = function* handleSetUser(params) {
+	const [server, user] = yield all([select(getServer), select(getUser)]);
+	if (params.language) {
+		I18n.locale = params.language;
+	}
+	yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user));
+};
+
 const root = function* root() {
 	// yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
 	// yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
@@ -184,5 +194,6 @@ const root = function* root() {
 	yield takeLatest(types.LOGOUT, handleLogout);
 	yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest);
 	yield takeLatest(types.LOGIN.OPEN, watchLoginOpen);
+	yield takeLatest(types.USER.SET, handleSetUser);
 };
 export default root;
diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js
index 09d317095..77d4666d4 100644
--- a/app/views/ForgotPasswordView.js
+++ b/app/views/ForgotPasswordView.js
@@ -78,29 +78,27 @@ export default class ForgotPasswordView extends LoggedView {
 				<ScrollView {...scrollPersistTaps} contentContainerStyle={styles.containerScrollView}>
 					<SafeAreaView testID='forgot-password-view'>
 						<View style={styles.loginView}>
-							<View style={styles.formContainer}>
-								<TextInput
-									inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}}
-									label={I18n.t('Email')}
-									placeholder={I18n.t('Email')}
-									keyboardType='email-address'
-									returnKeyType='next'
-									onChangeText={email => this.validate(email)}
-									onSubmitEditing={() => this.resetPassword()}
-									testID='forgot-password-view-email'
-								/>
-
-								<View style={styles.alignItemsFlexStart}>
-									<Button
-										title={I18n.t('Reset_password')}
-										type='primary'
-										onPress={this.resetPassword}
-										testID='forgot-password-view-submit'
-									/>
-								</View>
+							<TextInput
+								inputStyle={this.state.invalidEmail ? { borderColor: 'red' } : {}}
+								label={I18n.t('Email')}
+								placeholder={I18n.t('Email')}
+								keyboardType='email-address'
+								returnKeyType='next'
+								onChangeText={email => this.validate(email)}
+								onSubmitEditing={() => this.resetPassword()}
+								testID='forgot-password-view-email'
+							/>
 
-								{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
+							<View style={styles.alignItemsFlexStart}>
+								<Button
+									title={I18n.t('Reset_password')}
+									type='primary'
+									onPress={this.resetPassword}
+									testID='forgot-password-view-submit'
+								/>
 							</View>
+
+							{this.props.login.failure ? <Text style={styles.error}>{this.props.login.error.reason}</Text> : null}
 							<Loading visible={this.props.login.isFetching} />
 						</View>
 					</SafeAreaView>
diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js
index 2cd391dcc..c9a55546d 100644
--- a/app/views/LoginSignupView.js
+++ b/app/views/LoginSignupView.js
@@ -209,61 +209,68 @@ export default class LoginSignupView extends LoggedView {
 					{I18n.t('Or_continue_using_social_accounts')}
 				</Text>
 				<View style={sharedStyles.loginOAuthButtons} key='services'>
-					{this.props.Accounts_OAuth_Facebook && this.props.services.facebook &&
+					{this.props.Accounts_OAuth_Facebook && this.props.services.facebook ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.facebookButton]}
 							onPress={this.onPressFacebook}
 						>
 							<Icon name='facebook' size={20} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
-					{this.props.Accounts_OAuth_Github && this.props.services.github &&
+					{this.props.Accounts_OAuth_Github && this.props.services.github ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.githubButton]}
 							onPress={this.onPressGithub}
 						>
 							<Icon name='github' size={20} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
-					{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab &&
+					{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.gitlabButton]}
 							onPress={this.onPressGitlab}
 						>
 							<Icon name='gitlab' size={20} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
-					{this.props.Accounts_OAuth_Google && this.props.services.google &&
+					{this.props.Accounts_OAuth_Google && this.props.services.google ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.googleButton]}
 							onPress={this.onPressGoogle}
 						>
 							<Icon name='google' size={20} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
-					{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin &&
+					{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.linkedinButton]}
 							onPress={this.onPressLinkedin}
 						>
 							<Icon name='linkedin' size={20} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
-					{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] &&
+					{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.meteorButton]}
 							onPress={this.onPressMeteor}
 						>
 							<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
-					{this.props.Accounts_OAuth_Twitter && this.props.services.twitter &&
+					{this.props.Accounts_OAuth_Twitter && this.props.services.twitter ?
 						<TouchableOpacity
 							style={[sharedStyles.oauthButton, sharedStyles.twitterButton]}
 							onPress={this.onPressTwitter}
 						>
 							<Icon name='twitter' size={20} color='#ffffff' />
 						</TouchableOpacity>
+						: null
 					}
 				</View>
 			</View>
diff --git a/app/views/LoginView.js b/app/views/LoginView.js
index 25b29853d..86d11b69b 100644
--- a/app/views/LoginView.js
+++ b/app/views/LoginView.js
@@ -135,7 +135,7 @@ export default class LoginView extends LoggedView {
 							</Text>
 						</View>
 
-						{this.props.failure && <Text style={styles.error}>{this.props.reason}</Text>}
+						{this.props.failure ? <Text style={styles.error}>{this.props.reason}</Text> : null}
 						<Loading visible={this.props.isFetching} />
 					</SafeAreaView>
 				</ScrollView>
diff --git a/app/views/MentionedMessagesView/index.js b/app/views/MentionedMessagesView/index.js
index 226153cb1..7a8b5c479 100644
--- a/app/views/MentionedMessagesView/index.js
+++ b/app/views/MentionedMessagesView/index.js
@@ -109,8 +109,8 @@ export default class MentionedMessagesView extends LoggedView {
 					style={styles.list}
 					keyExtractor={item => item._id}
 					onEndReached={this.moreData}
-					ListHeaderComponent={loading && <RCActivityIndicator />}
-					ListFooterComponent={loadingMore && <RCActivityIndicator />}
+					ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
+					ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
 				/>
 			]
 		);
diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js
index 107be23b0..47befc457 100644
--- a/app/views/PinnedMessagesView/index.js
+++ b/app/views/PinnedMessagesView/index.js
@@ -133,8 +133,8 @@ export default class PinnedMessagesView extends LoggedView {
 					style={styles.list}
 					keyExtractor={item => item._id}
 					onEndReached={this.moreData}
-					ListHeaderComponent={loading && <RCActivityIndicator />}
-					ListFooterComponent={loadingMore && <RCActivityIndicator />}
+					ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
+					ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
 				/>,
 				<ActionSheet
 					key='pinned-messages-view-action-sheet'
diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js
index d4e11c676..c4c0b8ad7 100644
--- a/app/views/ProfileView/index.js
+++ b/app/views/ProfileView/index.js
@@ -1,18 +1,436 @@
 import React from 'react';
-import { Text, View } from 'react-native';
+import PropTypes from 'prop-types';
+import { View, ScrollView, SafeAreaView, Keyboard } from 'react-native';
+import { connect } from 'react-redux';
+import Dialog from 'react-native-dialog';
+import SHA256 from 'js-sha256';
+import Icon from 'react-native-vector-icons/MaterialIcons';
+import ImagePicker from 'react-native-image-picker';
+import RNPickerSelect from 'react-native-picker-select';
 
 import LoggedView from '../View';
+import KeyboardView from '../../presentation/KeyboardView';
+import sharedStyles from '../Styles';
+import styles from './styles';
+import scrollPersistTaps from '../../utils/scrollPersistTaps';
+import { showErrorAlert, showToast } from '../../utils/info';
+import RocketChat from '../../lib/rocketchat';
+import RCTextInput from '../../containers/TextInput';
+import Loading from '../../containers/Loading';
+import log from '../../utils/log';
+import I18n from '../../i18n';
+import Button from '../../containers/Button';
+import Avatar from '../../containers/Avatar';
+import Touch from '../../utils/touch';
 
+@connect(state => ({
+	user: state.login.user,
+	Accounts_CustomFields: state.settings.Accounts_CustomFields
+}))
 export default class ProfileView extends LoggedView {
+	static propTypes = {
+		navigation: PropTypes.object,
+		user: PropTypes.object,
+		Accounts_CustomFields: PropTypes.string
+	};
+
 	constructor(props) {
 		super('ProfileView', props);
+		this.state = {
+			showPasswordAlert: false,
+			saving: false,
+			name: null,
+			username: null,
+			email: null,
+			newPassword: null,
+			typedPassword: null,
+			avatarUrl: null,
+			avatar: {},
+			avatarSuggestions: {},
+			customFields: {}
+		};
+	}
+
+	async componentDidMount() {
+		this.init();
+
+		try {
+			const result = await RocketChat.getAvatarSuggestion();
+			this.setState({ avatarSuggestions: result });
+		} catch (e) {
+			log('getAvatarSuggestion', e);
+		}
+	}
+
+	componentWillReceiveProps(nextProps) {
+		if (this.props.user !== nextProps.user) {
+			this.init(nextProps.user);
+		}
+	}
+
+	init = (user) => {
+		const {
+			name, username, emails, customFields
+		} = user || this.props.user;
+		this.setState({
+			name,
+			username,
+			email: emails ? emails[0].address : null,
+			newPassword: null,
+			typedPassword: null,
+			avatarUrl: null,
+			avatar: {},
+			customFields: customFields || {}
+		});
+	}
+
+	formIsChanged = () => {
+		const {
+			name, username, email, newPassword, avatar, customFields
+		} = this.state;
+		const { user } = this.props;
+		let customFieldsChanged = false;
+
+		const customFieldsKeys = Object.keys(customFields);
+		if (customFieldsKeys.length) {
+			customFieldsKeys.forEach((key) => {
+				if (user.customFields[key] !== customFields[key]) {
+					customFieldsChanged = true;
+				}
+			});
+		}
+
+		return !(user.name === name &&
+			user.username === username &&
+			!newPassword &&
+			(user.emails && user.emails[0].address === email) &&
+			!avatar.data &&
+			!customFieldsChanged
+		);
+	}
+
+	closePasswordAlert = () => {
+		this.setState({ showPasswordAlert: false });
+	}
+
+	handleError = (e, func, action) => {
+		if (e && e.error && e.error !== 500) {
+			if (e.details && e.details.timeToReset) {
+				return showErrorAlert(I18n.t('error-too-many-requests', {
+					seconds: parseInt(e.details.timeToReset / 1000, 10)
+				}));
+			}
+			return showErrorAlert(I18n.t(e.error, e.details));
+		}
+		showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t(action) }));
+		log(func, e);
+	}
+
+	submit = async() => {
+		Keyboard.dismiss();
+
+		if (!this.formIsChanged()) {
+			return;
+		}
+
+		this.setState({ saving: true, showPasswordAlert: false });
+
+		const {
+			name, username, email, newPassword, typedPassword, avatar, customFields
+		} = this.state;
+		const { user } = this.props;
+		const params = {};
+
+		// Name
+		if (user.name !== name) {
+			params.realname = name;
+		}
+
+		// Username
+		if (user.username !== username) {
+			params.username = username;
+		}
+
+		// Email
+		if (user.emails && user.emails[0].address !== email) {
+			params.email = email;
+		}
+
+		// newPassword
+		if (newPassword) {
+			params.newPassword = newPassword;
+		}
+
+		// typedPassword
+		if (typedPassword) {
+			params.typedPassword = SHA256(typedPassword);
+		}
+
+		const requirePassword = !!params.email || newPassword;
+		if (requirePassword && !params.typedPassword) {
+			return this.setState({ showPasswordAlert: true, saving: false });
+		}
+
+		try {
+			if (avatar.url) {
+				try {
+					await RocketChat.setAvatarFromService(avatar);
+				} catch (e) {
+					this.setState({ saving: false, typedPassword: null });
+					return setTimeout(() => this.handleError(e, 'setAvatarFromService', 'changing_avatar'), 300);
+				}
+			}
+
+			await RocketChat.saveUserProfile(params, customFields);
+			this.setState({ saving: false });
+			setTimeout(() => {
+				showToast(I18n.t('Profile_saved_successfully'));
+				this.init();
+			}, 300);
+		} catch (e) {
+			this.setState({ saving: false, typedPassword: null });
+			setTimeout(() => {
+				this.handleError(e, 'saveUserProfile', 'saving_profile');
+			}, 300);
+		}
+	}
+
+	setAvatar = (avatar) => {
+		this.setState({ avatar });
+	}
+
+	resetAvatar = async() => {
+		try {
+			await RocketChat.resetAvatar();
+			showToast(I18n.t('Avatar_changed_successfully'));
+			this.init();
+		} catch (e) {
+			this.handleError(e, 'resetAvatar', 'changing_avatar');
+		}
+	}
+
+	pickImage = () => {
+		const options = {
+			title: I18n.t('Select_Avatar')
+		};
+		ImagePicker.showImagePicker(options, async(response) => {
+			if (response.didCancel) {
+				console.warn('User cancelled image picker');
+			} else if (response.error) {
+				log('ImagePicker Error', response.error);
+			} else {
+				this.setAvatar({ url: response.uri, data: `data:image/jpeg;base64,${ response.data }`, service: 'upload' });
+			}
+		});
+	}
+
+	renderAvatarButton = ({
+		key, child, onPress, disabled = false
+	}) => (
+		<Touch
+			key={key}
+			testID={key}
+			onPress={onPress}
+			underlayColor='rgba(255, 255, 255, 0.5)'
+			activeOpacity={0.3}
+			disabled={disabled}
+		>
+			<View
+				style={[styles.avatarButton, { opacity: disabled ? 0.5 : 1 }]}
+			>
+				{child}
+			</View>
+		</Touch>
+	)
+
+	renderAvatarButtons = () => (
+		<View style={styles.avatarButtons}>
+			{this.renderAvatarButton({
+				child: <Avatar text={this.props.user.username} size={50} forceInitials />,
+				onPress: () => this.resetAvatar(),
+				key: 'profile-view-reset-avatar'
+			})}
+			{this.renderAvatarButton({
+				child: <Icon name='file-upload' size={30} />,
+				onPress: () => this.pickImage(),
+				key: 'profile-view-upload-avatar'
+			})}
+			{this.renderAvatarButton({
+				child: <Icon name='link' size={30} />,
+				onPress: () => this.setAvatar({ url: this.state.avatarUrl, data: this.state.avatarUrl, service: 'url' }),
+				disabled: !this.state.avatarUrl,
+				key: 'profile-view-avatar-url-button'
+			})}
+			{Object.keys(this.state.avatarSuggestions).map((service) => {
+				const { url, blob, contentType } = this.state.avatarSuggestions[service];
+				return this.renderAvatarButton({
+					key: `profile-view-avatar-${ service }`,
+					child: <Avatar avatar={url} size={50} />,
+					onPress: () => this.setAvatar({
+						url, data: blob, service, contentType
+					})
+				});
+			})}
+		</View>
+	);
+
+	renderCustomFields = () => {
+		const { customFields } = this.state;
+		if (!this.props.Accounts_CustomFields) {
+			return null;
+		}
+		const parsedCustomFields = JSON.parse(this.props.Accounts_CustomFields);
+		return Object.keys(parsedCustomFields).map((key, index, array) => {
+			if (parsedCustomFields[key].type === 'select') {
+				const options = parsedCustomFields[key].options.map(option => ({ label: option, value: option }));
+				return (
+					<RNPickerSelect
+						key={key}
+						items={options}
+						onValueChange={(value) => {
+							const newValue = {};
+							newValue[key] = value;
+							this.setState({ customFields: { ...this.state.customFields, ...newValue } });
+						}}
+						value={customFields[key]}
+					>
+						<RCTextInput
+							inputRef={(e) => { this[key] = e; }}
+							label={key}
+							placeholder={key}
+							value={customFields[key]}
+							testID='settings-view-language'
+						/>
+					</RNPickerSelect>
+				);
+			}
+
+			return (
+				<RCTextInput
+					inputRef={(e) => { this[key] = e; }}
+					key={key}
+					label={key}
+					placeholder={key}
+					value={customFields[key]}
+					onChangeText={(value) => {
+						const newValue = {};
+						newValue[key] = value;
+						this.setState({ customFields: { ...this.state.customFields, ...newValue } });
+					}}
+					onSubmitEditing={() => {
+						if (array.length - 1 > index) {
+							return this[array[index + 1]].focus();
+						}
+						this.avatarUrl.focus();
+					}}
+				/>
+			);
+		});
 	}
 
 	render() {
+		const {
+			name, username, email, newPassword, avatarUrl, customFields
+		} = this.state;
 		return (
-			<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
-				<Text>ProfileView</Text>
-			</View>
+			<KeyboardView
+				contentContainerStyle={sharedStyles.container}
+				keyboardVerticalOffset={128}
+			>
+				<ScrollView
+					contentContainerStyle={sharedStyles.containerScrollView}
+					testID='profile-view-list'
+					{...scrollPersistTaps}
+				>
+					<SafeAreaView testID='profile-view'>
+						<View style={styles.avatarContainer} testID='profile-view-avatar'>
+							<Avatar
+								text={username}
+								avatar={this.state.avatar && this.state.avatar.url}
+								size={100}
+							/>
+						</View>
+						<RCTextInput
+							inputRef={(e) => { this.name = e; }}
+							label={I18n.t('Name')}
+							placeholder={I18n.t('Name')}
+							value={name}
+							onChangeText={value => this.setState({ name: value })}
+							onSubmitEditing={() => { this.username.focus(); }}
+							testID='profile-view-name'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.username = e; }}
+							label={I18n.t('Username')}
+							placeholder={I18n.t('Username')}
+							value={username}
+							onChangeText={value => this.setState({ username: value })}
+							onSubmitEditing={() => { this.email.focus(); }}
+							testID='profile-view-username'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.email = e; }}
+							label={I18n.t('Email')}
+							placeholder={I18n.t('Email')}
+							value={email}
+							onChangeText={value => this.setState({ email: value })}
+							onSubmitEditing={() => { this.newPassword.focus(); }}
+							testID='profile-view-email'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.newPassword = e; }}
+							label={I18n.t('New_Password')}
+							placeholder={I18n.t('New_Password')}
+							value={newPassword}
+							onChangeText={value => this.setState({ newPassword: value })}
+							onSubmitEditing={() => {
+								if (Object.keys(customFields).length) {
+									return this[Object.keys(customFields)[0]].focus();
+								}
+								this.avatarUrl.focus();
+							}}
+							secureTextEntry
+							testID='profile-view-new-password'
+						/>
+						{this.renderCustomFields()}
+						<RCTextInput
+							inputRef={(e) => { this.avatarUrl = e; }}
+							label={I18n.t('Avatar_Url')}
+							placeholder={I18n.t('Avatar_Url')}
+							value={avatarUrl}
+							onChangeText={value => this.setState({ avatarUrl: value })}
+							onSubmitEditing={this.submit}
+							testID='profile-view-avatar-url'
+						/>
+						{this.renderAvatarButtons()}
+						<View style={sharedStyles.alignItemsFlexStart}>
+							<Button
+								title={I18n.t('Save_Changes')}
+								type='primary'
+								onPress={this.submit}
+								disabled={!this.formIsChanged()}
+								testID='profile-view-submit'
+							/>
+						</View>
+						<Loading visible={this.state.saving} />
+						<Dialog.Container visible={this.state.showPasswordAlert}>
+							<Dialog.Title>
+								{I18n.t('Please_enter_your_password')}
+							</Dialog.Title>
+							<Dialog.Description>
+								{I18n.t('For_your_security_you_must_enter_your_current_password_to_continue')}
+							</Dialog.Description>
+							<Dialog.Input
+								onChangeText={value => this.setState({ typedPassword: value })}
+								secureTextEntry
+								testID='profile-view-typed-password'
+							/>
+							<Dialog.Button label={I18n.t('Cancel')} onPress={this.closePasswordAlert} />
+							<Dialog.Button label={I18n.t('Save')} onPress={this.submit} />
+						</Dialog.Container>
+					</SafeAreaView>
+				</ScrollView>
+			</KeyboardView>
 		);
 	}
 }
diff --git a/app/views/ProfileView/styles.js b/app/views/ProfileView/styles.js
new file mode 100644
index 000000000..3ac375c40
--- /dev/null
+++ b/app/views/ProfileView/styles.js
@@ -0,0 +1,24 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+	avatarContainer: {
+		alignItems: 'center',
+		justifyContent: 'center',
+		marginBottom: 10
+	},
+	avatarButtons: {
+		flexWrap: 'wrap',
+		flexDirection: 'row',
+		justifyContent: 'flex-start'
+	},
+	avatarButton: {
+		backgroundColor: '#e1e5e8',
+		width: 50,
+		height: 50,
+		alignItems: 'center',
+		justifyContent: 'center',
+		marginRight: 15,
+		marginBottom: 15,
+		borderRadius: 2
+	}
+});
diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js
index caeb72613..db3193b4a 100644
--- a/app/views/RegisterView.js
+++ b/app/views/RegisterView.js
@@ -207,10 +207,11 @@ export default class RegisterView extends LoggedView {
 						<Text style={[styles.loginText, styles.loginTitle]}>{I18n.t('Sign_Up')}</Text>
 						{this._renderRegister()}
 						{this._renderUsername()}
-						{this.props.login.failure &&
+						{this.props.login.failure ?
 							<Text style={styles.error} testID='register-view-error'>
 								{this.props.login.error.reason}
 							</Text>
+							: null
 						}
 						<Loading visible={this.props.login.isFetching} />
 					</SafeAreaView>
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 52aef0c93..c921ea310 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -381,7 +381,7 @@ export default class RoomActionsView extends LoggedView {
 		] : [
 			<Icon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />,
 			<Text key='name' style={styles.sectionItemName}>{ item.name }</Text>,
-			item.description && <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text>,
+			item.description ? <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text> : null,
 			<Icon key='right-icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#ccc' />
 		];
 		return this.renderTouchableItem(subview, item);
diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js
index 175c04bd3..1d4717f89 100644
--- a/app/views/RoomActionsView/styles.js
+++ b/app/views/RoomActionsView/styles.js
@@ -4,13 +4,6 @@ export default StyleSheet.create({
 	container: {
 		backgroundColor: '#F6F7F9'
 	},
-	headerButton: {
-		backgroundColor: 'transparent',
-		height: 44,
-		width: 44,
-		alignItems: 'center',
-		justifyContent: 'center'
-	},
 	sectionItem: {
 		backgroundColor: '#ffffff',
 		paddingVertical: 16,
diff --git a/app/views/RoomFilesView/index.js b/app/views/RoomFilesView/index.js
index 8a4941cbd..8028275e5 100644
--- a/app/views/RoomFilesView/index.js
+++ b/app/views/RoomFilesView/index.js
@@ -107,8 +107,8 @@ export default class RoomFilesView extends LoggedView {
 					style={styles.list}
 					keyExtractor={item => item._id}
 					onEndReached={this.moreData}
-					ListHeaderComponent={loading && <RCActivityIndicator />}
-					ListFooterComponent={loadingMore && <RCActivityIndicator />}
+					ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
+					ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
 				/>
 			]
 		);
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
index 95effd731..58b74f350 100644
--- a/app/views/RoomInfoEditView/index.js
+++ b/app/views/RoomInfoEditView/index.js
@@ -190,7 +190,7 @@ export default class RoomInfoEditView extends LoggedView {
 		await this.setState({ saving: false });
 		setTimeout(() => {
 			if (error) {
-				showErrorAlert(I18n.t('There_was_an_error_while_saving_settings'));
+				showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_settings') }));
 			} else {
 				showToast(I18n.t('Settings_succesfully_changed'));
 			}
@@ -266,133 +266,133 @@ export default class RoomInfoEditView extends LoggedView {
 					{...scrollPersistTaps}
 				>
 					<SafeAreaView testID='room-info-edit-view'>
-						<View style={sharedStyles.formContainer}>
-							<RCTextInput
-								inputRef={(e) => { this.name = e; }}
-								label={I18n.t('Name')}
-								value={name}
-								onChangeText={value => this.setState({ name: value })}
-								onSubmitEditing={() => { this.description.focus(); }}
-								error={nameError}
-								testID='room-info-edit-view-name'
-							/>
-							<RCTextInput
-								inputRef={(e) => { this.description = e; }}
-								label={I18n.t('Description')}
-								value={description}
-								onChangeText={value => this.setState({ description: value })}
-								onSubmitEditing={() => { this.topic.focus(); }}
-								testID='room-info-edit-view-description'
-							/>
-							<RCTextInput
-								inputRef={(e) => { this.topic = e; }}
-								label={I18n.t('Topic')}
-								value={topic}
-								onChangeText={value => this.setState({ topic: value })}
-								onSubmitEditing={() => { this.announcement.focus(); }}
-								testID='room-info-edit-view-topic'
-							/>
-							<RCTextInput
-								inputRef={(e) => { this.announcement = e; }}
-								label={I18n.t('Announcement')}
-								value={announcement}
-								onChangeText={value => this.setState({ announcement: value })}
-								onSubmitEditing={() => { this.joinCode.focus(); }}
-								testID='room-info-edit-view-announcement'
-							/>
-							<RCTextInput
-								inputRef={(e) => { this.joinCode = e; }}
-								label={I18n.t('Password')}
-								value={joinCode}
-								onChangeText={value => this.setState({ joinCode: value })}
-								onSubmitEditing={this.submit}
-								secureTextEntry
-								testID='room-info-edit-view-password'
-							/>
+						<RCTextInput
+							inputRef={(e) => { this.name = e; }}
+							label={I18n.t('Name')}
+							value={name}
+							onChangeText={value => this.setState({ name: value })}
+							onSubmitEditing={() => { this.description.focus(); }}
+							error={nameError}
+							testID='room-info-edit-view-name'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.description = e; }}
+							label={I18n.t('Description')}
+							value={description}
+							onChangeText={value => this.setState({ description: value })}
+							onSubmitEditing={() => { this.topic.focus(); }}
+							testID='room-info-edit-view-description'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.topic = e; }}
+							label={I18n.t('Topic')}
+							value={topic}
+							onChangeText={value => this.setState({ topic: value })}
+							onSubmitEditing={() => { this.announcement.focus(); }}
+							testID='room-info-edit-view-topic'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.announcement = e; }}
+							label={I18n.t('Announcement')}
+							value={announcement}
+							onChangeText={value => this.setState({ announcement: value })}
+							onSubmitEditing={() => { this.joinCode.focus(); }}
+							testID='room-info-edit-view-announcement'
+						/>
+						<RCTextInput
+							inputRef={(e) => { this.joinCode = e; }}
+							label={I18n.t('Password')}
+							value={joinCode}
+							onChangeText={value => this.setState({ joinCode: value })}
+							onSubmitEditing={this.submit}
+							secureTextEntry
+							testID='room-info-edit-view-password'
+						/>
+						<SwitchContainer
+							value={t}
+							leftLabelPrimary={I18n.t('Public')}
+							leftLabelSecondary={I18n.t('Everyone_can_access_this_channel')}
+							rightLabelPrimary={I18n.t('Private')}
+							rightLabelSecondary={I18n.t('Just_invited_people_can_access_this_channel')}
+							onValueChange={value => this.setState({ t: value })}
+							testID='room-info-edit-view-t'
+						/>
+						<SwitchContainer
+							value={ro}
+							leftLabelPrimary={I18n.t('Colaborative')}
+							leftLabelSecondary={I18n.t('All_users_in_the_channel_can_write_new_messages')}
+							rightLabelPrimary={I18n.t('Read_Only')}
+							rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')}
+							onValueChange={value => this.setState({ ro: value })}
+							disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
+							testID='room-info-edit-view-ro'
+						/>
+						{ro && !room.broadcast ?
 							<SwitchContainer
-								value={t}
-								leftLabelPrimary={I18n.t('Public')}
-								leftLabelSecondary={I18n.t('Everyone_can_access_this_channel')}
-								rightLabelPrimary={I18n.t('Private')}
-								rightLabelSecondary={I18n.t('Just_invited_people_can_access_this_channel')}
-								onValueChange={value => this.setState({ t: value })}
-								testID='room-info-edit-view-t'
+								value={reactWhenReadOnly}
+								leftLabelPrimary={I18n.t('No_Reactions')}
+								leftLabelSecondary={I18n.t('Reactions_are_disabled')}
+								rightLabelPrimary={I18n.t('Allow_Reactions')}
+								rightLabelSecondary={I18n.t('Reactions_are_enabled')}
+								onValueChange={value => this.setState({ reactWhenReadOnly: value })}
+								disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
+								testID='room-info-edit-view-react-when-ro'
 							/>
-							<SwitchContainer
-								value={ro}
-								leftLabelPrimary={I18n.t('Colaborative')}
-								leftLabelSecondary={I18n.t('All_users_in_the_channel_can_write_new_messages')}
-								rightLabelPrimary={I18n.t('Read_Only')}
-								rightLabelSecondary={I18n.t('Only_authorized_users_can_write_new_messages')}
-								onValueChange={value => this.setState({ ro: value })}
-								disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
-								testID='room-info-edit-view-ro'
-							/>
-							{ro && !room.broadcast &&
-								<SwitchContainer
-									value={reactWhenReadOnly}
-									leftLabelPrimary={I18n.t('No_Reactions')}
-									leftLabelSecondary={I18n.t('Reactions_are_disabled')}
-									rightLabelPrimary={I18n.t('Allow_Reactions')}
-									rightLabelSecondary={I18n.t('Reactions_are_enabled')}
-									onValueChange={value => this.setState({ reactWhenReadOnly: value })}
-									disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
-									testID='room-info-edit-view-react-when-ro'
-								/>
-							}
-							{room.broadcast &&
-								[
-									<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
-									<View style={styles.divider} />
-								]
-							}
+							: null
+						}
+						{room.broadcast ?
+							[
+								<Text style={styles.broadcast}>{I18n.t('Broadcast_Channel')}</Text>,
+								<View style={styles.divider} />
+							]
+							: null
+						}
+						<TouchableOpacity
+							style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
+							onPress={this.submit}
+							disabled={!this.formIsChanged()}
+							testID='room-info-edit-view-submit'
+						>
+							<Text style={sharedStyles.button} accessibilityTraits='button'>{I18n.t('SAVE')}</Text>
+						</TouchableOpacity>
+						<View style={{ flexDirection: 'row' }}>
 							<TouchableOpacity
-								style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
-								onPress={this.submit}
-								disabled={!this.formIsChanged()}
-								testID='room-info-edit-view-submit'
+								style={[sharedStyles.buttonContainer_inverted, styles.buttonInverted, { flex: 1 }]}
+								onPress={this.reset}
+								testID='room-info-edit-view-reset'
 							>
-								<Text style={sharedStyles.button} accessibilityTraits='button'>{I18n.t('SAVE')}</Text>
+								<Text style={sharedStyles.button_inverted} accessibilityTraits='button'>{I18n.t('RESET')}</Text>
 							</TouchableOpacity>
-							<View style={{ flexDirection: 'row' }}>
-								<TouchableOpacity
-									style={[sharedStyles.buttonContainer_inverted, styles.buttonInverted, { flex: 1 }]}
-									onPress={this.reset}
-									testID='room-info-edit-view-reset'
-								>
-									<Text style={sharedStyles.button_inverted} accessibilityTraits='button'>{I18n.t('RESET')}</Text>
-								</TouchableOpacity>
-								<TouchableOpacity
-									style={[
-										sharedStyles.buttonContainer_inverted,
-										styles.buttonDanger,
-										!this.hasArchivePermission() && sharedStyles.opacity5,
-										{ flex: 1, marginLeft: 10 }
-									]}
-									onPress={this.toggleArchive}
-									disabled={!this.hasArchivePermission()}
-									testID='room-info-edit-view-archive'
-								>
-									<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>
-										{ room.archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
-									</Text>
-								</TouchableOpacity>
-							</View>
-							<View style={styles.divider} />
 							<TouchableOpacity
 								style={[
 									sharedStyles.buttonContainer_inverted,
-									sharedStyles.buttonContainerLastChild,
 									styles.buttonDanger,
-									!this.hasDeletePermission() && sharedStyles.opacity5
+									!this.hasArchivePermission() && sharedStyles.opacity5,
+									{ flex: 1, marginLeft: 10 }
 								]}
-								onPress={this.delete}
-								disabled={!this.hasDeletePermission()}
-								testID='room-info-edit-view-delete'
+								onPress={this.toggleArchive}
+								disabled={!this.hasArchivePermission()}
+								testID='room-info-edit-view-archive'
 							>
-								<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text>
+								<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>
+									{ room.archived ? I18n.t('UNARCHIVE') : I18n.t('ARCHIVE') }
+								</Text>
 							</TouchableOpacity>
 						</View>
+						<View style={styles.divider} />
+						<TouchableOpacity
+							style={[
+								sharedStyles.buttonContainer_inverted,
+								sharedStyles.buttonContainerLastChild,
+								styles.buttonDanger,
+								!this.hasDeletePermission() && sharedStyles.opacity5
+							]}
+							onPress={this.delete}
+							disabled={!this.hasDeletePermission()}
+							testID='room-info-edit-view-delete'
+						>
+							<Text style={[sharedStyles.button_inverted, styles.colorDanger]} accessibilityTraits='button'>{I18n.t('DELETE')}</Text>
+						</TouchableOpacity>
 						<Loading visible={this.state.saving} />
 					</SafeAreaView>
 				</ScrollView>
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index 68209a655..0047354d9 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -61,7 +61,7 @@ export default class RoomInfoView extends LoggedView {
 					accessibilityTraits='button'
 					testID='room-info-view-edit-button'
 				>
-					<View style={styles.headerButton}>
+					<View style={sharedStyles.headerButton}>
 						<MaterialIcon name='edit' size={20} />
 					</View>
 				</Touch>
@@ -146,17 +146,18 @@ export default class RoomInfoView extends LoggedView {
 	);
 
 	renderRoles = () => (
-		this.state.roles.length > 0 &&
-		<View style={styles.item}>
-			<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
-			<View style={styles.rolesContainer}>
-				{this.state.roles.map(role => (
-					<View style={styles.roleBadge} key={role}>
-						<Text>{ this.props.roles[role] }</Text>
-					</View>
-				))}
+		this.state.roles.length > 0 ?
+			<View style={styles.item}>
+				<Text style={styles.itemLabel}>{I18n.t('Roles')}</Text>
+				<View style={styles.rolesContainer}>
+					{this.state.roles.map(role => (
+						<View style={styles.roleBadge} key={role}>
+							<Text>{ this.props.roles[role] }</Text>
+						</View>
+					))}
+				</View>
 			</View>
-		</View>
+			: null
 	)
 
 	renderTimezone = (userId) => {
@@ -210,12 +211,12 @@ export default class RoomInfoView extends LoggedView {
 					{this.renderAvatar(room, roomUser)}
 					<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
 				</View>
-				{!this.isDirect() && this.renderItem('description', room)}
-				{!this.isDirect() && this.renderItem('topic', room)}
-				{!this.isDirect() && this.renderItem('announcement', room)}
-				{this.isDirect() && this.renderRoles()}
-				{this.isDirect() && this.renderTimezone(roomUser._id)}
-				{room.broadcast && this.renderBroadcast()}
+				{!this.isDirect() ? this.renderItem('description', room) : null}
+				{!this.isDirect() ? this.renderItem('topic', room) : null}
+				{!this.isDirect() ? this.renderItem('announcement', room) : null}
+				{this.isDirect() ? this.renderRoles() : null}
+				{this.isDirect() ? this.renderTimezone(roomUser._id) : null}
+				{room.broadcast ? this.renderBroadcast() : null}
 			</ScrollView>
 		);
 	}
diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.js
index fe9c0af71..70c44dc17 100644
--- a/app/views/RoomInfoView/styles.js
+++ b/app/views/RoomInfoView/styles.js
@@ -7,13 +7,6 @@ export default StyleSheet.create({
 		backgroundColor: '#ffffff',
 		padding: 10
 	},
-	headerButton: {
-		backgroundColor: 'transparent',
-		height: 44,
-		width: 44,
-		alignItems: 'center',
-		justifyContent: 'center'
-	},
 	item: {
 		padding: 10,
 		// borderColor: '#EBEDF1',
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index 68fc2d0cf..dcadebf56 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -1,13 +1,13 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { FlatList, Text, View, TextInput, Vibration } from 'react-native';
+import { FlatList, Text, View, TextInput, Vibration, TouchableOpacity } from 'react-native';
 import { connect } from 'react-redux';
 import ActionSheet from 'react-native-actionsheet';
 
 import LoggedView from '../View';
 import styles from './styles';
+import sharedStyles from '../Styles';
 import RoomItem from '../../presentation/RoomItem';
-import Touch from '../../utils/touch';
 import scrollPersistTaps from '../../utils/scrollPersistTaps';
 import RocketChat from '../../lib/rocketchat';
 import { goRoom } from '../../containers/routes/NavigationService';
@@ -33,19 +33,15 @@ export default class MentionedMessagesView extends LoggedView {
 		}
 		return {
 			headerRight: (
-				<Touch
+				<TouchableOpacity
 					onPress={params.onPressToogleStatus}
-					underlayColor='#ffffff'
-					activeOpacity={0.5}
 					accessibilityLabel={label}
 					accessibilityTraits='button'
-					style={styles.headerButtonTouchable}
+					style={[sharedStyles.headerButton, styles.headerButton]}
 					testID='room-members-view-toggle-status'
 				>
-					<View style={styles.headerButton}>
-						<Text style={styles.headerButtonText}>{label}</Text>
-					</View>
-				</Touch>
+					<Text>{label}</Text>
+				</TouchableOpacity>
 			)
 		};
 	};
diff --git a/app/views/RoomMembersView/styles.js b/app/views/RoomMembersView/styles.js
index 12faa2c29..07e5c1e5a 100644
--- a/app/views/RoomMembersView/styles.js
+++ b/app/views/RoomMembersView/styles.js
@@ -31,18 +31,6 @@ export default StyleSheet.create({
 		fontSize: 16,
 		color: '#444'
 	},
-	headerButtonTouchable: {
-		borderRadius: 4
-	},
-	headerButton: {
-		padding: 6,
-		backgroundColor: 'transparent',
-		alignItems: 'center',
-		justifyContent: 'center'
-	},
-	headerButtonText: {
-		color: '#292E35'
-	},
 	searchBoxView: {
 		backgroundColor: '#eee'
 	},
@@ -53,5 +41,9 @@ export default StyleSheet.create({
 		padding: 5,
 		paddingLeft: 10,
 		color: '#aaa'
+	},
+	headerButton: {
+		marginRight: 9,
+		alignItems: 'flex-end'
 	}
 });
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 275150aad..2cf249c17 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -14,6 +14,7 @@ import { closeRoom } from '../../../actions/room';
 import log from '../../../utils/log';
 import RoomTypeIcon from '../../../containers/RoomTypeIcon';
 import I18n from '../../../i18n';
+import sharedStyles from '../../Styles';
 
 const title = (offline, connecting, authenticating, logged) => {
 	if (offline) {
@@ -159,7 +160,7 @@ export default class RoomHeaderView extends React.PureComponent {
 						</Text>
 					</View>
 
-					{ t && <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text>}
+					{ t ? <Text style={styles.userStatus} allowFontScaling={false} numberOfLines={1}>{t}</Text> : null}
 
 				</View>
 			</TouchableOpacity>
@@ -169,7 +170,7 @@ export default class RoomHeaderView extends React.PureComponent {
 	renderRight = () => (
 		<View style={styles.right}>
 			<TouchableOpacity
-				style={styles.headerButton}
+				style={sharedStyles.headerButton}
 				onPress={() => {
 					try {
 						RocketChat.toggleFavorite(this.state.room.rid, this.state.room.f);
@@ -189,7 +190,7 @@ export default class RoomHeaderView extends React.PureComponent {
 				/>
 			</TouchableOpacity>
 			<TouchableOpacity
-				style={styles.headerButton}
+				style={sharedStyles.headerButton}
 				onPress={() => this.props.navigation.navigate({ key: 'RoomActions', routeName: 'RoomActions', params: { rid: this.state.room.rid } })}
 				accessibilityLabel={I18n.t('Room_actions')}
 				accessibilityTraits='button'
diff --git a/app/views/RoomView/Header/styles.js b/app/views/RoomView/Header/styles.js
index 56cbd186c..1be2af4f5 100644
--- a/app/views/RoomView/Header/styles.js
+++ b/app/views/RoomView/Header/styles.js
@@ -40,13 +40,6 @@ export default StyleSheet.create({
 	right: {
 		flexDirection: 'row'
 	},
-	headerButton: {
-		backgroundColor: 'transparent',
-		height: 44,
-		width: 40,
-		alignItems: 'center',
-		justifyContent: 'center'
-	},
 	avatar: {
 		marginRight: 5
 	}
diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js
index a9036a6ef..95443cfca 100644
--- a/app/views/RoomView/ListView.js
+++ b/app/views/RoomView/ListView.js
@@ -113,8 +113,8 @@ export class ListView extends OldList2 {
 
 		// const { renderSectionHeader } = this.props;
 
-		const header = this.props.renderHeader && this.props.renderHeader();
-		const footer = this.props.renderFooter && this.props.renderFooter();
+		const header = this.props.renderHeader ? this.props.renderHeader() : null;
+		const footer = this.props.renderFooter ? this.props.renderFooter() : null;
 		// let totalIndex = header ? 1 : 0;
 
 		const { data } = this.props;
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 28cfadb00..db86edd13 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -95,8 +95,12 @@ export default class RoomView extends LoggedView {
 		}
 
 		requestAnimationFrame(async() => {
-			const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts });
-			this.setState({ end: result < 20 });
+			try {
+				const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: this.state.room.t, latest: lastRowData.ts });
+				this.setState({ end: result < 20 });
+			} catch (e) {
+				log('RoomView.onEndReached', e);
+			}
 		});
 	}
 
diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js
index a225b7d70..f9272ac2b 100644
--- a/app/views/RoomsListView/Header/index.js
+++ b/app/views/RoomsListView/Header/index.js
@@ -13,6 +13,7 @@ import RocketChat from '../../../lib/rocketchat';
 import { STATUS_COLORS } from '../../../constants/colors';
 import { setSearch } from '../../../actions/rooms';
 import styles from './styles';
+import sharedStyles from '../../Styles';
 import log from '../../../utils/log';
 import I18n from '../../../i18n';
 
@@ -130,7 +131,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
 				testID='rooms-list-view-sidebar'
 			>
 				<TouchableOpacity
-					style={styles.headerButton}
+					style={sharedStyles.headerButton}
 					onPress={() => this.props.navigation.openDrawer()}
 				>
 					<FastImage
@@ -174,7 +175,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
 				</Avatar>
 				<View style={styles.rows}>
 					<Text accessible={false} style={styles.title} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{this.props.user.username}</Text>
-					{ t && <Text accessible={false} style={styles.status_text} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{t}</Text>}
+					{ t ? <Text accessible={false} style={styles.status_text} ellipsizeMode='tail' numberOfLines={1} allowFontScaling={false}>{t}</Text> : null}
 				</View>
 			</TouchableOpacity>
 		);
@@ -189,7 +190,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
 			<View style={styles.right}>
 				{Platform.OS === 'android' ?
 					<TouchableOpacity
-						style={styles.headerButton}
+						style={sharedStyles.headerButton}
 						onPress={() => this.onPressSearchButton()}
 						accessibilityLabel={I18n.t('Search')}
 						accessibilityTraits='button'
@@ -203,7 +204,7 @@ export default class RoomsListHeaderView extends React.PureComponent {
 					</TouchableOpacity> : null}
 				{Platform.OS === 'ios' ?
 					<TouchableOpacity
-						style={styles.headerButton}
+						style={sharedStyles.headerButton}
 						onPress={() => this.createChannel()}
 						accessibilityLabel={I18n.t('Create_Channel')}
 						accessibilityTraits='button'
diff --git a/app/views/RoomsListView/Header/styles.js b/app/views/RoomsListView/Header/styles.js
index 1a4e2f587..282bbe58d 100644
--- a/app/views/RoomsListView/Header/styles.js
+++ b/app/views/RoomsListView/Header/styles.js
@@ -55,13 +55,6 @@ export default StyleSheet.create({
 		borderBottomColor: 'rgba(0, 0, 0, .3)',
 		paddingHorizontal: 20
 	},
-	headerButton: {
-		backgroundColor: 'transparent',
-		height: 44,
-		width: 44,
-		alignItems: 'center',
-		justifyContent: 'center'
-	},
 	user_status: {
 		position: 'absolute',
 		bottom: -2,
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index 929e0cfb9..444ebee3b 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -223,6 +223,6 @@ export default class RoomsListView extends LoggedView {
 	render = () => (
 		<View style={styles.container} testID='rooms-list-view'>
 			{this.renderList()}
-			{Platform.OS === 'android' && this.renderCreateButtons()}
+			{Platform.OS === 'android' ? this.renderCreateButtons() : null}
 		</View>)
 }
diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js
index 4aa45da26..db6bc7162 100644
--- a/app/views/SearchMessagesView/index.js
+++ b/app/views/SearchMessagesView/index.js
@@ -129,8 +129,8 @@ export default class SearchMessagesView extends LoggedView {
 					style={styles.list}
 					keyExtractor={item => item._id}
 					onEndReached={this.moreData}
-					ListHeaderComponent={searching && <RCActivityIndicator />}
-					ListFooterComponent={loadingMore && <RCActivityIndicator />}
+					ListHeaderComponent={searching ? <RCActivityIndicator /> : null}
+					ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
 					{...scrollPersistTaps}
 				/>
 			</View>
diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js
index c25511c71..8c1bac4dd 100644
--- a/app/views/SettingsView/index.js
+++ b/app/views/SettingsView/index.js
@@ -1,18 +1,134 @@
 import React from 'react';
-import { Text, View } from 'react-native';
+import PropTypes from 'prop-types';
+import { View, ScrollView, SafeAreaView } from 'react-native';
+import RNPickerSelect from 'react-native-picker-select';
+import { connect } from 'react-redux';
 
 import LoggedView from '../View';
+import RocketChat from '../../lib/rocketchat';
+import KeyboardView from '../../presentation/KeyboardView';
+import sharedStyles from '../Styles';
+import RCTextInput from '../../containers/TextInput';
+import scrollPersistTaps from '../../utils/scrollPersistTaps';
+import I18n from '../../i18n';
+import Button from '../../containers/Button';
+import Loading from '../../containers/Loading';
+import { showErrorAlert, showToast } from '../../utils/info';
+import log from '../../utils/log';
+import { setUser } from '../../actions/login';
 
+@connect(state => ({
+	user: state.login.user
+}), dispatch => ({
+	setUser: params => dispatch(setUser(params))
+}))
 export default class SettingsView extends LoggedView {
+	static propTypes = {
+		user: PropTypes.object,
+		setUser: PropTypes.func
+	};
+
 	constructor(props) {
 		super('SettingsView', props);
+		this.state = {
+			placeholder: {},
+			language: props.user ? props.user.language : 'en',
+			languages: [{
+				label: 'English',
+				value: 'en'
+			}],
+			saving: false
+		};
+	}
+
+	formIsChanged = () => {
+		const { language } = this.state;
+		const { user } = this.props;
+		return !(user.language === language);
+	}
+
+	submit = async() => {
+		this.setState({ saving: true });
+
+		const {
+			language
+		} = this.state;
+		const { user } = this.props;
+
+		if (!this.formIsChanged()) {
+			return;
+		}
+
+		const params = {};
+
+		// language
+		if (user.language !== language) {
+			params.language = language;
+		}
+
+		try {
+			await RocketChat.saveUserPreferences(params);
+			this.props.setUser({ language: params.language });
+			this.props.navigation.setParams({ title: I18n.t('Settings') });
+
+			this.setState({ saving: false });
+			setTimeout(() => {
+				showToast(I18n.t('Preferences_saved'));
+			}, 300);
+		} catch (e) {
+			this.setState({ saving: false });
+			setTimeout(() => {
+				if (e && e.error) {
+					return showErrorAlert(I18n.t(e.error, e.details));
+				}
+				showErrorAlert(I18n.t('There_was_an_error_while_action', { action: I18n.t('saving_preferences') }));
+				log('saveUserPreferences', e);
+			}, 300);
+		}
 	}
 
 	render() {
+		const { language, languages, placeholder } = this.state;
 		return (
-			<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
-				<Text>SettingsView</Text>
-			</View>
+			<KeyboardView
+				contentContainerStyle={sharedStyles.container}
+				keyboardVerticalOffset={128}
+			>
+				<ScrollView
+					contentContainerStyle={sharedStyles.containerScrollView}
+					testID='settings-view-list'
+					{...scrollPersistTaps}
+				>
+					<SafeAreaView testID='settings-view'>
+						<RNPickerSelect
+							items={languages}
+							onValueChange={(value) => {
+								this.setState({ language: value });
+							}}
+							value={language}
+							placeholder={placeholder}
+						>
+							<RCTextInput
+								inputRef={(e) => { this.name = e; }}
+								label={I18n.t('Language')}
+								placeholder={I18n.t('Language')}
+								value={language}
+								testID='settings-view-language'
+							/>
+						</RNPickerSelect>
+						<View style={sharedStyles.alignItemsFlexStart}>
+							<Button
+								title={I18n.t('Save_Changes')}
+								type='primary'
+								onPress={this.submit}
+								disabled={!this.formIsChanged()}
+								testID='settings-view-button'
+							/>
+						</View>
+						<Loading visible={this.state.saving} />
+					</SafeAreaView>
+				</ScrollView>
+			</KeyboardView>
 		);
 	}
 }
diff --git a/app/views/SnippetedMessagesView/index.js b/app/views/SnippetedMessagesView/index.js
index f1b240023..7b0f426dd 100644
--- a/app/views/SnippetedMessagesView/index.js
+++ b/app/views/SnippetedMessagesView/index.js
@@ -109,8 +109,8 @@ export default class SnippetedMessagesView extends LoggedView {
 					style={styles.list}
 					keyExtractor={item => item._id}
 					onEndReached={this.moreData}
-					ListHeaderComponent={loading && <RCActivityIndicator />}
-					ListFooterComponent={loadingMore && <RCActivityIndicator />}
+					ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
+					ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
 				/>
 			]
 		);
diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js
index 6feed3c3b..ce468bc89 100644
--- a/app/views/StarredMessagesView/index.js
+++ b/app/views/StarredMessagesView/index.js
@@ -133,8 +133,8 @@ export default class StarredMessagesView extends LoggedView {
 					style={styles.list}
 					keyExtractor={item => item._id}
 					onEndReached={this.moreData}
-					ListHeaderComponent={loading && <RCActivityIndicator />}
-					ListFooterComponent={loadingMore && <RCActivityIndicator />}
+					ListHeaderComponent={loading ? <RCActivityIndicator /> : null}
+					ListFooterComponent={loadingMore ? <RCActivityIndicator /> : null}
 				/>,
 				<ActionSheet
 					key='starred-messages-view-action-sheet'
diff --git a/app/views/Styles.js b/app/views/Styles.js
index 2a0d4d040..9570aa898 100644
--- a/app/views/Styles.js
+++ b/app/views/Styles.js
@@ -195,5 +195,12 @@ export default StyleSheet.create({
 		width: 50,
 		height: 50,
 		marginVertical: 25
+	},
+	headerButton: {
+		backgroundColor: 'transparent',
+		height: 44,
+		width: 44,
+		alignItems: 'center',
+		justifyContent: 'center'
 	}
 });
diff --git a/e2e/11-broadcast.spec.js b/e2e/11-broadcast.spec.js
index 675b81a24..4346b7f26 100644
--- a/e2e/11-broadcast.spec.js
+++ b/e2e/11-broadcast.spec.js
@@ -2,7 +2,7 @@ const {
 	device, expect, element, by, waitFor
 } = require('detox');
 const { takeScreenshot } = require('./helpers/screenshot');
-const { logout, navigateToLogin } = require('./helpers/app');
+const { logout, navigateToLogin, login } = require('./helpers/app');
 const data = require('./data');
 
 describe('Broadcast room', () => {
@@ -99,4 +99,13 @@ describe('Broadcast room', () => {
 	afterEach(async() => {
 		takeScreenshot();
 	});
+
+	after(async() => {
+		// log back as main test user and left screen on RoomsListView
+		await element(by.id('header-back')).tap();
+		await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+		await logout();
+		await navigateToLogin();
+		await login();
+	})
 });
diff --git a/e2e/12-profile.spec.js b/e2e/12-profile.spec.js
new file mode 100644
index 000000000..ecf2ff107
--- /dev/null
+++ b/e2e/12-profile.spec.js
@@ -0,0 +1,115 @@
+const {
+	device, expect, element, by, waitFor
+} = require('detox');
+const { takeScreenshot } = require('./helpers/screenshot');
+const { logout, navigateToLogin, login } = require('./helpers/app');
+const data = require('./data');
+
+const scrollDown = 200;
+
+describe('Profile screen', () => {
+	before(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-profile'))).toBeVisible().withTimeout(2000);
+		await expect(element(by.id('sidebar-profile'))).toBeVisible();
+		await element(by.id('sidebar-profile')).tap();
+		await waitFor(element(by.id('profile-view'))).toBeVisible().withTimeout(2000);
+
+	});
+
+	describe('Render', async() => {
+		it('should have profile view', async() => {
+			await expect(element(by.id('profile-view'))).toBeVisible();
+		});
+
+		it('should have avatar', async() => {
+			await expect(element(by.id('profile-view-avatar')).atIndex(0)).toBeVisible();
+		});
+
+		it('should have name', async() => {
+			await expect(element(by.id('profile-view-name'))).toBeVisible();
+		});
+
+		it('should have username', async() => {
+			await expect(element(by.id('profile-view-username'))).toBeVisible();
+		});
+
+		it('should have email', async() => {
+			await expect(element(by.id('profile-view-email'))).toExist();
+		});
+
+		it('should have new password', async() => {
+			await expect(element(by.id('profile-view-new-password'))).toBeVisible();
+		});
+
+		it('should have avatar url', async() => {
+			await expect(element(by.id('profile-view-avatar-url'))).toBeVisible();
+		});
+		
+		it('should have reset avatar button', async() => {
+			await waitFor(element(by.id('profile-view-reset-avatar'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+			await expect(element(by.id('profile-view-reset-avatar'))).toBeVisible();
+		});
+
+		it('should have upload avatar button', async() => {
+			await waitFor(element(by.id('profile-view-upload-avatar'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+			await expect(element(by.id('profile-view-upload-avatar'))).toBeVisible();
+		});
+
+		it('should have avatar url button', async() => {
+			await waitFor(element(by.id('profile-view-avatar-url-button'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+			await expect(element(by.id('profile-view-avatar-url-button'))).toBeVisible();
+		});
+
+		it('should have submit button', async() => {
+			await waitFor(element(by.id('profile-view-submit'))).toBeVisible().whileElement(by.id('profile-view-list')).scroll(scrollDown, 'down');
+			await expect(element(by.id('profile-view-submit'))).toBeVisible();
+		});
+
+		after(async() => {
+			takeScreenshot();
+		});
+	});
+
+	describe('Usage', async() => {
+		it('should change name and username', async() => {
+			await element(by.id('profile-view-list')).swipe('down');
+			await element(by.id('profile-view-name')).replaceText(`${ data.user }new`);
+			await element(by.id('profile-view-username')).replaceText(`${ data.user }new`);
+			await element(by.id('profile-view-list')).swipe('up');
+			await element(by.id('profile-view-submit')).tap();
+			await waitFor(element(by.text('Profile saved successfully!'))).toBeVisible().withTimeout(60000);
+			await expect(element(by.text('Profile saved successfully!'))).toBeVisible();
+			await waitFor(element(by.text('Profile saved successfully!'))).toBeNotVisible().withTimeout(10000);
+			await expect(element(by.text('Profile saved successfully!'))).toBeNotVisible();
+		});
+
+		it('should change email and password', async() => {
+			await element(by.id('profile-view-email')).replaceText(`test${ data.email }`);
+			await element(by.id('profile-view-new-password')).replaceText(`${ data.password }new`);
+			await element(by.id('profile-view-submit')).tap();
+			await waitFor(element(by.id('profile-view-typed-password'))).toBeVisible().withTimeout(10000);
+			await expect(element(by.id('profile-view-typed-password'))).toBeVisible();
+			await element(by.id('profile-view-typed-password')).replaceText(`${ data.password }`);
+			await element(by.text('Save')).tap();
+			await waitFor(element(by.text('Profile saved successfully!'))).toBeVisible().withTimeout(60000);
+			await expect(element(by.text('Profile saved successfully!'))).toBeVisible();
+			await waitFor(element(by.text('Profile saved successfully!'))).toBeNotVisible().withTimeout(10000);
+			await expect(element(by.text('Profile saved successfully!'))).toBeNotVisible();
+		});
+
+		it('should reset avatar', async() => {
+			await element(by.id('profile-view-list')).swipe('up');
+			await element(by.id('profile-view-reset-avatar')).tap();
+			await waitFor(element(by.text('Avatar changed successfully!'))).toBeVisible().withTimeout(60000);
+			await expect(element(by.text('Avatar changed successfully!'))).toBeVisible();
+			await waitFor(element(by.text('Avatar changed successfully!'))).toBeNotVisible().withTimeout(10000);
+			await expect(element(by.text('Avatar changed successfully!'))).toBeNotVisible();
+		});
+
+		after(async() => {
+			takeScreenshot();
+		});
+	});
+});
diff --git a/package-lock.json b/package-lock.json
index e0d2ec7de..1458aa307 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10764,6 +10764,11 @@
       "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
       "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ=="
     },
+    "js-sha256": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+      "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+    },
     "js-tokens": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@@ -11116,6 +11121,11 @@
       "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz",
       "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U="
     },
+    "lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+    },
     "lodash.isplainobject": {
       "version": "4.0.6",
       "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -14980,6 +14990,26 @@
         "prop-types": "15.6.1"
       }
     },
+    "react-native-dialog": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-4.0.0.tgz",
+      "integrity": "sha512-BQ2nR2ISDohgSZ/9V34o66FbuuIlJvjOb8FXMoc69aP+fuZt9JA0AiPn2kjQpryfQtTMnFNjcPTDJjXLylSLsw==",
+      "requires": {
+        "prop-types": "15.6.1",
+        "react-native-modal": "5.4.0"
+      },
+      "dependencies": {
+        "react-native-modal": {
+          "version": "5.4.0",
+          "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-5.4.0.tgz",
+          "integrity": "sha512-Bvq4FQPMAFijqjqNX6TxLgKOwdbruM6GvFwF9rb+mowbaFZVoYbHTKLaAbdPlrblgaZKWyOuuxBUoDx41+Xktg==",
+          "requires": {
+            "prop-types": "15.6.1",
+            "react-native-animatable": "1.2.4"
+          }
+        }
+      }
+    },
     "react-native-dismiss-keyboard": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/react-native-dismiss-keyboard/-/react-native-dismiss-keyboard-1.0.0.tgz",
@@ -15127,6 +15157,14 @@
         "prop-types": "15.6.1"
       }
     },
+    "react-native-picker-select": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-3.1.1.tgz",
+      "integrity": "sha512-zuASTVjdW9fkT1NXMGguLwL2bmiZH0AXATAAKPAS/Rqu5/4GRhwJ+HFwnSL+rGYaGTh4Q2vMlox4cmfSv0IIFQ==",
+      "requires": {
+        "lodash.isequal": "4.5.0"
+      }
+    },
     "react-native-push-notification": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/react-native-push-notification/-/react-native-push-notification-3.0.2.tgz",
diff --git a/package.json b/package.json
index 1bf7ec01a..c2faafedd 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "deep-equal": "^1.0.1",
     "ejson": "^2.1.2",
     "js-base64": "^2.4.5",
+    "js-sha256": "^0.9.0",
     "lodash": "^4.17.10",
     "markdown-it-flowdock": "^0.3.7",
     "moment": "^2.22.2",
@@ -44,6 +45,7 @@
     "react-native-action-button": "^2.8.3",
     "react-native-actionsheet": "^2.4.2",
     "react-native-audio": "^4.1.3",
+    "react-native-dialog": "^4.0.0",
     "react-native-fabric": "^0.5.1",
     "react-native-fast-image": "^4.0.14",
     "react-native-fetch-blob": "^0.10.8",
@@ -56,6 +58,7 @@
     "react-native-meteor": "^1.3.0",
     "react-native-modal": "^6.1.0",
     "react-native-optimized-flatlist": "^1.0.4",
+    "react-native-picker-select": "^3.1.1",
     "react-native-push-notification": "^3.0.1",
     "react-native-responsive-ui": "^1.1.1",
     "react-native-safari-view": "^2.1.0",
-- 
GitLab