diff --git a/README.md b/README.md
index dd5376ae6e181bc219c426d48b0a6d8f7eecdea8..aa58923ab47499f80e504fd33d9b451cd376910d 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,8 @@
 [![CodeFactor](https://www.codefactor.io/repository/github/rocketchat/rocket.chat.reactnative/badge)](https://www.codefactor.io/repository/github/rocketchat/rocket.chat.reactnative)
 [![Known Vulnerabilities](https://snyk.io/test/github/rocketchat/rocket.chat.reactnative/badge.svg)](https://snyk.io/test/github/rocketchat/rocket.chat.reactnative)
 
+**Supported Server Versions:** 0.66.0+
+
 ## Download
 <a href="https://play.google.com/store/apps/details?id=chat.rocket.reactnative">
   <img alt="Download on Google Play" src="https://play.google.com/intl/en_us/badges/images/badge_new.png" height=43>
diff --git a/app/containers/Avatar.js b/app/containers/Avatar.js
index fc531de1f584484033ca3946f3e7c711ceed35c7..ab858e456285f13d014f3f1ed0e270be881ab23e 100644
--- a/app/containers/Avatar.js
+++ b/app/containers/Avatar.js
@@ -1,9 +1,9 @@
-import React from 'react';
+import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
 import { View, ViewPropTypes } from 'react-native';
 import FastImage from 'react-native-fast-image';
 
-export default class Avatar extends React.PureComponent {
+export default class Avatar extends PureComponent {
 	static propTypes = {
 		baseUrl: PropTypes.string.isRequired,
 		style: ViewPropTypes.style,
diff --git a/app/containers/EmojiPicker/TabBar.js b/app/containers/EmojiPicker/TabBar.js
index 494d547fd8aa08fbec062e333804b362f3945c4e..c298484baa4365687a6b83d82ea575e16e319112 100644
--- a/app/containers/EmojiPicker/TabBar.js
+++ b/app/containers/EmojiPicker/TabBar.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { View, TouchableOpacity, Text } from 'react-native';
 import styles from './styles';
 
-export default class TabBar extends React.PureComponent {
+export default class TabBar extends React.Component {
 	static propTypes = {
 		goToPage: PropTypes.func,
 		activeTab: PropTypes.number,
@@ -11,6 +11,14 @@ export default class TabBar extends React.PureComponent {
 		tabEmojiStyle: PropTypes.object
 	}
 
+	shouldComponentUpdate(nextProps) {
+		const { activeTab } = this.props;
+		if (nextProps.activeTab !== activeTab) {
+			return true;
+		}
+		return false;
+	}
+
 	render() {
 		const {
 			tabs, goToPage, tabEmojiStyle, activeTab
diff --git a/app/containers/EmojiPicker/index.js b/app/containers/EmojiPicker/index.js
index d8cbdaf55ae37dcf31b2f1eb9ac0ffd6511ccb82..9860dea979aa71d8dce9068b246773f2ccd008eb 100644
--- a/app/containers/EmojiPicker/index.js
+++ b/app/containers/EmojiPicker/index.js
@@ -4,6 +4,8 @@ import { ScrollView } from 'react-native';
 import ScrollableTabView from 'react-native-scrollable-tab-view';
 import map from 'lodash/map';
 import { emojify } from 'react-emojione';
+import equal from 'deep-equal';
+
 import TabBar from './TabBar';
 import EmojiCategory from './EmojiCategory';
 import styles from './styles';
@@ -28,26 +30,41 @@ export default class EmojiPicker extends Component {
 
 	constructor(props) {
 		super(props);
+		this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
+		this.customEmojis = database.objects('customEmojis');
 		this.state = {
 			frequentlyUsed: [],
-			customEmojis: []
+			customEmojis: [],
+			show: false
 		};
-		this.frequentlyUsed = database.objects('frequentlyUsedEmoji').sorted('count', true);
-		this.customEmojis = database.objects('customEmojis');
 		this.updateFrequentlyUsed = this.updateFrequentlyUsed.bind(this);
 		this.updateCustomEmojis = this.updateCustomEmojis.bind(this);
 	}
-	//
-	// shouldComponentUpdate(nextProps) {
-	// 	return false;
-	// }
 
 	componentDidMount() {
+		this.updateFrequentlyUsed();
+		this.updateCustomEmojis();
 		requestAnimationFrame(() => this.setState({ show: true }));
 		this.frequentlyUsed.addListener(this.updateFrequentlyUsed);
 		this.customEmojis.addListener(this.updateCustomEmojis);
-		this.updateFrequentlyUsed();
-		this.updateCustomEmojis();
+	}
+
+	shouldComponentUpdate(nextProps, nextState) {
+		const { frequentlyUsed, customEmojis, show } = this.state;
+		const { width } = this.props;
+		if (nextState.show !== show) {
+			return true;
+		}
+		if (nextProps.width !== width) {
+			return true;
+		}
+		if (!equal(nextState.frequentlyUsed, frequentlyUsed)) {
+			return true;
+		}
+		if (!equal(nextState.customEmojis, customEmojis)) {
+			return true;
+		}
+		return false;
 	}
 
 	componentWillUnmount() {
diff --git a/app/containers/MessageBox/FilesActions.js b/app/containers/MessageBox/FilesActions.js
index fee1494576f49ec89b1bb2d844e95553310b54d9..8973595fe075d5d75f4c067a5ea4406b03bcd3ac 100644
--- a/app/containers/MessageBox/FilesActions.js
+++ b/app/containers/MessageBox/FilesActions.js
@@ -1,10 +1,10 @@
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
 import ActionSheet from 'react-native-actionsheet';
 
 import I18n from '../../i18n';
 
-export default class FilesActions extends Component {
+export default class FilesActions extends PureComponent {
 	static propTypes = {
 		hideActions: PropTypes.func.isRequired,
 		takePhoto: PropTypes.func.isRequired,
diff --git a/app/containers/MessageBox/ReplyPreview.js b/app/containers/MessageBox/ReplyPreview.js
index b0c8e076434fc76f6dabaf3861538d2c8949c573..8246f0801df194f2ebb89c16db5d5b0ee599ed7c 100644
--- a/app/containers/MessageBox/ReplyPreview.js
+++ b/app/containers/MessageBox/ReplyPreview.js
@@ -56,6 +56,10 @@ export default class ReplyPreview extends Component {
 		username: PropTypes.string.isRequired
 	}
 
+	shouldComponentUpdate() {
+		return false;
+	}
+
 	close = () => {
 		const { close } = this.props;
 		close();
diff --git a/app/containers/MessageBox/UploadModal.js b/app/containers/MessageBox/UploadModal.js
index aa4ec42e06bcdc54a5025dfbb919da15c4c10eb5..06417a0b8dd44f4d9f85f998f6075c1909a5fa14 100644
--- a/app/containers/MessageBox/UploadModal.js
+++ b/app/containers/MessageBox/UploadModal.js
@@ -90,6 +90,28 @@ export default class UploadModal extends Component {
 		return null;
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { name, description, file } = this.state;
+		const { window, isVisible } = this.props;
+
+		if (nextState.name !== name) {
+			return true;
+		}
+		if (nextState.description !== description) {
+			return true;
+		}
+		if (nextProps.isVisible !== isVisible) {
+			return true;
+		}
+		if (nextProps.window.width !== window.width) {
+			return true;
+		}
+		if (!equal(nextState.file, file)) {
+			return true;
+		}
+		return false;
+	}
+
 	submit = () => {
 		const { file, submit } = this.props;
 		const { name, description } = this.state;
@@ -162,12 +184,12 @@ export default class UploadModal extends Component {
 					<ScrollView style={styles.scrollView}>
 						<Image source={{ isStatic: true, uri: file.path }} style={styles.image} />
 						<TextInput
-							placeholder='File name'
+							placeholder={I18n.t('File_name')}
 							value={name}
 							onChangeText={value => this.setState({ name: value })}
 						/>
 						<TextInput
-							placeholder='File description'
+							placeholder={I18n.t('File_description')}
 							value={description}
 							onChangeText={value => this.setState({ description: value })}
 						/>
diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js
index b92c68acd5011ce7a20ad81834d49c9e220d2c2a..8635ab9149180808a7bc3d06d42845c5c03900d1 100644
--- a/app/containers/MessageBox/index.js
+++ b/app/containers/MessageBox/index.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import {
 	View, TextInput, FlatList, Text, TouchableOpacity, Alert, Image
@@ -9,6 +9,7 @@ import { emojify } from 'react-emojione';
 import { KeyboardAccessoryView } from 'react-native-keyboard-input';
 import ImagePicker from 'react-native-image-crop-picker';
 import { BorderlessButton } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import { userTyping as userTypingAction } from '../../actions/room';
 import {
@@ -59,7 +60,7 @@ const imagePickerConfig = {
 	typing: status => dispatch(userTypingAction(status)),
 	closeReply: () => dispatch(replyCancelAction())
 }))
-export default class MessageBox extends React.PureComponent {
+export default class MessageBox extends Component {
 	static propTypes = {
 		rid: PropTypes.string.isRequired,
 		baseUrl: PropTypes.string.isRequired,
@@ -108,6 +109,43 @@ export default class MessageBox extends React.PureComponent {
 		}
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
+		} = this.state;
+		const {
+			roomType, replying, editing
+		} = this.props;
+		if (nextProps.roomType !== roomType) {
+			return true;
+		}
+		if (nextProps.replying !== replying) {
+			return true;
+		}
+		if (nextProps.editing !== editing) {
+			return true;
+		}
+		if (nextState.showEmojiKeyboard !== showEmojiKeyboard) {
+			return true;
+		}
+		if (nextState.showFilesAction !== showFilesAction) {
+			return true;
+		}
+		if (nextState.showSend !== showSend) {
+			return true;
+		}
+		if (nextState.recording !== recording) {
+			return true;
+		}
+		if (!equal(nextState.mentions, mentions)) {
+			return true;
+		}
+		if (!equal(nextState.file, file)) {
+			return true;
+		}
+		return false;
+	}
+
 	onChangeText(text) {
 		const { typing } = this.props;
 
diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js
index a9ff4af78a66db2809ddeeb0bdc2feda8cafe1ed..711df2ffb0959fb4271291d3d5ffa728780c4aeb 100644
--- a/app/containers/Sidebar.js
+++ b/app/containers/Sidebar.js
@@ -6,6 +6,7 @@ import {
 import { connect } from 'react-redux';
 import Icon from 'react-native-vector-icons/MaterialIcons';
 import { Navigation } from 'react-native-navigation';
+import equal from 'deep-equal';
 
 import { setStackRoot as setStackRootAction } from '../actions';
 import { logout as logoutAction } from '../actions/login';
@@ -111,7 +112,8 @@ export default class Sidebar extends Component {
 	constructor(props) {
 		super(props);
 		this.state = {
-			showStatus: false
+			showStatus: false,
+			status: []
 		};
 		Navigation.events().bindComponent(this);
 	}
@@ -127,6 +129,43 @@ export default class Sidebar extends Component {
 		}
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { status, showStatus } = this.state;
+		const {
+			Site_Name, stackRoot, user, baseUrl
+		} = this.props;
+		if (nextState.showStatus !== showStatus) {
+			return true;
+		}
+		if (nextProps.Site_Name !== Site_Name) {
+			return true;
+		}
+		if (nextProps.stackRoot !== stackRoot) {
+			return true;
+		}
+		if (nextProps.Site_Name !== Site_Name) {
+			return true;
+		}
+		if (nextProps.baseUrl !== baseUrl) {
+			return true;
+		}
+		if (nextProps.user && user) {
+			if (nextProps.user.language !== user.language) {
+				return true;
+			}
+			if (nextProps.user.status !== user.status) {
+				return true;
+			}
+			if (nextProps.user.username !== user.username) {
+				return true;
+			}
+		}
+		if (!equal(nextState.status, status)) {
+			return true;
+		}
+		return false;
+	}
+
 	handleChangeStack = (event) => {
 		const { stack } = event;
 		this.setStack(stack);
@@ -140,22 +179,20 @@ export default class Sidebar extends Component {
 	}
 
 	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')
-				}]
-			});
+		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')
+			}]
 		});
 	}
 
diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js
index 0037eb57e6749332a64a6cdcd2acb7e5cc04ddf0..1a4b84fec2ffaa5116a275acd7de4c150cd0d5be 100644
--- a/app/containers/message/Audio.js
+++ b/app/containers/message/Audio.js
@@ -7,6 +7,7 @@ import Video from 'react-native-video';
 import Slider from 'react-native-slider';
 import moment from 'moment';
 import { BorderlessButton } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import Markdown from './Markdown';
 
@@ -47,7 +48,7 @@ const styles = StyleSheet.create({
 
 const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
 
-export default class Audio extends React.PureComponent {
+export default class Audio extends React.Component {
 	static propTypes = {
 		file: PropTypes.object.isRequired,
 		baseUrl: PropTypes.string.isRequired,
@@ -69,6 +70,29 @@ export default class Audio extends React.PureComponent {
 		};
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			currentTime, duration, paused, uri
+		} = this.state;
+		const { file } = this.props;
+		if (nextState.currentTime !== currentTime) {
+			return true;
+		}
+		if (nextState.duration !== duration) {
+			return true;
+		}
+		if (nextState.paused !== paused) {
+			return true;
+		}
+		if (nextState.uri !== uri) {
+			return true;
+		}
+		if (!equal(nextProps.file, file)) {
+			return true;
+		}
+		return false;
+	}
+
 	onLoad(data) {
 		this.setState({ duration: data.duration > 0 ? data.duration : 0 });
 	}
diff --git a/app/containers/message/Image.js b/app/containers/message/Image.js
index 6b81127dda38ac4508054e20d9de57a98c3d775c..c9eea974c820884a67785c86f25c2078deb0ad80 100644
--- a/app/containers/message/Image.js
+++ b/app/containers/message/Image.js
@@ -1,13 +1,14 @@
+import React, { Component } from 'react';
 import PropTypes from 'prop-types';
-import React from 'react';
 import FastImage from 'react-native-fast-image';
 import { RectButton } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import PhotoModal from './PhotoModal';
 import Markdown from './Markdown';
 import styles from './styles';
 
-export default class extends React.PureComponent {
+export default class extends Component {
 	static propTypes = {
 		file: PropTypes.object.isRequired,
 		baseUrl: PropTypes.string.isRequired,
@@ -18,7 +19,22 @@ export default class extends React.PureComponent {
 		])
 	}
 
-	state = { modalVisible: false };
+	state = { modalVisible: false, isPressed: false };
+
+	shouldComponentUpdate(nextProps, nextState) {
+		const { modalVisible, isPressed } = this.state;
+		const { file } = this.props;
+		if (nextState.modalVisible !== modalVisible) {
+			return true;
+		}
+		if (nextState.isPressed !== isPressed) {
+			return true;
+		}
+		if (!equal(nextProps.file, file)) {
+			return true;
+		}
+		return false;
+	}
 
 	onPressButton() {
 		this.setState({
diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index 8e3487be4ef299166db6fb1c2bb24ddd2844c2a9..fb7fdbd8145499bca530f5c4891ea51b3feb09a4 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -155,6 +155,8 @@ export default {
 	Error_uploading: 'Error uploading',
 	Favorites: 'Favorites',
 	Files: 'Files',
+	File_description: 'File description',
+	File_name: 'File name',
 	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',
@@ -170,6 +172,7 @@ export default {
 	is_a_valid_RocketChat_instance: 'is a valid Rocket.Chat instance',
 	is_not_a_valid_RocketChat_instance: 'is not a valid Rocket.Chat instance',
 	is_typing: 'is typing',
+	Invalid_server_version: 'The server you\'re trying to connect is using a version that\'s not supported by the app anymore: {{currentVersion}}.\n\nWe require version {{minVersion}}',
 	Join_the_community: 'Join the community',
 	Join: 'Join',
 	Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index f7e49cf899d9ff6d6c153b2f5060bf25ea27975d..9f0b91c9c25d1e94ddd0d5b56486bfbe758fa5cc 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -162,6 +162,8 @@ export default {
 	Error_uploading: 'Erro subindo',
 	Favorites: 'Favoritos',
 	Files: 'Arquivos',
+	File_description: 'Descrição do arquivo',
+	File_name: 'Nome do arquivo',
 	Finish_recording: 'Encerrar gravação',
 	For_your_security_you_must_enter_your_current_password_to_continue: 'Para sua segurança, você precisa digitar sua senha',
 	Forgot_my_password: 'Esqueci minha senha',
@@ -175,6 +177,7 @@ export default {
 	Invisible: 'Invisível',
 	Invite: 'Convidar',
 	is_typing: 'está digitando',
+	Invalid_server_version: 'O servidor que você está conectando não é suportado mais por esta versão do aplicativo: {{currentVersion}}.\n\nEsta versão do aplicativo requer a versão {{minVersion}} do servidor para funcionar corretamente.',
 	Join_the_community: 'Junte-se à comunidade',
 	Join: 'Entrar',
 	Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',
diff --git a/app/lib/methods/canOpenRoom.js b/app/lib/methods/canOpenRoom.js
index f4a16e8d23a508b232fd2c25af06f83ca7493672..12b28d6281f0e0ce967a6c08382ab1be0821aaa1 100644
--- a/app/lib/methods/canOpenRoom.js
+++ b/app/lib/methods/canOpenRoom.js
@@ -8,6 +8,7 @@ const restTypes = {
 
 async function open({ type, rid }) {
 	try {
+		// RC 0.61.0
 		await SDK.api.post(`${ restTypes[type] }.open`, { roomId: rid });
 		return true;
 	} catch (e) {
diff --git a/app/lib/methods/getCustomEmojis.js b/app/lib/methods/getCustomEmojis.js
index 45d5994f5fd5667010f0b77e0fd72b4f096a9225..32c15924006f891f17b4fddade13e9860ce3783b 100644
--- a/app/lib/methods/getCustomEmojis.js
+++ b/app/lib/methods/getCustomEmojis.js
@@ -16,6 +16,7 @@ const getLastMessage = () => {
 export default async function() {
 	try {
 		const lastMessage = getLastMessage();
+		// RC 0.61.0
 		const result = await SDK.api.get('emoji-custom');
 		let { emojis } = result;
 		emojis = emojis.filter(emoji => !lastMessage || emoji._updatedAt > lastMessage);
diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js
index 0c1e40e90a5268777153cd33f6a5b9bbdbc64c8a..95939e4dab17b43235578efa681070aa92e1c663 100644
--- a/app/lib/methods/getPermissions.js
+++ b/app/lib/methods/getPermissions.js
@@ -7,6 +7,7 @@ import defaultPermissions from '../../constants/permissions';
 
 export default async function() {
 	try {
+		// RC 0.66.0
 		const result = await SDK.api.get('permissions.list');
 
 		if (!result.success) {
diff --git a/app/lib/methods/getRooms.js b/app/lib/methods/getRooms.js
index f43316a5abcbc3e85871708a77076965556d37b6..fe1fbdec097ae058668ef55bc866662e9d562168 100644
--- a/app/lib/methods/getRooms.js
+++ b/app/lib/methods/getRooms.js
@@ -18,6 +18,8 @@ export default function() {
 	return new Promise(async(resolve, reject) => {
 		try {
 			const updatedSince = lastMessage();
+			// subscriptions.get: Since RC 0.60.0
+			// rooms.get: Since RC 0.62.0
 			const [subscriptionsResult, roomsResult] = await (updatedSince
 				? Promise.all([SDK.api.get('subscriptions.get', { updatedSince }), SDK.api.get('rooms.get', { updatedSince })])
 				: Promise.all([SDK.api.get('subscriptions.get'), SDK.api.get('rooms.get')])
diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js
index 38a79b09f2015003a8132c892133489976d34fa4..dd85f3761afd323c6c3917aa34cb0ed793d8dba8 100644
--- a/app/lib/methods/getSettings.js
+++ b/app/lib/methods/getSettings.js
@@ -16,6 +16,7 @@ function updateServer(param) {
 export default async function() {
 	try {
 		const settingsParams = JSON.stringify(Object.keys(settings));
+		// RC 0.60.0
 		const result = await fetch(`${ SDK.api.url }settings.public?query={"_id":{"$in":${ settingsParams }}}`).then(response => response.json());
 
 		if (!result.success) {
diff --git a/app/lib/methods/loadMessagesForRoom.js b/app/lib/methods/loadMessagesForRoom.js
index 1258769c0fa912d0fd12e28d411597c579aaaf5a..c1589172487cbb2ffac52ebdd18a1d24624de74a 100644
--- a/app/lib/methods/loadMessagesForRoom.js
+++ b/app/lib/methods/loadMessagesForRoom.js
@@ -8,6 +8,7 @@ import log from '../../utils/log';
 async function load({ rid: roomId, latest, t }) {
 	if (t === 'l') {
 		try {
+			// RC 0.51.0
 			const data = await SDK.driver.asyncCall('loadHistory', roomId, null, 50, latest);
 			if (!data || data.status === 'error') {
 				return [];
@@ -23,6 +24,7 @@ async function load({ rid: roomId, latest, t }) {
 	if (latest) {
 		params = { ...params, latest: new Date(latest).toISOString() };
 	}
+	// RC 0.48.0
 	const data = await SDK.api.get(`${ this.roomTypeToApiType(t) }.history`, params);
 	if (!data || data.status === 'error') {
 		return [];
diff --git a/app/lib/methods/loadMissedMessages.js b/app/lib/methods/loadMissedMessages.js
index 0ad952cdcfd20c61a8e7e8d11c5c3cd87371ea7d..243b84a0223938e7597bbb3412cf6c6f7403d84b 100644
--- a/app/lib/methods/loadMissedMessages.js
+++ b/app/lib/methods/loadMissedMessages.js
@@ -12,6 +12,7 @@ async function load({ rid: roomId, lastOpen }) {
 	} else {
 		return [];
 	}
+	// RC 0.60.0
 	const { result } = await SDK.api.get('chat.syncMessages', { roomId, lastUpdate, count: 50 });
 	return result;
 }
diff --git a/app/lib/methods/readMessages.js b/app/lib/methods/readMessages.js
index 1f4562b8687d27a811b56ce9711e0b86f33d209f..01dd063718c0b5c6ee595fb559607477376243c3 100644
--- a/app/lib/methods/readMessages.js
+++ b/app/lib/methods/readMessages.js
@@ -6,6 +6,7 @@ import log from '../../utils/log';
 export default async function readMessages(rid) {
 	const ls = new Date();
 	try {
+		// RC 0.61.0
 		const data = await SDK.api.post('subscriptions.read', { rid });
 		const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid);
 		database.write(() => {
diff --git a/app/lib/methods/sendFileMessage.js b/app/lib/methods/sendFileMessage.js
index 57102eeb3814909d3a943b5fa0ee290a9193fc17..29a3005ba01e87fd9026fffa95c11c25f60e93bb 100644
--- a/app/lib/methods/sendFileMessage.js
+++ b/app/lib/methods/sendFileMessage.js
@@ -15,6 +15,7 @@ function _ufsComplete(fileId, store, token) {
 }
 
 function _sendFileMessage(rid, data, msg = {}) {
+	// RC 0.22.0
 	return SDK.driver.asyncCall('sendFileMessage', rid, null, data, msg);
 }
 
diff --git a/app/lib/methods/sendMessage.js b/app/lib/methods/sendMessage.js
index 21b620d97015dbe25261f424fa44a096d62cd557..cd9c56202cedc9bc666272966c557c4894661aca 100644
--- a/app/lib/methods/sendMessage.js
+++ b/app/lib/methods/sendMessage.js
@@ -33,6 +33,7 @@ export const getMessage = (rid, msg = {}) => {
 
 export async function sendMessageCall(message) {
 	const { _id, rid, msg } = message;
+	// RC 0.60.0
 	const data = await SDK.api.post('chat.sendMessage', { message: { _id, rid, msg } });
 	return data;
 }
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index e5bab991907604f9b89d6059dda2c97efb6c1d57..21af17e279b71d1b18f239746f65766159f545a7 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -1,6 +1,7 @@
 import { AsyncStorage, Platform } from 'react-native';
 import foreach from 'lodash/forEach';
 import * as SDK from '@rocket.chat/sdk';
+import semver from 'semver';
 
 import reduxStore from './createStore';
 import defaultSettings from '../constants/settings';
@@ -42,6 +43,7 @@ const TOKEN_KEY = 'reactnativemeteor_usertoken';
 const SORT_PREFS_KEY = 'RC_SORT_PREFS_KEY';
 const call = (method, ...params) => SDK.driver.asyncCall(method, ...params);
 const returnAnArray = obj => obj || [];
+const MIN_ROCKETCHAT_VERSION = '0.66.0';
 
 const RocketChat = {
 	TOKEN_KEY,
@@ -51,6 +53,7 @@ const RocketChat = {
 	createChannel({
 		name, users, type, readOnly, broadcast
 	}) {
+		// RC 0.51.0
 		return call(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
 	},
 	async createDirectMessageAndWait(username) {
@@ -81,12 +84,27 @@ const RocketChat = {
 		try {
 			const result = await fetch(`${ server }/api/v1/info`).then(response => response.json());
 			if (result.success && result.info) {
-				return server;
+				if (semver.lt(result.info.version, MIN_ROCKETCHAT_VERSION)) {
+					return {
+						success: false,
+						message: 'Invalid_server_version',
+						messageOptions: {
+							currentVersion: result.info.version,
+							minVersion: MIN_ROCKETCHAT_VERSION
+						}
+					};
+				}
+				return {
+					success: true
+				};
 			}
 		} catch (e) {
 			log('testServer', e);
 		}
-		throw new Error({ error: 'invalid server' });
+		return {
+			success: false,
+			message: 'The_URL_is_invalid'
+		};
 	},
 	_setUser(ddpMessage) {
 		this.activeUsers = this.activeUsers || {};
@@ -235,14 +253,17 @@ const RocketChat = {
 	},
 
 	register(credentials) {
+		// RC 0.50.0
 		return SDK.api.post('users.register', credentials, false);
 	},
 
 	setUsername(username) {
+		// RC 0.51.0
 		return call('setUsername', username);
 	},
 
 	forgotPassword(email) {
+		// RC 0.64.0
 		return SDK.api.post('users.forgotPassword', { email }, false);
 	},
 
@@ -288,6 +309,7 @@ const RocketChat = {
 
 	async login(params) {
 		try {
+			// RC 0.64.0
 			return await SDK.api.login(params);
 		} catch (e) {
 			reduxStore.dispatch(loginFailure(e));
@@ -302,6 +324,7 @@ const RocketChat = {
 			console.log('logout -> removePushToken -> catch -> error', error);
 		}
 		try {
+			// RC 0.60.0
 			await SDK.api.logout();
 		} catch (error) {
 			console.log('​logout -> api logout -> catch -> error', error);
@@ -343,6 +366,7 @@ const RocketChat = {
 					type,
 					appName: 'chat.rocket.reactnative' // TODO: try to get from config file
 				};
+				// RC 0.60.0
 				return SDK.api.post('push.token', data);
 			}
 			return resolve();
@@ -351,6 +375,7 @@ const RocketChat = {
 	removePushToken() {
 		const token = getDeviceToken();
 		if (token) {
+			// RC 0.60.0
 			return SDK.api.del('push.token', { token });
 		}
 		return Promise.resolve();
@@ -430,14 +455,17 @@ const RocketChat = {
 	},
 
 	spotlight(search, usernames, type) {
+		// RC 0.51.0
 		return call('spotlight', search, usernames, type);
 	},
 
 	createDirectMessage(username) {
+		// RC 0.59.0
 		return SDK.api.post('im.create', { username });
 	},
 	joinRoom(roomId) {
 		// TODO: join code
+		// RC 0.48.0
 		return SDK.api.post('channels.join', { roomId });
 	},
 	sendFileMessage,
@@ -471,22 +499,28 @@ const RocketChat = {
 	},
 	deleteMessage(message) {
 		const { _id, rid } = message;
+		// RC 0.48.0
 		return SDK.api.post('chat.delete', { roomId: rid, msgId: _id });
 	},
 	editMessage(message) {
 		const { _id, msg, rid } = message;
+		// RC 0.49.0
 		return SDK.api.post('chat.update', { roomId: rid, msgId: _id, text: msg });
 	},
 	toggleStarMessage(message) {
 		if (message.starred) {
+			// RC 0.59.0
 			return SDK.api.post('chat.unStarMessage', { messageId: message._id });
 		}
+		// RC 0.59.0
 		return SDK.api.post('chat.starMessage', { messageId: message._id });
 	},
 	togglePinMessage(message) {
 		if (message.pinned) {
+			// RC 0.59.0
 			return SDK.api.post('chat.unPinMessage', { messageId: message._id });
 		}
+		// RC 0.59.0
 		return SDK.api.post('chat.pinMessage', { messageId: message._id });
 	},
 	getRoom(rid) {
@@ -496,9 +530,6 @@ const RocketChat = {
 		}
 		return Promise.resolve(result);
 	},
-	getRoomInfo(roomId) {
-		return SDK.api.get('rooms.info', { roomId });
-	},
 	async getPermalink(message) {
 		let room;
 		try {
@@ -535,22 +566,30 @@ const RocketChat = {
 		return call('UserPresence:setDefaultStatus', status);
 	},
 	setReaction(emoji, messageId) {
+		// RC 0.62.2
 		return SDK.api.post('chat.react', { emoji, messageId });
 	},
 	toggleFavorite(roomId, favorite) {
+		// RC 0.64.0
 		return SDK.api.post('rooms.favorite', { roomId, favorite });
 	},
 	getRoomMembers(rid, allUsers) {
+		// RC 0.42.0
 		return call('getUsersOfRoom', rid, allUsers);
 	},
 	getUserRoles() {
+		// RC 0.27.0
 		return call('getUserRoles');
 	},
 	getRoomCounters(roomId, t) {
+		// RC 0.65.0
 		return SDK.api.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId });
 	},
 	async getRoomMember(rid, currentUserId) {
 		try {
+			if (rid === `${ currentUserId }${ currentUserId }`) {
+				return Promise.resolve(currentUserId);
+			}
 			const membersResult = await RocketChat.getRoomMembers(rid, true);
 			return Promise.resolve(membersResult.records.find(m => m._id !== currentUserId));
 		} catch (error) {
@@ -559,53 +598,71 @@ const RocketChat = {
 	},
 	toggleBlockUser(rid, blocked, block) {
 		if (block) {
+			// RC 0.49.0
 			return call('blockUser', { rid, blocked });
 		}
+		// RC 0.49.0
 		return call('unblockUser', { rid, blocked });
 	},
 	leaveRoom(roomId, t) {
+		// RC 0.48.0
 		return SDK.api.post(`${ this.roomTypeToApiType(t) }.leave`, { roomId });
 	},
 	eraseRoom(roomId, t) {
+		// RC 0.49.0
 		return SDK.api.post(`${ this.roomTypeToApiType(t) }.delete`, { roomId });
 	},
 	toggleMuteUserInRoom(rid, username, mute) {
 		if (mute) {
+			// RC 0.51.0
 			return call('muteUserInRoom', { rid, username });
 		}
+		// RC 0.51.0
 		return call('unmuteUserInRoom', { rid, username });
 	},
 	toggleArchiveRoom(roomId, t, archive) {
 		if (archive) {
+			// RC 0.48.0
 			return SDK.api.post(`${ this.roomTypeToApiType(t) }.archive`, { roomId });
 		}
+		// RC 0.48.0
 		return SDK.api.post(`${ this.roomTypeToApiType(t) }.unarchive`, { roomId });
 	},
 	saveRoomSettings(rid, params) {
+		// RC 0.55.0
 		return call('saveRoomSettings', rid, params);
 	},
 	saveUserProfile(data) {
+		// RC 0.62.2
 		return SDK.api.post('users.updateOwnBasicInfo', { data });
 	},
 	saveUserPreferences(params) {
+		// RC 0.51.0
 		return call('saveUserPreferences', params);
 	},
 	saveNotificationSettings(roomId, notifications) {
+		// RC 0.63.0
 		return SDK.api.post('rooms.saveNotification', { roomId, notifications });
 	},
 	addUsersToRoom(rid) {
 		let { users } = reduxStore.getState().selectedUsers;
 		users = users.map(u => u.name);
+		// RC 0.51.0
 		return call('addUsersToRoom', { rid, users });
 	},
 	hasPermission(permissions, rid) {
-		// get the room from realm
-		const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
+		let roles = [];
+		try {
+			// get the room from realm
+			const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
+			// get room roles
+			roles = room.roles; // eslint-disable-line prefer-destructuring
+		} catch (error) {
+			console.log('hasPermission -> error', error);
+		}
 		// get permissions from realm
 		const permissionsFiltered = database.objects('permissions')
 			.filter(permission => permissions.includes(permission._id));
-		// get room roles
-		const { roles } = room;
 		// transform room roles to array
 		const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
 		// get user roles on the server from redux
@@ -625,12 +682,15 @@ const RocketChat = {
 		}, {});
 	},
 	getAvatarSuggestion() {
+		// RC 0.51.0
 		return call('getAvatarSuggestion');
 	},
 	resetAvatar(userId) {
+		// RC 0.55.0
 		return SDK.api.post('users.resetAvatar', { userId });
 	},
 	setAvatarFromService({ data, contentType = '', service = null }) {
+		// RC 0.51.0
 		return call('setAvatarFromService', data, contentType, service);
 	},
 	async getSortPreferences() {
@@ -668,6 +728,7 @@ const RocketChat = {
 		}
 	},
 	getUsernameSuggestion() {
+		// RC 0.65.0
 		return SDK.api.get('users.getUsernameSuggestion');
 	},
 	roomTypeToApiType(t) {
@@ -677,6 +738,7 @@ const RocketChat = {
 		return types[t];
 	},
 	getFiles(roomId, type, offset) {
+		// RC 0.59.0
 		return SDK.api.get(`${ this.roomTypeToApiType(type) }.files`, {
 			roomId,
 			offset,
@@ -687,6 +749,7 @@ const RocketChat = {
 		});
 	},
 	getMessages(roomId, type, query, offset) {
+		// RC 0.59.0
 		return SDK.api.get(`${ this.roomTypeToApiType(type) }.messages`, {
 			roomId,
 			query,
@@ -695,6 +758,7 @@ const RocketChat = {
 		});
 	},
 	searchMessages(roomId, searchText) {
+		// RC 0.60.0
 		return SDK.api.get('chat.search', {
 			roomId,
 			searchText
diff --git a/app/sagas/deepLinking.js b/app/sagas/deepLinking.js
index caa74f7d0ca4c4a71701440b3e31af5bf39d80f3..f46445b0de4ae7d143c4d7e3e692beb7ca0805dc 100644
--- a/app/sagas/deepLinking.js
+++ b/app/sagas/deepLinking.js
@@ -1,7 +1,7 @@
 import { AsyncStorage } from 'react-native';
 import { delay } from 'redux-saga';
 import {
-	takeLatest, take, select, put, all
+	takeLatest, take, select, put, all, race
 } from 'redux-saga/effects';
 import { Navigation } from 'react-native-navigation';
 
@@ -12,6 +12,10 @@ import database from '../lib/realm';
 import RocketChat from '../lib/rocketchat';
 import EventEmitter from '../utils/events';
 
+const roomTypes = {
+	channel: 'c', direct: 'd', group: 'p'
+};
+
 const navigate = function* navigate({ params, sameServer = true }) {
 	if (!sameServer) {
 		yield put(appStart('inside'));
@@ -37,11 +41,12 @@ const navigate = function* navigate({ params, sameServer = true }) {
 			} catch (error) {
 				console.log(error);
 			}
+			const [type, name] = params.path.split('/');
 			Navigation.push(stack, {
 				component: {
 					name: 'RoomView',
 					passProps: {
-						rid: params.rid
+						rid: params.rid, name, t: roomTypes[type]
 					}
 				}
 			});
@@ -78,15 +83,16 @@ const handleOpen = function* handleOpen({ params }) {
 	// if deep link is from same server
 	if (server === host) {
 		if (user) {
-			yield take(types.SERVER.SELECT_SUCCESS);
+			yield race({
+				typing: take(types.SERVER.SELECT_SUCCESS),
+				timeout: delay(3000)
+			});
 			yield navigate({ params });
 		}
 	} else {
 		// if deep link is from a different server
-		try {
-			// Verify if server is real
-			yield RocketChat.testServer(host);
-		} catch (error) {
+		const result = yield RocketChat.testServer(server);
+		if (!result.success) {
 			return;
 		}
 
diff --git a/app/sagas/messages.js b/app/sagas/messages.js
index 1cdafc66fd087e7532af44478691e3adf441dd12..e5d47afe5976b3eb7ef6135a82b5ddcde3a1d49e 100644
--- a/app/sagas/messages.js
+++ b/app/sagas/messages.js
@@ -74,13 +74,13 @@ const handleTogglePinRequest = function* handleTogglePinRequest({ message }) {
 	}
 };
 
-const goRoom = function* goRoom({ rid }) {
+const goRoom = function* goRoom({ rid, name }) {
 	yield Navigation.popToRoot('RoomsListView');
 	Navigation.push('RoomsListView', {
 		component: {
 			name: 'RoomView',
 			passProps: {
-				rid
+				rid, name, t: 'd'
 			}
 		}
 	});
@@ -91,10 +91,10 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
 		const { username } = message.u;
 		const subscriptions = database.objects('subscriptions').filtered('name = $0', username);
 		if (subscriptions.length) {
-			yield goRoom({ rid: subscriptions[0].rid });
+			yield goRoom({ rid: subscriptions[0].rid, name: username });
 		} else {
 			const room = yield RocketChat.createDirectMessage(username);
-			yield goRoom({ rid: room.rid });
+			yield goRoom({ rid: room.rid, name: username });
 		}
 		yield delay(500);
 		yield put(replyInit(message, false));
diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js
index defc7c563541b9cdac75310c83edfdf4dc38acb0..4ea9736a3e292c034215594ec6731d76b9da2e32 100644
--- a/app/sagas/rooms.js
+++ b/app/sagas/rooms.js
@@ -120,7 +120,6 @@ const watchuserTyping = function* watchuserTyping({ status }) {
 
 const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
 	try {
-		sub.stop();
 		const result = yield RocketChat.leaveRoom(rid, t);
 		if (result.success) {
 			yield Navigation.popToRoot('RoomsListView');
@@ -136,7 +135,6 @@ const handleLeaveRoom = function* handleLeaveRoom({ rid, t }) {
 
 const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
 	try {
-		sub.stop();
 		const result = yield RocketChat.eraseRoom(rid, t);
 		if (result.success) {
 			yield Navigation.popToRoot('RoomsListView');
@@ -148,7 +146,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
 
 const root = function* root() {
 	yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
-	yield takeLatest(types.ROOM.OPEN, watchRoomOpen);
+	yield takeEvery(types.ROOM.OPEN, watchRoomOpen);
 	yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
 	yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
 	yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js
index a2350ac66a8799c76860a79c81dac59443a656f5..189bb28d958def70f964a2d8a649c27499119fcd 100644
--- a/app/sagas/selectServer.js
+++ b/app/sagas/selectServer.js
@@ -1,5 +1,5 @@
 import { put, takeLatest } from 'redux-saga/effects';
-import { AsyncStorage } from 'react-native';
+import { AsyncStorage, Alert } from 'react-native';
 import { Navigation } from 'react-native-navigation';
 import { Provider } from 'react-redux';
 import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
@@ -13,6 +13,7 @@ import RocketChat from '../lib/rocketchat';
 import database from '../lib/realm';
 import log from '../utils/log';
 import store from '../lib/createStore';
+import I18n from '../i18n';
 
 let LoginSignupView = null;
 let LoginView = null;
@@ -49,7 +50,13 @@ const handleSelectServer = function* handleSelectServer({ server }) {
 
 const handleServerRequest = function* handleServerRequest({ server }) {
 	try {
-		yield RocketChat.testServer(server);
+		const result = yield RocketChat.testServer(server);
+		if (!result.success) {
+			Alert.alert(I18n.t('Oops'), I18n.t(result.message, result.messageOptions));
+			yield put(serverFailure());
+			return;
+		}
+
 		const loginServicesLength = yield RocketChat.getLoginServices(server);
 		if (loginServicesLength === 0) {
 			if (LoginView == null) {
diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js
index 193c1327f360742754160d2c6712b01698f2aa73..b9a40815800242979510df5622da66d8c0893c8a 100644
--- a/app/views/CreateChannelView.js
+++ b/app/views/CreateChannelView.js
@@ -6,6 +6,7 @@ import {
 } from 'react-native';
 import { Navigation } from 'react-native-navigation';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import Loading from '../containers/Loading';
 import LoggedView from './View';
@@ -128,6 +129,43 @@ export default class CreateChannelView extends LoggedView {
 		}, 600);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			channelName, type, readOnly, broadcast
+		} = this.state;
+		const {
+			error, failure, isFetching, result, users
+		} = this.props;
+		if (nextState.channelName !== channelName) {
+			return true;
+		}
+		if (nextState.type !== type) {
+			return true;
+		}
+		if (nextState.readOnly !== readOnly) {
+			return true;
+		}
+		if (nextState.broadcast !== broadcast) {
+			return true;
+		}
+		if (nextProps.failure !== failure) {
+			return true;
+		}
+		if (nextProps.isFetching !== isFetching) {
+			return true;
+		}
+		if (!equal(nextProps.error, error)) {
+			return true;
+		}
+		if (!equal(nextProps.result, result)) {
+			return true;
+		}
+		if (!equal(nextProps.users, users)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentDidUpdate(prevProps) {
 		const {
 			isFetching, failure, error, result, componentId
@@ -139,13 +177,14 @@ export default class CreateChannelView extends LoggedView {
 					const msg = error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
 					showErrorAlert(msg);
 				} else {
-					const { rid } = result;
+					const { type } = this.state;
+					const { rid, name } = result;
 					await Navigation.dismissModal(componentId);
 					Navigation.push('RoomsListView', {
 						component: {
 							name: 'RoomView',
 							passProps: {
-								rid
+								rid, name, t: type ? 'p' : 'c'
 							}
 						}
 					});
diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js
index c099539ac4e1a1ff361181f357dd25e3232b004b..9a9e4dd716b79fa7ff7a4dea0203cffa86f0100a 100644
--- a/app/views/ForgotPasswordView.js
+++ b/app/views/ForgotPasswordView.js
@@ -44,6 +44,20 @@ export default class ForgotPasswordView extends LoggedView {
 		}, 600);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { email, invalidEmail, isFetching } = this.state;
+		if (nextState.email !== email) {
+			return true;
+		}
+		if (nextState.invalidEmail !== invalidEmail) {
+			return true;
+		}
+		if (nextState.isFetching !== isFetching) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		if (this.timeout) {
 			clearTimeout(this.timeout);
diff --git a/app/views/LoginSignupView.js b/app/views/LoginSignupView.js
index 084f9cc3c3f992d722bbd6f6cc471601120d2cb9..7c54ad21e844470b9af5c64e66ed5ac18aebfc33 100644
--- a/app/views/LoginSignupView.js
+++ b/app/views/LoginSignupView.js
@@ -8,6 +8,7 @@ import { Navigation } from 'react-native-navigation';
 import { Base64 } from 'js-base64';
 import SafeAreaView from 'react-native-safe-area-view';
 import { gestureHandlerRootHOC, RectButton, BorderlessButton } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import LoggedView from './View';
 import sharedStyles from './Styles';
@@ -94,7 +95,6 @@ const SERVICES_COLLAPSED_HEIGHT = 174;
 
 @connect(state => ({
 	server: state.server.server,
-	isFetching: state.login.isFetching,
 	Site_Name: state.settings.Site_Name,
 	services: state.login.services
 }))
@@ -116,7 +116,6 @@ export default class LoginSignupView extends LoggedView {
 
 	static propTypes = {
 		componentId: PropTypes.string,
-		isFetching: PropTypes.bool,
 		server: PropTypes.string,
 		services: PropTypes.object,
 		Site_Name: PropTypes.string
@@ -133,6 +132,27 @@ export default class LoginSignupView extends LoggedView {
 		this.setTitle(componentId, Site_Name);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { collapsed, servicesHeight } = this.state;
+		const { server, Site_Name, services } = this.props;
+		if (nextState.collapsed !== collapsed) {
+			return true;
+		}
+		if (nextState.servicesHeight !== servicesHeight) {
+			return true;
+		}
+		if (nextProps.server !== server) {
+			return true;
+		}
+		if (nextProps.Site_Name !== Site_Name) {
+			return true;
+		}
+		if (!equal(nextProps.services, services)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentDidUpdate(prevProps) {
 		const { componentId, Site_Name } = this.props;
 		if (Site_Name && prevProps.Site_Name !== Site_Name) {
diff --git a/app/views/LoginView.js b/app/views/LoginView.js
index 1f6947f05a6f3b9ed7d331efc2acdd3445198347..c4b6a7296b48b57720b216e7dc3f89c7704b5095 100644
--- a/app/views/LoginView.js
+++ b/app/views/LoginView.js
@@ -126,6 +126,46 @@ export default class LoginView extends LoggedView {
 		}
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			user, password, code, showTOTP
+		} = this.state;
+		const {
+			isFetching, failure, error, Site_Name, Accounts_EmailOrUsernamePlaceholder, Accounts_PasswordPlaceholder
+		} = this.props;
+		if (nextState.user !== user) {
+			return true;
+		}
+		if (nextState.password !== password) {
+			return true;
+		}
+		if (nextState.code !== code) {
+			return true;
+		}
+		if (nextState.showTOTP !== showTOTP) {
+			return true;
+		}
+		if (nextProps.isFetching !== isFetching) {
+			return true;
+		}
+		if (nextProps.failure !== failure) {
+			return true;
+		}
+		if (nextProps.Site_Name !== Site_Name) {
+			return true;
+		}
+		if (nextProps.Accounts_EmailOrUsernamePlaceholder !== Accounts_EmailOrUsernamePlaceholder) {
+			return true;
+		}
+		if (nextProps.Accounts_PasswordPlaceholder !== Accounts_PasswordPlaceholder) {
+			return true;
+		}
+		if (!equal(nextProps.error, error)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		if (this.timeout) {
 			clearTimeout(this.timeout);
diff --git a/app/views/MentionedMessagesView/index.js b/app/views/MentionedMessagesView/index.js
index 524b69b4e5f1df1f51c25eca13308c90ee12881a..e831c408bf0dfcd1590f027ab018ff89e12701bc 100644
--- a/app/views/MentionedMessagesView/index.js
+++ b/app/views/MentionedMessagesView/index.js
@@ -12,11 +12,11 @@ import RCActivityIndicator from '../../containers/ActivityIndicator';
 import I18n from '../../i18n';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
 import RocketChat from '../../lib/rocketchat';
-import database from '../../lib/realm';
 
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
 	customEmojis: state.customEmojis,
+	room: state.room,
 	user: {
 		id: state.login.user && state.login.user.id,
 		username: state.login.user && state.login.user.username,
@@ -39,18 +39,16 @@ export default class MentionedMessagesView extends LoggedView {
 	}
 
 	static propTypes = {
-		rid: PropTypes.string,
 		user: PropTypes.object,
 		baseUrl: PropTypes.string,
-		customEmojis: PropTypes.object
+		customEmojis: PropTypes.object,
+		room: PropTypes.object
 	}
 
 	constructor(props) {
 		super('StarredMessagesView', props);
-		this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
 		this.state = {
 			loading: false,
-			room: this.rooms[0],
 			messages: []
 		};
 	}
@@ -60,12 +58,19 @@ export default class MentionedMessagesView extends LoggedView {
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
-		return !equal(this.state, nextState);
+		const { loading, messages } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (!equal(nextState.messages, messages)) {
+			return true;
+		}
+		return false;
 	}
 
 	load = async() => {
 		const {
-			messages, total, loading, room
+			messages, total, loading
 		} = this.state;
 		const { user } = this.props;
 		if (messages.length === total || loading) {
@@ -75,6 +80,7 @@ export default class MentionedMessagesView extends LoggedView {
 		this.setState({ loading: true });
 
 		try {
+			const { room } = this.props;
 			const result = await RocketChat.getMessages(
 				room.rid,
 				room.t,
diff --git a/app/views/NewMessageView.js b/app/views/NewMessageView.js
index 288a1d0d2dd117d1f9d68648a732382969f0b609..31c6ef32741772825a6173237f1578e6d956ba8a 100644
--- a/app/views/NewMessageView.js
+++ b/app/views/NewMessageView.js
@@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux';
 import { Navigation } from 'react-native-navigation';
 import SafeAreaView from 'react-native-safe-area-view';
 import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import database from '../lib/realm';
 import RocketChat from '../lib/rocketchat';
@@ -85,6 +86,14 @@ export default class NewMessageView extends LoggedView {
 		Navigation.events().bindComponent(this);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { search } = this.state;
+		if (!equal(nextState.search, search)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		this.updateState.stop();
 		this.data.removeAllListeners();
@@ -94,12 +103,10 @@ export default class NewMessageView extends LoggedView {
 		this.search(text);
 	}
 
-	onPressItem = (item) => {
+	onPressItem = async(item) => {
 		const { onPressItem } = this.props;
-		this.dismiss();
-		setTimeout(() => {
-			onPressItem(item);
-		}, 600);
+		await this.dismiss();
+		onPressItem(item);
 	}
 
 	navigationButtonPressed = ({ buttonId }) => {
@@ -110,7 +117,7 @@ export default class NewMessageView extends LoggedView {
 
 	dismiss = () => {
 		const { componentId } = this.props;
-		Navigation.dismissModal(componentId);
+		return Navigation.dismissModal(componentId);
 	}
 
 	// eslint-disable-next-line react/sort-comp
diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js
index f8c0c304ca07b394256f2925211e7dba39dad658..8fc62cd8b4b4806f8d3e215e5fdf0c04d4ec0f22 100644
--- a/app/views/NewServerView.js
+++ b/app/views/NewServerView.js
@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import {
-	Text, ScrollView, Keyboard, Image, Alert, StyleSheet, TouchableOpacity
+	Text, ScrollView, Keyboard, Image, StyleSheet, TouchableOpacity
 } from 'react-native';
 import { connect } from 'react-redux';
 import Icon from 'react-native-vector-icons/Ionicons';
@@ -58,8 +58,7 @@ const styles = StyleSheet.create({
 const defaultServer = 'https://open.rocket.chat';
 
 @connect(state => ({
-	connecting: state.server.connecting,
-	failure: state.server.failure
+	connecting: state.server.connecting
 }), dispatch => ({
 	connectServer: server => dispatch(serverRequest(server))
 }))
@@ -79,7 +78,6 @@ export default class NewServerView extends LoggedView {
 		componentId: PropTypes.string,
 		server: PropTypes.string,
 		connecting: PropTypes.bool.isRequired,
-		failure: PropTypes.bool.isRequired,
 		connectServer: PropTypes.func.isRequired
 	}
 
@@ -103,11 +101,16 @@ export default class NewServerView extends LoggedView {
 		}
 	}
 
-	componentWillReceiveProps(nextProps) {
-		const { failure } = this.props;
-		if (nextProps.failure && nextProps.failure !== failure) {
-			Alert.alert(I18n.t('Oops'), I18n.t('The_URL_is_invalid'));
+	shouldComponentUpdate(nextProps, nextState) {
+		const { text } = this.state;
+		const { connecting } = this.props;
+		if (nextState.text !== text) {
+			return true;
+		}
+		if (nextProps.connecting !== connecting) {
+			return true;
 		}
+		return false;
 	}
 
 	componentWillUnmount() {
diff --git a/app/views/OnboardingView/index.js b/app/views/OnboardingView/index.js
index 63eab53d46971140ca05ba810bd766a880f4a413..a9746b54c824e71ef7f989d4246a9171a84687c1 100644
--- a/app/views/OnboardingView/index.js
+++ b/app/views/OnboardingView/index.js
@@ -68,6 +68,10 @@ export default class OnboardingView extends LoggedView {
 		EventEmitter.addEventListener('NewServer', this.handleNewServerEvent);
 	}
 
+	shouldComponentUpdate() {
+		return false;
+	}
+
 	componentWillUnmount() {
 		const {
 			selectServer, previousServer, currentServer, adding, finishAdd
diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js
index 544ce1cafc8ab48108a2824b186393b9c0acbe28..91063abe7187568fafc8ddb47ebe62be2cef33d1 100644
--- a/app/views/PinnedMessagesView/index.js
+++ b/app/views/PinnedMessagesView/index.js
@@ -13,7 +13,6 @@ import RCActivityIndicator from '../../containers/ActivityIndicator';
 import I18n from '../../i18n';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
 import RocketChat from '../../lib/rocketchat';
-import database from '../../lib/realm';
 
 const PIN_INDEX = 0;
 const CANCEL_INDEX = 1;
@@ -22,6 +21,7 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')];
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
 	customEmojis: state.customEmojis,
+	room: state.room,
 	user: {
 		id: state.login.user && state.login.user.id,
 		username: state.login.user && state.login.user.username,
@@ -44,18 +44,16 @@ export default class PinnedMessagesView extends LoggedView {
 	}
 
 	static propTypes = {
-		rid: PropTypes.string,
 		user: PropTypes.object,
 		baseUrl: PropTypes.string,
-		customEmojis: PropTypes.object
+		customEmojis: PropTypes.object,
+		room: PropTypes.object
 	}
 
 	constructor(props) {
 		super('PinnedMessagesView', props);
-		this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
 		this.state = {
 			loading: false,
-			room: this.rooms[0],
 			messages: []
 		};
 	}
@@ -65,7 +63,14 @@ export default class PinnedMessagesView extends LoggedView {
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
-		return !equal(this.state, nextState);
+		const { loading, messages } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (!equal(nextState.messages, messages)) {
+			return true;
+		}
+		return false;
 	}
 
 	onLongPress = (message) => {
@@ -101,7 +106,7 @@ export default class PinnedMessagesView extends LoggedView {
 
 	load = async() => {
 		const {
-			messages, total, loading, room
+			messages, total, loading
 		} = this.state;
 		if (messages.length === total || loading) {
 			return;
@@ -110,6 +115,7 @@ export default class PinnedMessagesView extends LoggedView {
 		this.setState({ loading: true });
 
 		try {
+			const { room } = this.props;
 			const result = await RocketChat.getMessages(room.rid, room.t, { pinned: true }, messages.length);
 			if (result.success) {
 				this.setState(prevState => ({
diff --git a/app/views/ProfileView/index.js b/app/views/ProfileView/index.js
index 527727447a52bdc5a572994c0842ebf1b27a1847..181757ede1fa0ec51d903a4de71ffb2523571c30 100644
--- a/app/views/ProfileView/index.js
+++ b/app/views/ProfileView/index.js
@@ -11,6 +11,7 @@ import ImagePicker from 'react-native-image-crop-picker';
 import RNPickerSelect from 'react-native-picker-select';
 import { Navigation } from 'react-native-navigation';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import LoggedView from '../View';
 import KeyboardView from '../../presentation/KeyboardView';
@@ -118,6 +119,16 @@ export default class ProfileView extends LoggedView {
 		}
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		if (!equal(nextState, this.state)) {
+			return true;
+		}
+		if (!equal(nextProps, this.props)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
 	}
diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js
index debd84e93a734b470df7dab379f9841c9f526349..7c71df3e3f00cc7593c578826b5c663791ef0771 100644
--- a/app/views/RegisterView.js
+++ b/app/views/RegisterView.js
@@ -25,6 +25,8 @@ let TermsServiceView = null;
 let PrivacyPolicyView = null;
 let LegalView = null;
 
+const shouldUpdateState = ['name', 'email', 'password', 'username', 'saving'];
+
 @connect(null, dispatch => ({
 	loginRequest: params => dispatch(loginRequestAction(params))
 }))
@@ -67,6 +69,11 @@ export default class RegisterView extends LoggedView {
 		}, 600);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		// eslint-disable-next-line react/destructuring-assignment
+		return shouldUpdateState.some(key => nextState[key] !== this.state[key]);
+	}
+
 	componentDidUpdate(prevProps) {
 		const { componentId, Site_Name } = this.props;
 		if (Site_Name && prevProps.Site_Name !== Site_Name) {
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 4a3a4c100741e2a58266c82833224c15be26ddd9..2773197235bf781ce75f0d33fcad797f49b7ccd9 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -9,6 +9,7 @@ import { connect, Provider } from 'react-redux';
 import { Navigation } from 'react-native-navigation';
 import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import { leaveRoom as leaveRoomAction } from '../../actions/room';
 import LoggedView from '../View';
@@ -33,7 +34,8 @@ const modules = {};
 @connect(state => ({
 	userId: state.login.user && state.login.user.id,
 	username: state.login.user && state.login.user.username,
-	baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
+	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
+	room: state.room
 }), dispatch => ({
 	leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t))
 }))
@@ -58,15 +60,16 @@ export default class RoomActionsView extends LoggedView {
 		componentId: PropTypes.string,
 		userId: PropTypes.string,
 		username: PropTypes.string,
+		room: PropTypes.object,
 		leaveRoom: PropTypes.func
 	}
 
 	constructor(props) {
 		super('RoomActionsView', props);
-		const { rid } = props;
+		const { rid, room } = props;
 		this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
 		this.state = {
-			room: this.rooms[0] || {},
+			room,
 			membersCount: 0,
 			member: {},
 			joined: false,
@@ -86,14 +89,34 @@ export default class RoomActionsView extends LoggedView {
 			} catch (error) {
 				console.log('RoomActionsView -> getRoomCounters -> error', error);
 			}
-		}
-
-		if (room.t === 'd') {
+		} else if (room.t === 'd') {
 			this.updateRoomMember();
 		}
 		this.rooms.addListener(this.updateRoom);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			room, membersCount, member, joined, canViewMembers
+		} = this.state;
+		if (nextState.membersCount !== membersCount) {
+			return true;
+		}
+		if (nextState.joined !== joined) {
+			return true;
+		}
+		if (nextState.canViewMembers !== canViewMembers) {
+			return true;
+		}
+		if (!equal(nextState.room, room)) {
+			return true;
+		}
+		if (!equal(nextState.member, member)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		this.rooms.removeAllListeners();
 	}
@@ -109,7 +132,8 @@ export default class RoomActionsView extends LoggedView {
 			Navigation.push(componentId, {
 				component: {
 					name: item.route,
-					passProps: item.params
+					passProps: item.params,
+					options: item.navigationOptions
 				}
 			});
 		}
@@ -156,11 +180,20 @@ export default class RoomActionsView extends LoggedView {
 	}
 
 	get sections() {
-		const { room, membersCount, canViewMembers } = this.state;
+		const {
+			room, membersCount, canViewMembers, joined
+		} = this.state;
 		const {
 			rid, t, blocker, notifications
 		} = room;
 
+		const notificationsAction = {
+			icon: `ios-notifications${ notifications ? '' : '-off' }`,
+			name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`),
+			event: () => this.toggleNotifications(),
+			testID: 'room-actions-notifications'
+		};
+
 		const sections = [{
 			data: [{
 				icon: 'ios-star',
@@ -193,7 +226,6 @@ export default class RoomActionsView extends LoggedView {
 					icon: 'ios-attach',
 					name: I18n.t('Files'),
 					route: 'RoomFilesView',
-					params: { rid },
 					testID: 'room-actions-files',
 					require: () => require('../RoomFilesView').default
 				},
@@ -201,7 +233,6 @@ export default class RoomActionsView extends LoggedView {
 					icon: 'ios-at',
 					name: I18n.t('Mentions'),
 					route: 'MentionedMessagesView',
-					params: { rid },
 					testID: 'room-actions-mentioned',
 					require: () => require('../MentionedMessagesView').default
 				},
@@ -209,7 +240,6 @@ export default class RoomActionsView extends LoggedView {
 					icon: 'ios-star',
 					name: I18n.t('Starred'),
 					route: 'StarredMessagesView',
-					params: { rid },
 					testID: 'room-actions-starred',
 					require: () => require('../StarredMessagesView').default
 				},
@@ -231,7 +261,6 @@ export default class RoomActionsView extends LoggedView {
 					icon: 'ios-pin',
 					name: I18n.t('Pinned'),
 					route: 'PinnedMessagesView',
-					params: { rid },
 					testID: 'room-actions-pinned',
 					require: () => require('../PinnedMessagesView').default
 				},
@@ -242,12 +271,6 @@ export default class RoomActionsView extends LoggedView {
 					params: { rid },
 					testID: 'room-actions-snippeted',
 					require: () => require('../SnippetedMessagesView').default
-				},
-				{
-					icon: `ios-notifications${ notifications ? '' : '-off' }`,
-					name: I18n.t(`${ notifications ? 'Enable' : 'Disable' }_notifications`),
-					event: () => this.toggleNotifications(),
-					testID: 'room-actions-notifications'
 				}
 			],
 			renderItem: this.renderItem
@@ -266,6 +289,7 @@ export default class RoomActionsView extends LoggedView {
 				],
 				renderItem: this.renderItem
 			});
+			sections[2].data.push(notificationsAction);
 		} else if (t === 'c' || t === 'p') {
 			const actions = [];
 
@@ -273,7 +297,7 @@ export default class RoomActionsView extends LoggedView {
 				actions.push({
 					icon: 'ios-people',
 					name: I18n.t('Members'),
-					description: `${ membersCount } ${ I18n.t('members') }`,
+					description: membersCount > 0 ? `${ membersCount } ${ I18n.t('members') }` : null,
 					route: 'RoomMembersView',
 					params: { rid },
 					testID: 'room-actions-members',
@@ -290,29 +314,42 @@ export default class RoomActionsView extends LoggedView {
 						nextAction: 'ADD_USER',
 						rid
 					},
+					navigationOptions: {
+						topBar: {
+							title: {
+								text: I18n.t('Add_user')
+							}
+						}
+					},
 					testID: 'room-actions-add-user',
 					require: () => require('../SelectedUsersView').default
 				});
 			}
 			sections[2].data = [...actions, ...sections[2].data];
-			sections.push({
-				data: [
-					{
-						icon: 'block',
-						name: I18n.t('Leave_channel'),
-						type: 'danger',
-						event: () => this.leaveChannel(),
-						testID: 'room-actions-leave-channel'
-					}
-				],
-				renderItem: this.renderItem
-			});
+
+			if (joined) {
+				sections[2].data.push(notificationsAction);
+				sections.push({
+					data: [
+						{
+							icon: 'block',
+							name: I18n.t('Leave_channel'),
+							type: 'danger',
+							event: () => this.leaveChannel(),
+							testID: 'room-actions-leave-channel'
+						}
+					],
+					renderItem: this.renderItem
+				});
+			}
 		}
 		return sections;
 	}
 
 	updateRoom = () => {
-		this.setState({ room: this.rooms[0] || {} });
+		if (this.rooms.length > 0) {
+			this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
+		}
 	}
 
 	updateRoomMember = async() => {
@@ -322,10 +359,10 @@ export default class RoomActionsView extends LoggedView {
 
 		try {
 			const member = await RocketChat.getRoomMember(rid, userId);
-			this.setState({ member });
+			this.setState({ member: member || {} });
 		} catch (e) {
 			log('RoomActions updateRoomMember', e);
-			return {};
+			this.setState({ member: {} });
 		}
 	}
 
diff --git a/app/views/RoomFilesView/index.js b/app/views/RoomFilesView/index.js
index 354c772cad246fa0d9d6e36b6a672cd369da4342..f8f981c0dcd48728f1fc5cde41819ac2a463ad42 100644
--- a/app/views/RoomFilesView/index.js
+++ b/app/views/RoomFilesView/index.js
@@ -11,12 +11,12 @@ import Message from '../../containers/message/Message';
 import RCActivityIndicator from '../../containers/ActivityIndicator';
 import I18n from '../../i18n';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
-import database from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
 	customEmojis: state.customEmojis,
+	room: state.room,
 	user: {
 		id: state.login.user && state.login.user.id,
 		username: state.login.user && state.login.user.username,
@@ -39,18 +39,16 @@ export default class RoomFilesView extends LoggedView {
 	}
 
 	static propTypes = {
-		rid: PropTypes.string,
 		user: PropTypes.object,
 		baseUrl: PropTypes.string,
-		customEmojis: PropTypes.object
+		customEmojis: PropTypes.object,
+		room: PropTypes.object
 	}
 
 	constructor(props) {
 		super('RoomFilesView', props);
-		this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
 		this.state = {
 			loading: false,
-			room: this.rooms[0],
 			messages: []
 		};
 	}
@@ -60,12 +58,19 @@ export default class RoomFilesView extends LoggedView {
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
-		return !equal(this.state, nextState);
+		const { loading, messages } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (!equal(nextState.messages, messages)) {
+			return true;
+		}
+		return false;
 	}
 
 	load = async() => {
 		const {
-			messages, total, loading, room
+			messages, total, loading
 		} = this.state;
 		if (messages.length === total || loading) {
 			return;
@@ -74,6 +79,7 @@ export default class RoomFilesView extends LoggedView {
 		this.setState({ loading: true });
 
 		try {
+			const { room } = this.props;
 			const result = await RocketChat.getFiles(room.rid, room.t, messages.length);
 			if (result.success) {
 				this.setState(prevState => ({
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
index afc4a8b8a779ac5b3e74151e14db44c6e013803e..f0b6701a2d71f795108436c0422254f53ed109a6 100644
--- a/app/views/RoomInfoEditView/index.js
+++ b/app/views/RoomInfoEditView/index.js
@@ -5,6 +5,7 @@ import {
 } from 'react-native';
 import { connect } from 'react-redux';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import { eraseRoom as eraseRoomAction } from '../../actions/room';
 import LoggedView from '../View';
@@ -67,7 +68,7 @@ export default class RoomInfoEditView extends LoggedView {
 		this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
 		this.permissions = {};
 		this.state = {
-			room: this.rooms[0] || {},
+			room: JSON.parse(JSON.stringify(this.rooms[0] || {})),
 			name: '',
 			description: '',
 			topic: '',
@@ -90,12 +91,26 @@ export default class RoomInfoEditView extends LoggedView {
 		this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, room.rid);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { room } = this.state;
+		if (!equal(nextState, this.state)) {
+			return true;
+		}
+		if (!equal(nextState.room, room)) {
+			return true;
+		}
+		if (!equal(nextProps, this.props)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		this.rooms.removeAllListeners();
 	}
 
 	updateRoom = () => {
-		this.setState({ room: this.rooms[0] || {} });
+		this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0] || {})) });
 	}
 
 	init = () => {
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index 00d41e966e928e06fbd838606c2121c15d9e7a20..1e9b3d8b428584eb31ad7f50b32318829e9fc65f 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -6,6 +6,7 @@ import moment from 'moment';
 import { Navigation } from 'react-native-navigation';
 import SafeAreaView from 'react-native-safe-area-view';
 import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import LoggedView from '../View';
 import Status from '../../containers/status';
@@ -40,9 +41,10 @@ let RoomInfoEditView = null;
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
 	userId: state.login.user && state.login.user.id,
-	activeUsers: state.activeUsers,
+	activeUsers: state.activeUsers, // TODO: remove it
 	Message_TimeFormat: state.settings.Message_TimeFormat,
-	allRoles: state.roles
+	allRoles: state.roles,
+	room: state.room
 }))
 /** @extends React.Component */
 export default class RoomInfoView extends LoggedView {
@@ -66,29 +68,34 @@ export default class RoomInfoView extends LoggedView {
 		baseUrl: PropTypes.string,
 		activeUsers: PropTypes.object,
 		Message_TimeFormat: PropTypes.string,
-		allRoles: PropTypes.object
+		allRoles: PropTypes.object,
+		room: PropTypes.object
 	}
 
 	constructor(props) {
 		super('RoomInfoView', props);
-		const { rid } = props;
+		const { rid, room } = props;
 		this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
 		this.sub = {
 			unsubscribe: () => {}
 		};
 		this.state = {
-			room: {},
+			room,
 			roomUser: {},
 			roles: []
 		};
 		Navigation.events().bindComponent(this);
 	}
 
-	componentDidMount() {
-		this.updateRoom();
+	async componentDidMount() {
 		this.rooms.addListener(this.updateRoom);
 
-		const [room] = this.rooms;
+		let room = {};
+		if (this.rooms.length > 0) {
+			room = this.rooms[0]; // eslint-disable-line prefer-destructuring
+		} else {
+			room = this.state.room; // eslint-disable-line
+		}
 		const { componentId } = this.props;
 		const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
 		if (permissions[PERMISSION_EDIT_ROOM]) {
@@ -102,6 +109,57 @@ export default class RoomInfoView extends LoggedView {
 				}
 			});
 		}
+
+		// get user of room
+		if (room) {
+			if (room.t === 'd') {
+				try {
+					const { userId, activeUsers } = this.props;
+					const roomUser = await RocketChat.getRoomMember(room.rid, userId);
+					this.setState({ roomUser: roomUser || {} });
+					const username = room.name;
+
+					const activeUser = activeUsers[roomUser._id];
+					if (!activeUser || !activeUser.utcOffset) {
+						// get full user data looking for utcOffset
+						// will be catched by .on('users) and saved on activeUsers reducer
+						this.getFullUserData(username);
+					}
+
+					// get all users roles
+					// needs to be changed by a better method
+					const allUsersRoles = await RocketChat.getUserRoles();
+					const userRoles = allUsersRoles.find(user => user.username === username);
+					if (userRoles) {
+						this.setState({ roles: userRoles.roles || [] });
+					}
+				} catch (e) {
+					log('RoomInfoView.componentDidMount', e);
+				}
+			}
+		}
+	}
+
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			room, roomUser, roles
+		} = this.state;
+		const { activeUsers } = this.props;
+		if (!equal(nextState.room, room)) {
+			return true;
+		}
+		if (!equal(nextState.roomUser, roomUser)) {
+			return true;
+		}
+		if (!equal(nextState.roles, roles)) {
+			return true;
+		}
+		if (roomUser._id) {
+			if (nextProps.activeUsers[roomUser._id] !== activeUsers[roomUser._id]) {
+				return true;
+			}
+		}
+		return false;
 	}
 
 	componentWillUnmount() {
@@ -143,38 +201,9 @@ export default class RoomInfoView extends LoggedView {
 		return t === 'd';
 	}
 
-	updateRoom = async() => {
-		const { userId, activeUsers } = this.props;
-
-		const [room] = this.rooms;
-		this.setState({ room });
-
-		// get user of room
-		if (room) {
-			if (room.t === 'd') {
-				try {
-					const roomUser = await RocketChat.getRoomMember(room.rid, userId);
-					this.setState({ roomUser: roomUser || {} });
-					const username = room.name;
-
-					const activeUser = activeUsers[roomUser._id];
-					if (!activeUser || !activeUser.utcOffset) {
-						// get full user data looking for utcOffset
-						// will be catched by .on('users) and saved on activeUsers reducer
-						this.getFullUserData(username);
-					}
-
-					// get all users roles
-					// needs to be changed by a better method
-					const allUsersRoles = await RocketChat.getUserRoles();
-					const userRoles = allUsersRoles.find(user => user.username === username);
-					if (userRoles) {
-						this.setState({ roles: userRoles.roles || [] });
-					}
-				} catch (e) {
-					log('RoomInfoView.componentDidMount', e);
-				}
-			}
+	updateRoom = () => {
+		if (this.rooms.length > 0) {
+			this.setState({ room: JSON.parse(JSON.stringify(this.rooms[0])) });
 		}
 	}
 
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index ad7b0e87cc362cde0d93229e7e3ec64fa1206d56..7a82cfbad7db2c5ad6209f99ef028ef4df31c3b6 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -7,6 +7,7 @@ import ActionSheet from 'react-native-actionsheet';
 import { connect } from 'react-redux';
 import { Navigation } from 'react-native-navigation';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import LoggedView from '../View';
 import styles from './styles';
@@ -21,7 +22,8 @@ import SearchBox from '../../containers/SearchBox';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
 
 @connect(state => ({
-	baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
+	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
+	room: state.room
 }))
 /** @extends React.Component */
 export default class RoomMembersView extends LoggedView {
@@ -48,7 +50,8 @@ export default class RoomMembersView extends LoggedView {
 		componentId: PropTypes.string,
 		rid: PropTypes.string,
 		members: PropTypes.array,
-		baseUrl: PropTypes.string
+		baseUrl: PropTypes.string,
+		room: PropTypes.object
 	}
 
 	constructor(props) {
@@ -57,7 +60,7 @@ export default class RoomMembersView extends LoggedView {
 		this.CANCEL_INDEX = 0;
 		this.MUTE_INDEX = 1;
 		this.actionSheetOptions = [''];
-		const { rid, members } = props;
+		const { rid, members, room } = props;
 		this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
 		this.permissions = RocketChat.hasPermission(['mute-user'], rid);
 		this.state = {
@@ -67,7 +70,8 @@ export default class RoomMembersView extends LoggedView {
 			members,
 			membersFiltered: [],
 			userLongPressed: {},
-			room: this.rooms[0] || {}
+			room,
+			options: []
 		};
 		Navigation.events().bindComponent(this);
 	}
@@ -77,6 +81,34 @@ export default class RoomMembersView extends LoggedView {
 		this.rooms.addListener(this.updateRoom);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const {
+			allUsers, filtering, members, membersFiltered, userLongPressed, room, options
+		} = this.state;
+		if (nextState.allUsers !== allUsers) {
+			return true;
+		}
+		if (nextState.filtering !== filtering) {
+			return true;
+		}
+		if (!equal(nextState.members, members)) {
+			return true;
+		}
+		if (!equal(nextState.options, options)) {
+			return true;
+		}
+		if (!equal(nextState.membersFiltered, membersFiltered)) {
+			return true;
+		}
+		if (!equal(nextState.userLongPressed, userLongPressed)) {
+			return true;
+		}
+		if (!equal(nextState.room.muted, room.muted)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		this.rooms.removeAllListeners();
 	}
@@ -118,11 +150,11 @@ export default class RoomMembersView extends LoggedView {
 		try {
 			const subscriptions = database.objects('subscriptions').filtered('name = $0', item.username);
 			if (subscriptions.length) {
-				this.goRoom({ rid: subscriptions[0].rid });
+				this.goRoom({ rid: subscriptions[0].rid, name: item.username });
 			} else {
 				const result = await RocketChat.createDirectMessage(item.username);
 				if (result.success) {
-					this.goRoom({ rid: result.room._id });
+					this.goRoom({ rid: result.room._id, name: item.username });
 				}
 			}
 		} catch (e) {
@@ -134,21 +166,25 @@ export default class RoomMembersView extends LoggedView {
 		if (!this.permissions['mute-user']) {
 			return;
 		}
-		const { room } = this.state;
-		const { muted } = room;
-
-		this.actionSheetOptions = [I18n.t('Cancel')];
-		const userIsMuted = !!muted.find(m => m.value === user.username);
-		user.muted = userIsMuted;
-		if (userIsMuted) {
-			this.actionSheetOptions.push(I18n.t('Unmute'));
-		} else {
-			this.actionSheetOptions.push(I18n.t('Mute'));
-		}
-		this.setState({ userLongPressed: user });
-		Vibration.vibrate(50);
-		if (this.actionSheet && this.actionSheet.show) {
-			this.actionSheet.show();
+		try {
+			const { room } = this.state;
+			const { muted } = room;
+
+			const options = [I18n.t('Cancel')];
+			const userIsMuted = !!muted.find(m => m.value === user.username);
+			user.muted = userIsMuted;
+			if (userIsMuted) {
+				options.push(I18n.t('Unmute'));
+			} else {
+				options.push(I18n.t('Mute'));
+			}
+			this.setState({ userLongPressed: user, options });
+			Vibration.vibrate(50);
+			if (this.actionSheet && this.actionSheet.show) {
+				this.actionSheet.show();
+			}
+		} catch (error) {
+			console.log('onLongPressUser -> catch -> error', error);
 		}
 	}
 
@@ -159,19 +195,21 @@ export default class RoomMembersView extends LoggedView {
 		this.setState({ allUsers: status, members });
 	}
 
-	updateRoom = async() => {
-		const [room] = this.rooms;
-		await this.setState({ room });
+	updateRoom = () => {
+		if (this.rooms.length > 0) {
+			const [room] = this.rooms;
+			this.setState({ room });
+		}
 	}
 
-	goRoom = async({ rid }) => {
+	goRoom = async({ rid, name }) => {
 		const { componentId } = this.props;
 		await Navigation.popToRoot(componentId);
 		Navigation.push('RoomsListView', {
 			component: {
 				name: 'RoomView',
 				passProps: {
-					rid
+					rid, name, t: 'd'
 				}
 			}
 		});
@@ -219,7 +257,9 @@ export default class RoomMembersView extends LoggedView {
 	}
 
 	render() {
-		const { filtering, members, membersFiltered } = this.state;
+		const {
+			filtering, members, membersFiltered, options
+		} = this.state;
 		return (
 			<SafeAreaView style={styles.list} testID='room-members-view' forceInset={{ bottom: 'never' }}>
 				<FlatList
@@ -234,7 +274,7 @@ export default class RoomMembersView extends LoggedView {
 				<ActionSheet
 					ref={o => this.actionSheet = o}
 					title={I18n.t('Actions')}
-					options={this.actionSheetOptions}
+					options={options}
 					cancelButtonIndex={this.CANCEL_INDEX}
 					onPress={this.handleActionPress}
 				/>
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 813c145bce4fcb11978b4e3ca5fd9ba07c1ad8e2..5105d946108626e0db3808f5525dbf70a02d8ffe 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -1,4 +1,4 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import {
 	View, Text, StyleSheet, Image, Platform, LayoutAnimation
@@ -81,7 +81,7 @@ const styles = StyleSheet.create({
 		status
 	};
 })
-export default class RoomHeaderView extends PureComponent {
+export default class RoomHeaderView extends Component {
 	static propTypes = {
 		title: PropTypes.string,
 		type: PropTypes.string,
@@ -90,10 +90,37 @@ export default class RoomHeaderView extends PureComponent {
 		status: PropTypes.string
 	};
 
+	shouldComponentUpdate(nextProps) {
+		const {
+			type, title, status, usersTyping, window
+		} = this.props;
+		if (nextProps.type !== type) {
+			return true;
+		}
+		if (nextProps.title !== title) {
+			return true;
+		}
+		if (nextProps.status !== status) {
+			return true;
+		}
+		if (nextProps.window.width !== window.width) {
+			return true;
+		}
+		if (nextProps.window.height !== window.height) {
+			return true;
+		}
+		if (!equal(nextProps.usersTyping, usersTyping)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentDidUpdate(prevProps) {
-		const { usersTyping } = this.props;
-		if (!equal(prevProps.usersTyping, usersTyping)) {
-			LayoutAnimation.easeInEaseOut();
+		if (isIOS()) {
+			const { usersTyping } = this.props;
+			if (!equal(prevProps.usersTyping, usersTyping)) {
+				LayoutAnimation.easeInEaseOut();
+			}
 		}
 	}
 
diff --git a/app/views/RoomView/ListView.js b/app/views/RoomView/ListView.js
index 6f11d236cc1eff9e57ce2eda32c063d31505b1ab..28935f5ab72a363aa996a85964ec9a19b538c96c 100644
--- a/app/views/RoomView/ListView.js
+++ b/app/views/RoomView/ListView.js
@@ -1,6 +1,8 @@
 import { ListView as OldList } from 'realm/react-native';
 import React from 'react';
-import { ScrollView, ListView as OldList2, ImageBackground } from 'react-native';
+import {
+	ScrollView, ListView as OldList2, ImageBackground, ActivityIndicator
+} from 'react-native';
 import moment from 'moment';
 import { connect } from 'react-redux';
 import PropTypes from 'prop-types';
@@ -9,7 +11,9 @@ import Separator from './Separator';
 import styles from './styles';
 import database from '../../lib/realm';
 import scrollPersistTaps from '../../utils/scrollPersistTaps';
-import throttle from '../../utils/throttle';
+import debounce from '../../utils/debounce';
+import RocketChat from '../../lib/rocketchat';
+import log from '../../utils/log';
 
 const DEFAULT_SCROLL_CALLBACK_THROTTLE = 100;
 
@@ -24,34 +28,38 @@ export class DataSource extends OldList.DataSource {
 	}
 }
 
-const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id || r1._updatedAt.toISOString() !== r2._updatedAt.toISOString() });
+const ds = new DataSource({ rowHasChanged: (r1, r2) => r1._id !== r2._id });
 
 export class List extends React.Component {
 	static propTypes = {
 		onEndReached: PropTypes.func,
 		renderFooter: PropTypes.func,
 		renderRow: PropTypes.func,
-		room: PropTypes.string,
-		end: PropTypes.bool,
-		loadingMore: PropTypes.bool
+		room: PropTypes.object
 	};
 
 	constructor(props) {
 		super(props);
 		this.data = database
 			.objects('messages')
-			.filtered('rid = $0', props.room)
+			.filtered('rid = $0', props.room.rid)
 			.sorted('ts', true);
+		this.state = {
+			loading: true,
+			loadingMore: false,
+			end: false
+		};
 		this.dataSource = ds.cloneWithRows(this.data);
 	}
 
 	componentDidMount() {
+		this.updateState();
 		this.data.addListener(this.updateState);
 	}
 
-	shouldComponentUpdate(nextProps) {
-		const { end, loadingMore } = this.props;
-		return end !== nextProps.end || loadingMore !== nextProps.loadingMore;
+	shouldComponentUpdate(nextProps, nextState) {
+		const { loadingMore, loading, end } = this.state;
+		return end !== nextState.end || loadingMore !== nextState.loadingMore || loading !== nextState.loading;
 	}
 
 	componentWillUnmount() {
@@ -60,16 +68,39 @@ export class List extends React.Component {
 	}
 
 	// eslint-disable-next-line react/sort-comp
-	updateState = throttle(() => {
-		// this.setState({
+	updateState = debounce(() => {
+		this.setState({ loading: true });
 		this.dataSource = this.dataSource.cloneWithRows(this.data);
-		// LayoutAnimation.easeInEaseOut();
-		this.forceUpdate();
-		// });
-	}, 1000);
+		this.setState({ loading: false });
+	}, 300);
+
+	onEndReached = async() => {
+		const { loadingMore, end } = this.state;
+		if (loadingMore || end || this.data.length < 50) {
+			return;
+		}
+
+		this.setState({ loadingMore: true });
+		const { room } = this.props;
+		try {
+			const result = await RocketChat.loadMessagesForRoom({ rid: room.rid, t: room.t, latest: this.data[this.data.length - 1].ts });
+			this.setState({ end: result.length < 50, loadingMore: false });
+		} catch (e) {
+			this.setState({ loadingMore: false });
+			log('ListView.onEndReached', e);
+		}
+	}
+
+	renderFooter = () => {
+		const { loadingMore, loading } = this.state;
+		if (loadingMore || loading) {
+			return <ActivityIndicator style={styles.loadingMore} />;
+		}
+		return null;
+	}
 
 	render() {
-		const { renderFooter, onEndReached, renderRow } = this.props;
+		const { renderRow } = this.props;
 
 		return (
 			<ListView
@@ -78,8 +109,8 @@ export class List extends React.Component {
 				data={this.data}
 				keyExtractor={item => item._id}
 				onEndReachedThreshold={100}
-				renderFooter={renderFooter}
-				onEndReached={() => onEndReached(this.data[this.data.length - 1])}
+				renderFooter={this.renderFooter}
+				onEndReached={this.onEndReached}
 				dataSource={this.dataSource}
 				renderRow={(item, previousItem) => renderRow(item, previousItem)}
 				initialListSize={1}
diff --git a/app/views/RoomView/UploadProgress.js b/app/views/RoomView/UploadProgress.js
index 30620396319c75353b84a021f946cb391cc12ca0..becf386f159b0e52be3cf3de561ca71854cc3697 100644
--- a/app/views/RoomView/UploadProgress.js
+++ b/app/views/RoomView/UploadProgress.js
@@ -5,6 +5,7 @@ import {
 import PropTypes from 'prop-types';
 import Icon from 'react-native-vector-icons/MaterialIcons';
 import { responsive } from 'react-native-responsive-ui';
+import equal from 'deep-equal';
 
 import database from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
@@ -84,6 +85,18 @@ export default class UploadProgress extends Component {
 		});
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { uploads } = this.state;
+		const { window } = this.props;
+		if (nextProps.window.width !== window.width) {
+			return true;
+		}
+		if (!equal(nextState.uploads, uploads)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		this.uploads.removeAllListeners();
 	}
@@ -107,14 +120,15 @@ export default class UploadProgress extends Component {
 			database.write(() => {
 				item.error = false;
 			});
-			await RocketChat.sendFileMessage(rid, JSON.parse(JSON.stringify(item)));
+			await RocketChat.sendFileMessage(rid, item);
 		} catch (e) {
 			log('UploadProgess.tryAgain', e);
 		}
 	}
 
 	updateUploads = () => {
-		this.setState({ uploads: this.uploads });
+		const uploads = this.uploads.map(item => JSON.parse(JSON.stringify(item)));
+		this.setState({ uploads });
 	}
 
 	renderItemContent = (item) => {
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index fd0ecc4c99aa69349577caab5cdce0510a8fccf0..ae5ec244b85ba1c0ca1ad2a2a470698dbbd98bf7 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -84,6 +84,8 @@ export default class RoomView extends LoggedView {
 			token: PropTypes.string.isRequired
 		}),
 		rid: PropTypes.string,
+		name: PropTypes.string,
+		t: PropTypes.string,
 		showActions: PropTypes.bool,
 		showErrorActions: PropTypes.bool,
 		actionMessage: PropTypes.object,
@@ -100,24 +102,20 @@ export default class RoomView extends LoggedView {
 		this.state = {
 			loaded: false,
 			joined: this.rooms.length > 0,
-			room: {},
-			end: false,
-			loadingMore: false
+			room: {}
 		};
+		this.focused = true;
 		this.onReactionPress = this.onReactionPress.bind(this);
 		Navigation.events().bindComponent(this);
 	}
 
-	async componentDidMount() {
+	componentDidMount() {
 		if (this.rooms.length === 0 && this.rid) {
-			const result = await RocketChat.getRoomInfo(this.rid);
-			if (result.success) {
-				const { room } = result;
-				this.setState(
-					{ room: { rid: room._id, t: room.t, name: room.name } },
-					() => this.updateRoom()
-				);
-			}
+			const { rid, name, t } = this.props;
+			this.setState(
+				{ room: { rid, name, t } },
+				() => this.updateRoom()
+			);
 		}
 		this.rooms.addListener(this.updateRoom);
 		this.internalSetState({ loaded: true });
@@ -125,7 +123,7 @@ export default class RoomView extends LoggedView {
 
 	shouldComponentUpdate(nextProps, nextState) {
 		const {
-			room, loaded, joined, end, loadingMore
+			room, loaded, joined
 		} = this.state;
 		const { showActions, showErrorActions, appState } = this.props;
 
@@ -143,10 +141,6 @@ export default class RoomView extends LoggedView {
 			return true;
 		} else if (joined !== nextState.joined) {
 			return true;
-		} else if (end !== nextState.end) {
-			return true;
-		} else if (loadingMore !== nextState.loadingMore) {
-			return true;
 		} else if (showActions !== nextProps.showActions) {
 			return true;
 		} else if (showErrorActions !== nextProps.showErrorActions) {
@@ -187,32 +181,18 @@ export default class RoomView extends LoggedView {
 
 	componentWillUnmount() {
 		const { closeRoom } = this.props;
-		this.rooms.removeAllListeners();
-		if (this.onEndReached && this.onEndReached.stop) {
-			this.onEndReached.stop();
-		}
 		closeRoom();
+		this.rooms.removeAllListeners();
 	}
 
-	onEndReached = async(lastRowData) => {
-		if (!lastRowData) {
-			return;
-		}
-
-		const { loadingMore, end } = this.state;
-		if (loadingMore || end) {
-			return;
-		}
+	// eslint-disable-next-line
+	componentDidAppear() {
+		this.focused = true;
+	}
 
-		this.setState({ loadingMore: true });
-		const { room } = this.state;
-		try {
-			const result = await RocketChat.loadMessagesForRoom({ rid: this.rid, t: room.t, latest: lastRowData.ts });
-			this.internalSetState({ end: result.length < 50, loadingMore: false });
-		} catch (e) {
-			this.internalSetState({ loadingMore: false });
-			log('RoomView.onEndReached', e);
-		}
+	// eslint-disable-next-line
+	componentDidDisappear() {
+		this.focused = false;
 	}
 
 	onMessageLongPress = (message) => {
@@ -269,15 +249,19 @@ export default class RoomView extends LoggedView {
 		}
 	}
 
+	// eslint-disable-next-line react/sort-comp
 	updateRoom = () => {
 		const { openRoom, setLastOpen } = this.props;
 
+		if (!this.focused) {
+			return;
+		}
 		if (this.rooms.length > 0) {
 			const { room: prevRoom } = this.state;
 			const room = JSON.parse(JSON.stringify(this.rooms[0] || {}));
 			this.internalSetState({ room });
 
-			if (!prevRoom.rid) {
+			if (!prevRoom._id) {
 				openRoom({
 					...room
 				});
@@ -289,8 +273,10 @@ export default class RoomView extends LoggedView {
 			}
 		} else {
 			const { room } = this.state;
-			openRoom(room);
-			this.internalSetState({ joined: false });
+			if (room.rid) {
+				openRoom(room);
+				this.internalSetState({ joined: false });
+			}
 		}
 	}
 
@@ -370,7 +356,7 @@ export default class RoomView extends LoggedView {
 
 		if (!joined) {
 			return (
-				<View style={styles.joinRoomContainer} key='room-view-join'>
+				<View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'>
 					<Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text>
 					<RectButton
 						onPress={this.joinRoom}
@@ -378,7 +364,7 @@ export default class RoomView extends LoggedView {
 						activeOpacity={0.5}
 						underlayColor='#fff'
 					>
-						<Text style={styles.joinRoomText}>{I18n.t('Join')}</Text>
+						<Text style={styles.joinRoomText} testID='room-view-join-button'>{I18n.t('Join')}</Text>
 					</RectButton>
 				</View>
 			);
@@ -400,28 +386,16 @@ export default class RoomView extends LoggedView {
 		return <MessageBox key='room-view-messagebox' onSubmit={this.sendMessage} rid={this.rid} />;
 	};
 
-	renderHeader = () => {
-		const { loadingMore } = this.state;
-		if (loadingMore) {
-			return <ActivityIndicator style={styles.loadingMore} />;
-		}
-		return null;
-	}
-
 	renderList = () => {
-		const { loaded, end, loadingMore } = this.state;
-		if (!loaded) {
+		const { loaded, room } = this.state;
+		if (!loaded || !room.rid) {
 			return <ActivityIndicator style={styles.loading} />;
 		}
 		return (
 			[
 				<List
 					key='room-view-messages'
-					end={end}
-					loadingMore={loadingMore}
-					room={this.rid}
-					renderFooter={this.renderHeader}
-					onEndReached={this.onEndReached}
+					room={room}
 					renderRow={this.renderItem}
 				/>,
 				this.renderFooter()
diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js
index 24d45b3bb3ade30d6c1dafbe00f90a7937c6c422..88a452572c185fe916a1a4ddc942b4483331c20c 100644
--- a/app/views/RoomsListView/Header/index.js
+++ b/app/views/RoomsListView/Header/index.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 
@@ -18,7 +18,7 @@ import Header from './Header';
 	closeSort: () => dispatch(closeSortDropdown()),
 	setSearch: searchText => dispatch(setSearchAction(searchText))
 }))
-export default class RoomsListHeaderView extends Component {
+export default class RoomsListHeaderView extends PureComponent {
 	static propTypes = {
 		showServerDropdown: PropTypes.bool,
 		showSortDropdown: PropTypes.bool,
diff --git a/app/views/RoomsListView/ServerDropdown.js b/app/views/RoomsListView/ServerDropdown.js
index 81e2322afa2e0dfc3dbb314150f671422352f6de..c8cf3d6910440ba5f3e2a252e21600d244917b49 100644
--- a/app/views/RoomsListView/ServerDropdown.js
+++ b/app/views/RoomsListView/ServerDropdown.js
@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import { Navigation } from 'react-native-navigation';
 import * as SDK from '@rocket.chat/sdk';
+import equal from 'deep-equal';
 
 import { toggleServerDropdown as toggleServerDropdownAction } from '../../actions/rooms';
 import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
@@ -39,10 +40,12 @@ export default class ServerDropdown extends Component {
 
 	constructor(props) {
 		super(props);
+		this.servers = database.databases.serversDB.objects('servers');
 		this.state = {
-			servers: []
+			servers: this.servers
 		};
 		this.animatedValue = new Animated.Value(0);
+		this.servers.addListener(this.updateState);
 	}
 
 	componentDidMount() {
@@ -51,12 +54,25 @@ export default class ServerDropdown extends Component {
 			{
 				toValue: 1,
 				duration: ANIMATION_DURATION,
-				easing: Easing.ease,
+				easing: Easing.inOut(Easing.quad),
 				useNativeDriver: true
 			},
 		).start();
-		this.servers = database.databases.serversDB.objects('servers');
-		this.servers.addListener(this.updateState);
+	}
+
+	shouldComponentUpdate(nextProps, nextState) {
+		const { servers } = this.state;
+		const { closeServerDropdown, server } = this.props;
+		if (nextProps.closeServerDropdown !== closeServerDropdown) {
+			return true;
+		}
+		if (nextProps.server !== server) {
+			return true;
+		}
+		if (!equal(nextState.servers, servers)) {
+			return true;
+		}
+		return false;
 	}
 
 	componentDidUpdate(prevProps) {
@@ -78,7 +94,7 @@ export default class ServerDropdown extends Component {
 			{
 				toValue: 0,
 				duration: ANIMATION_DURATION,
-				easing: Easing.ease,
+				easing: Easing.inOut(Easing.quad),
 				useNativeDriver: true
 			}
 		).start(() => toggleServerDropdown());
diff --git a/app/views/RoomsListView/SortDropdown.js b/app/views/RoomsListView/SortDropdown.js
index f011d71686c89f0b2fdc656bdf4c5c890975ba1f..094e121394becbb55ae95157e15866a0897b797a 100644
--- a/app/views/RoomsListView/SortDropdown.js
+++ b/app/views/RoomsListView/SortDropdown.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 import {
 	View, Text, Animated, Easing, Image, TouchableWithoutFeedback
 } from 'react-native';
@@ -19,7 +19,7 @@ const ANIMATION_DURATION = 200;
 }), dispatch => ({
 	setSortPreference: preference => dispatch(setPreference(preference))
 }))
-export default class Sort extends Component {
+export default class Sort extends PureComponent {
 	static propTypes = {
 		closeSortDropdown: PropTypes.bool,
 		close: PropTypes.func,
@@ -41,7 +41,7 @@ export default class Sort extends Component {
 			{
 				toValue: 1,
 				duration: ANIMATION_DURATION,
-				easing: Easing.ease,
+				easing: Easing.inOut(Easing.quad),
 				useNativeDriver: true
 			},
 		).start();
@@ -95,7 +95,7 @@ export default class Sort extends Component {
 			{
 				toValue: 0,
 				duration: ANIMATION_DURATION,
-				easing: Easing.ease,
+				easing: Easing.inOut(Easing.quad),
 				useNativeDriver: true
 			},
 		).start(() => close());
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index 4cdb2060cbea003ec8866bf8b622f33baaf75752..b33363313e8d9a58315f321f975b64d075aa31be 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -26,10 +26,12 @@ import { appStart as appStartAction } from '../../actions';
 import store from '../../lib/createStore';
 import Drawer from '../../Drawer';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
+import debounce from '../../utils/debounce';
 
 const ROW_HEIGHT = 70;
 const SCROLL_OFFSET = 56;
 
+const shouldUpdateProps = ['searchText', 'loadingServer', 'showServerDropdown', 'showSortDropdown', 'sortBy', 'groupByType', 'showFavorites', 'showUnread', 'useRealName', 'appState'];
 const isAndroid = () => Platform.OS === 'android';
 const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index });
 const keyExtractor = item => item.rid;
@@ -161,7 +163,61 @@ export default class RoomsListView extends LoggedView {
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
-		return !(isEqual(this.props, nextProps) && isEqual(this.state, nextState));
+		// eslint-disable-next-line react/destructuring-assignment
+		const propsUpdated = shouldUpdateProps.some(key => nextProps[key] !== this.props[key]);
+		if (propsUpdated) {
+			return true;
+		}
+
+		const { loading, searching } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (nextState.searching !== searching) {
+			return true;
+		}
+
+		const { showUnread, showFavorites, groupByType } = this.props;
+		if (showUnread) {
+			const { unread } = this.state;
+			if (!isEqual(nextState.unread, unread)) {
+				return true;
+			}
+		}
+		if (showFavorites) {
+			const { favorites } = this.state;
+			if (!isEqual(nextState.favorites, favorites)) {
+				return true;
+			}
+		}
+		if (groupByType) {
+			const {
+				channels, privateGroup, direct, livechat
+			} = this.state;
+			if (!isEqual(nextState.channels, channels)) {
+				return true;
+			}
+			if (!isEqual(nextState.privateGroup, privateGroup)) {
+				return true;
+			}
+			if (!isEqual(nextState.direct, direct)) {
+				return true;
+			}
+			if (!isEqual(nextState.livechat, livechat)) {
+				return true;
+			}
+		} else {
+			const { chats } = this.state;
+			if (!isEqual(nextState.chats, chats)) {
+				return true;
+			}
+		}
+
+		const { search } = this.state;
+		if (!isEqual(nextState.search, search)) {
+			return true;
+		}
+		return false;
 	}
 
 	componentDidUpdate(prevProps) {
@@ -190,10 +246,6 @@ export default class RoomsListView extends LoggedView {
 		this.removeListener(this.direct);
 		this.removeListener(this.livechat);
 		BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
-
-		if (this.timeout) {
-			clearTimeout(this.timeout);
-		}
 	}
 
 	navigationButtonPressed = ({ buttonId }) => {
@@ -262,9 +314,7 @@ export default class RoomsListView extends LoggedView {
 			if (showUnread) {
 				this.unread = this.data.filtered('archived != true && open == true').filtered('(unread > 0 || alert == true)');
 				unread = this.removeRealmInstance(this.unread);
-				setTimeout(() => {
-					this.unread.addListener(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }));
-				});
+				this.unread.addListener(debounce(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }), 300));
 			} else {
 				this.removeListener(unread);
 			}
@@ -272,9 +322,7 @@ export default class RoomsListView extends LoggedView {
 			if (showFavorites) {
 				this.favorites = this.data.filtered('f == true');
 				favorites = this.removeRealmInstance(this.favorites);
-				setTimeout(() => {
-					this.favorites.addListener(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }));
-				});
+				this.favorites.addListener(debounce(() => this.internalSetState({ favorites: this.removeRealmInstance(this.favorites) }), 300));
 			} else {
 				this.removeListener(favorites);
 			}
@@ -296,12 +344,10 @@ export default class RoomsListView extends LoggedView {
 				this.livechat = this.data.filtered('t == $0', 'l');
 				livechat = this.removeRealmInstance(this.livechat);
 
-				setTimeout(() => {
-					this.channels.addListener(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }));
-					this.privateGroup.addListener(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }));
-					this.direct.addListener(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }));
-					this.livechat.addListener(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }));
-				});
+				this.channels.addListener(debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300));
+				this.privateGroup.addListener(debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300));
+				this.direct.addListener(debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300));
+				this.livechat.addListener(debounce(() => this.internalSetState({ livechat: this.removeRealmInstance(this.livechat) }), 300));
 				this.removeListener(this.chats);
 			} else {
 				// chats
@@ -312,11 +358,7 @@ export default class RoomsListView extends LoggedView {
 				}
 				chats = this.removeRealmInstance(this.chats);
 
-				setTimeout(() => {
-					this.chats.addListener(() => {
-						this.internalSetState({ chats: this.removeRealmInstance(this.chats) });
-					});
-				});
+				this.chats.addListener(debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300));
 				this.removeListener(this.channels);
 				this.removeListener(this.privateGroup);
 				this.removeListener(this.direct);
@@ -325,12 +367,9 @@ export default class RoomsListView extends LoggedView {
 
 			// setState
 			this.internalSetState({
-				chats, unread, favorites, channels, privateGroup, direct, livechat
+				chats, unread, favorites, channels, privateGroup, direct, livechat, loading: false
 			});
 		}
-		this.timeout = setTimeout(() => {
-			this.internalSetState({ loading: false });
-		}, 200);
 	}
 
 	removeRealmInstance = (data) => {
@@ -399,13 +438,13 @@ export default class RoomsListView extends LoggedView {
 		});
 	}
 
-	goRoom = (rid) => {
+	goRoom = ({ rid, name, t }) => {
 		this.cancelSearchingAndroid();
 		Navigation.push('RoomsListView', {
 			component: {
 				name: 'RoomView',
 				passProps: {
-					rid
+					rid, name, t
 				}
 			}
 		});
@@ -413,8 +452,8 @@ export default class RoomsListView extends LoggedView {
 
 	_onPressItem = async(item = {}) => {
 		if (!item.search) {
-			const { rid } = item;
-			return this.goRoom(rid);
+			const { rid, name, t } = item;
+			return this.goRoom({ rid, name, t });
 		}
 		if (item.t === 'd') {
 			// if user is using the search we need first to join/create room
@@ -422,24 +461,25 @@ export default class RoomsListView extends LoggedView {
 				const { username } = item;
 				const result = await RocketChat.createDirectMessage(username);
 				if (result.success) {
-					return this.goRoom(result.room._id);
+					return this.goRoom({ rid: result.room._id, name: username, t: 'd' });
 				}
 			} catch (e) {
 				log('RoomsListView._onPressItem', e);
 			}
 		} else {
-			const { rid } = item;
-			return this.goRoom(rid);
+			const { rid, name, t } = item;
+			return this.goRoom({ rid, name, t });
 		}
 	}
 
 	toggleSort = () => {
 		const { toggleSortDropdown } = this.props;
 
-		if (Platform.OS === 'ios') {
-			this.scroll.scrollTo({ x: 0, y: SCROLL_OFFSET, animated: true });
-		} else {
-			this.scroll.scrollTo({ x: 0, y: 0, animated: true });
+		const offset = isAndroid() ? 0 : SCROLL_OFFSET;
+		if (this.scroll.scrollTo) {
+			this.scroll.scrollTo({ x: 0, y: offset, animated: true });
+		} else if (this.scroll.scrollToOffset) {
+			this.scroll.scrollToOffset({ offset });
 		}
 		setTimeout(() => {
 			toggleSortDropdown();
@@ -461,6 +501,7 @@ export default class RoomsListView extends LoggedView {
 
 		return (
 			<Touch
+				key='rooms-list-view-sort'
 				onPress={this.toggleSort}
 				style={styles.dropdownContainerHeader}
 			>
@@ -474,10 +515,17 @@ export default class RoomsListView extends LoggedView {
 
 	renderSearchBar = () => {
 		if (Platform.OS === 'ios') {
-			return <SearchBox onChangeText={this.search} testID='rooms-list-view-search' />;
+			return <SearchBox onChangeText={this.search} testID='rooms-list-view-search' key='rooms-list-view-search' />;
 		}
 	}
 
+	renderListHeader = () => (
+		[
+			this.renderSearchBar(),
+			this.renderHeader()
+		]
+	)
+
 	renderItem = ({ item }) => {
 		const { useRealName, userId, baseUrl } = this.props;
 		const id = item.rid.replace(userId, '').trim();
@@ -504,17 +552,11 @@ export default class RoomsListView extends LoggedView {
 
 	renderSeparator = () => <View style={styles.separator} />
 
-	renderSectionHeader = (header) => {
-		const { showUnread, showFavorites, groupByType } = this.props;
-		if (!(showUnread || showFavorites || groupByType)) {
-			return null;
-		}
-		return (
-			<View style={styles.groupTitleContainer}>
-				<Text style={styles.groupTitle}>{I18n.t(header)}</Text>
-			</View>
-		);
-	}
+	renderSectionHeader = header => (
+		<View style={styles.groupTitleContainer}>
+			<Text style={styles.groupTitle}>{I18n.t(header)}</Text>
+		</View>
+	)
 
 	renderSection = (data, header) => {
 		const { showUnread, showFavorites, groupByType } = this.props;
@@ -542,6 +584,8 @@ export default class RoomsListView extends LoggedView {
 					enableEmptySections
 					removeClippedSubviews
 					keyboardShouldPersistTaps='always'
+					initialNumToRender={12}
+					windowSize={7}
 				/>
 			);
 		}
@@ -566,6 +610,8 @@ export default class RoomsListView extends LoggedView {
 					enableEmptySections
 					removeClippedSubviews
 					keyboardShouldPersistTaps='always'
+					initialNumToRender={12}
+					windowSize={7}
 				/>
 			);
 		}
@@ -590,6 +636,30 @@ export default class RoomsListView extends LoggedView {
 			return <ActivityIndicator style={styles.loading} />;
 		}
 
+		const { showUnread, showFavorites, groupByType } = this.props;
+		if (!(showUnread || showFavorites || groupByType)) {
+			const { chats, search } = this.state;
+			return (
+				<FlatList
+					ref={this.getScrollRef}
+					data={search.length ? search : chats}
+					extraData={search.length ? search : chats}
+					contentOffset={Platform.OS === 'ios' ? { x: 0, y: SCROLL_OFFSET } : {}}
+					keyExtractor={keyExtractor}
+					style={styles.list}
+					renderItem={this.renderItem}
+					ItemSeparatorComponent={this.renderSeparator}
+					ListHeaderComponent={this.renderListHeader}
+					getItemLayout={getItemLayout}
+					enableEmptySections
+					removeClippedSubviews
+					keyboardShouldPersistTaps='always'
+					initialNumToRender={12}
+					windowSize={7}
+				/>
+			);
+		}
+
 		return (
 			<ScrollView
 				ref={this.getScrollRef}
@@ -597,8 +667,7 @@ export default class RoomsListView extends LoggedView {
 				keyboardShouldPersistTaps='always'
 				testID='rooms-list-view-list'
 			>
-				{this.renderSearchBar()}
-				{this.renderHeader()}
+				{this.renderListHeader()}
 				{this.renderList()}
 			</ScrollView>
 		);
diff --git a/app/views/SearchMessagesView/index.js b/app/views/SearchMessagesView/index.js
index 74510939bdd9aad9aaebc049453c2121f2abb6b1..65226a0642d369f8c894a1d5d115a9fa631b4d31 100644
--- a/app/views/SearchMessagesView/index.js
+++ b/app/views/SearchMessagesView/index.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { View, FlatList, Text } from 'react-native';
 import { connect } from 'react-redux';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import LoggedView from '../View';
 import RCTextInput from '../../containers/TextInput';
@@ -15,7 +16,6 @@ import Message from '../../containers/message/Message';
 import scrollPersistTaps from '../../utils/scrollPersistTaps';
 import I18n from '../../i18n';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
-import database from '../../lib/realm';
 
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@@ -50,10 +50,8 @@ export default class SearchMessagesView extends LoggedView {
 
 	constructor(props) {
 		super('SearchMessagesView', props);
-		this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
 		this.state = {
 			loading: false,
-			room: this.rooms[0],
 			messages: [],
 			searchText: ''
 		};
@@ -63,17 +61,31 @@ export default class SearchMessagesView extends LoggedView {
 		this.name.focus();
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { loading, searchText, messages } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (nextState.searchText !== searchText) {
+			return true;
+		}
+		if (!equal(nextState.messages, messages)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		this.search.stop();
 	}
 
 	// eslint-disable-next-line react/sort-comp
 	search = debounce(async(searchText) => {
-		const { room } = this.state;
+		const { rid } = this.props;
 		this.setState({ searchText, loading: true, messages: [] });
 
 		try {
-			const result = await RocketChat.searchMessages(room.rid, searchText);
+			const result = await RocketChat.searchMessages(rid, searchText);
 			if (result.success) {
 				this.setState({
 					messages: result.messages || [],
diff --git a/app/views/SearchMessagesView/styles.js b/app/views/SearchMessagesView/styles.js
index 09e8163ba04338d33429250c8c045be502e1db4b..9764532648fdc8b2407e0e56377cca9dc926a160 100644
--- a/app/views/SearchMessagesView/styles.js
+++ b/app/views/SearchMessagesView/styles.js
@@ -21,5 +21,11 @@ export default StyleSheet.create({
 		height: StyleSheet.hairlineWidth,
 		backgroundColor: '#E7EBF2',
 		marginVertical: 20
+	},
+	listEmptyContainer: {
+		flex: 1,
+		alignItems: 'center',
+		justifyContent: 'flex-start',
+		backgroundColor: '#ffffff'
 	}
 });
diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js
index 9320de37166e92597daff8bd501dfd774a854a6c..4d15893bead8b71983892bcf53a3de33eb4d55cd 100644
--- a/app/views/SelectedUsersView.js
+++ b/app/views/SelectedUsersView.js
@@ -7,6 +7,7 @@ import { connect, Provider } from 'react-redux';
 import { Navigation } from 'react-native-navigation';
 import SafeAreaView from 'react-native-safe-area-view';
 import { gestureHandlerRootHOC } from 'react-native-gesture-handler';
+import equal from 'deep-equal';
 
 import {
 	addUser as addUserAction, removeUser as removeUserAction, reset as resetAction, setLoading as setLoadingAction
@@ -80,6 +81,21 @@ export default class SelectedUsersView extends LoggedView {
 		Navigation.events().bindComponent(this);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { search } = this.state;
+		const { users, loading } = this.props;
+		if (nextProps.loading !== loading) {
+			return true;
+		}
+		if (!equal(nextProps.users, users)) {
+			return true;
+		}
+		if (!equal(nextState.search, search)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentDidUpdate(prevProps) {
 		const { componentId, users } = this.props;
 		if (prevProps.users.length !== users.length) {
diff --git a/app/views/SetUsernameView.js b/app/views/SetUsernameView.js
index 4863687070785f72728bec58e595ca91c2e23522..39f48fdfd6926daa00062c6102f2f1686087c81b 100644
--- a/app/views/SetUsernameView.js
+++ b/app/views/SetUsernameView.js
@@ -72,6 +72,17 @@ export default class SetUsernameView extends LoggedView {
 		}
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { username, saving } = this.state;
+		if (nextState.username !== username) {
+			return true;
+		}
+		if (nextState.saving !== saving) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		if (this.timeout) {
 			clearTimeout(this.timeout);
diff --git a/app/views/SettingsView/index.js b/app/views/SettingsView/index.js
index 10a29fb4bf78b860fb34384bd802416198da0b97..53cf5e6da46815faf6a58b7758ddea626a025388 100644
--- a/app/views/SettingsView/index.js
+++ b/app/views/SettingsView/index.js
@@ -89,6 +89,21 @@ export default class SettingsView extends LoggedView {
 		BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { language, saving } = this.state;
+		const { userLanguage } = this.props;
+		if (nextState.language !== language) {
+			return true;
+		}
+		if (nextState.saving !== saving) {
+			return true;
+		}
+		if (nextProps.userLanguage !== userLanguage) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
 	}
diff --git a/app/views/SnippetedMessagesView/index.js b/app/views/SnippetedMessagesView/index.js
index 556139cc1133dad62ebc46cdfe3f7dc1495cf0b8..ec814e6b17aef05f2bf9f075a44faba56d893cf3 100644
--- a/app/views/SnippetedMessagesView/index.js
+++ b/app/views/SnippetedMessagesView/index.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
 import { FlatList, View, Text } from 'react-native';
 import { connect } from 'react-redux';
 import SafeAreaView from 'react-native-safe-area-view';
+import equal from 'deep-equal';
 
 import { openSnippetedMessages as openSnippetedMessagesAction, closeSnippetedMessages as closeSnippetedMessagesAction } from '../../actions/snippetedMessages';
 import LoggedView from '../View';
@@ -68,6 +69,24 @@ export default class SnippetedMessagesView extends LoggedView {
 		}
 	}
 
+	shouldComponentUpdate(nextProps, nextState) {
+		const { loading, loadingMore } = this.state;
+		const { messages, ready } = this.props;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (nextState.loadingMore !== loadingMore) {
+			return true;
+		}
+		if (nextProps.ready !== ready) {
+			return true;
+		}
+		if (!equal(nextState.messages, messages)) {
+			return true;
+		}
+		return false;
+	}
+
 	componentWillUnmount() {
 		const { closeSnippetedMessages } = this.props;
 		closeSnippetedMessages();
diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js
index a51fd84f4095a302bf51b9d2475ab0d71eb7d4a2..bf5fdb637f71a72b01f9bf7495f7396109d33bd0 100644
--- a/app/views/StarredMessagesView/index.js
+++ b/app/views/StarredMessagesView/index.js
@@ -13,7 +13,6 @@ import RCActivityIndicator from '../../containers/ActivityIndicator';
 import I18n from '../../i18n';
 import { DEFAULT_HEADER } from '../../constants/headerOptions';
 import RocketChat from '../../lib/rocketchat';
-import database from '../../lib/realm';
 
 const STAR_INDEX = 0;
 const CANCEL_INDEX = 1;
@@ -22,6 +21,7 @@ const options = [I18n.t('Unstar'), I18n.t('Cancel')];
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
 	customEmojis: state.customEmojis,
+	room: state.room,
 	user: {
 		id: state.login.user && state.login.user.id,
 		username: state.login.user && state.login.user.username,
@@ -44,18 +44,16 @@ export default class StarredMessagesView extends LoggedView {
 	}
 
 	static propTypes = {
-		rid: PropTypes.string,
 		user: PropTypes.object,
 		baseUrl: PropTypes.string,
-		customEmojis: PropTypes.object
+		customEmojis: PropTypes.object,
+		room: PropTypes.object
 	}
 
 	constructor(props) {
 		super('StarredMessagesView', props);
-		this.rooms = database.objects('subscriptions').filtered('rid = $0', props.rid);
 		this.state = {
 			loading: false,
-			room: this.rooms[0],
 			messages: []
 		};
 	}
@@ -65,7 +63,14 @@ export default class StarredMessagesView extends LoggedView {
 	}
 
 	shouldComponentUpdate(nextProps, nextState) {
-		return !equal(this.state, nextState);
+		const { loading, messages } = this.state;
+		if (nextState.loading !== loading) {
+			return true;
+		}
+		if (!equal(nextState.messages, messages)) {
+			return true;
+		}
+		return false;
 	}
 
 	onLongPress = (message) => {
@@ -101,7 +106,7 @@ export default class StarredMessagesView extends LoggedView {
 
 	load = async() => {
 		const {
-			messages, total, loading, room
+			messages, total, loading
 		} = this.state;
 		const { user } = this.props;
 		if (messages.length === total || loading) {
@@ -111,6 +116,7 @@ export default class StarredMessagesView extends LoggedView {
 		this.setState({ loading: true });
 
 		try {
+			const { room } = this.props;
 			const result = await RocketChat.getMessages(
 				room.rid,
 				room.t,
diff --git a/e2e/14-joinpublicroom.spec.js b/e2e/14-joinpublicroom.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b61b7362f6e7870e0599e3eea1ca38326f3128f1
--- /dev/null
+++ b/e2e/14-joinpublicroom.spec.js
@@ -0,0 +1,199 @@
+const {
+	device, expect, element, by, waitFor
+} = require('detox');
+const { takeScreenshot } = require('./helpers/screenshot');
+const data = require('./data');
+const { tapBack } = require('./helpers/app');
+
+const room = 'detox-public';
+
+async function mockMessage(message) {
+	await element(by.id('messagebox-input')).tap();
+	await element(by.id('messagebox-input')).typeText(`${ data.random }${ message }`);
+	await element(by.id('messagebox-send-message')).tap();
+	await waitFor(element(by.text(`${ data.random }${ message }`))).toExist().withTimeout(60000);
+};
+
+async function navigateToRoom() {
+	await element(by.id('rooms-list-view-search')).replaceText(room);
+    await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeVisible().withTimeout(60000);
+    await element(by.id(`rooms-list-view-item-${ room }`)).tap();
+    await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
+}
+
+async function navigateToRoomActions() {
+	await element(by.id('room-view-header-actions')).tap();
+	await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000);
+}
+
+describe('Join public room', () => {
+	before(async() => {
+		await device.reloadReactNative();
+		await navigateToRoom();
+	});
+
+	describe('Render', async() => {
+		it('should have room screen', async() => {
+			await expect(element(by.id('room-view'))).toBeVisible();
+		});
+
+		it('should have messages list', async() => {
+			await expect(element(by.id('room-view-messages'))).toBeVisible();
+		});
+
+		// Render - Header
+		describe('Header', async() => {
+			it('should have star button', async() => {
+				await expect(element(by.id('room-view-header-star'))).toBeVisible();
+			});
+
+			it('should have actions button ', async() => {
+				await expect(element(by.id('room-view-header-actions'))).toBeVisible();
+			});
+		});
+
+		// Render - Join
+		describe('Join', async() => {
+			it('should have join', async() => {
+				await expect(element(by.id('room-view-join'))).toBeVisible();
+			});
+
+			it('should have join text', async() => {
+				await expect(element(by.text('You are in preview mode'))).toBeVisible();
+			});
+
+			it('should have join button', async() => {
+				await expect(element(by.id('room-view-join-button'))).toBeVisible();
+			});
+
+			it('should not have messagebox', async() => {
+				await expect(element(by.id('messagebox'))).toBeNotVisible();
+			});
+		});
+
+		describe('Room Actions', async() => {
+			before(async() => {
+				await navigateToRoomActions('c');
+			});
+
+			it('should have room actions screen', async() => {
+				await expect(element(by.id('room-actions-view'))).toBeVisible();
+			});
+	
+			it('should have info', async() => {
+				await expect(element(by.id('room-actions-info'))).toBeVisible();
+			});
+	
+			it('should have voice', async() => {
+				await expect(element(by.id('room-actions-voice'))).toBeVisible();
+			});
+	
+			it('should have video', async() => {
+				await expect(element(by.id('room-actions-video'))).toBeVisible();
+			});
+
+			it('should have members', async() => {
+				await expect(element(by.id('room-actions-members'))).toBeVisible();
+			});
+	
+			it('should have files', async() => {
+				await expect(element(by.id('room-actions-files'))).toBeVisible();
+			});
+	
+			it('should have mentions', async() => {
+				await expect(element(by.id('room-actions-mentioned'))).toBeVisible();
+			});
+	
+			it('should have starred', async() => {
+				await expect(element(by.id('room-actions-starred'))).toBeVisible();
+			});
+	
+			it('should have search', async() => {
+				await expect(element(by.id('room-actions-search'))).toBeVisible();
+			});
+	
+			it('should have share', async() => {
+				await element(by.id('room-actions-list')).swipe('up');
+				await expect(element(by.id('room-actions-share'))).toBeVisible();
+			});
+	
+			it('should have pinned', async() => {
+				await expect(element(by.id('room-actions-pinned'))).toBeVisible();
+			});
+	
+			it('should have snippeted', async() => {
+				await expect(element(by.id('room-actions-snippeted'))).toBeVisible();
+			});
+	
+			it('should not have notifications', async() => {
+				await expect(element(by.id('room-actions-notifications'))).toBeNotVisible();
+			});
+	
+			it('should not have leave channel', async() => {
+				await expect(element(by.id('room-actions-leave-channel'))).toBeNotVisible();
+			});
+
+			after(async() => {
+				await tapBack();
+				await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
+			})
+		});
+
+		after(async() => {
+			takeScreenshot();
+		});
+	});
+
+	describe('Usage', async() => {
+		it('should join room', async() => {
+			await element(by.id('room-view-join-button')).tap();
+			await waitFor(element(by.id('messagebox'))).toBeVisible().withTimeout(60000);
+			await expect(element(by.id('messagebox'))).toBeVisible();
+			await expect(element(by.id('room-view-join'))).toBeNotVisible();
+		});
+
+		it('should send message', async() => {
+			await mockMessage('message');
+			await expect(element(by.text(`${ data.random }message`))).toExist();
+		});
+
+		it('should have disable notifications and leave channel', async() => {
+			await navigateToRoomActions('c');
+			await expect(element(by.id('room-actions-view'))).toBeVisible();
+			await expect(element(by.id('room-actions-info'))).toBeVisible();
+			await expect(element(by.id('room-actions-voice'))).toBeVisible();
+			await expect(element(by.id('room-actions-video'))).toBeVisible();
+			await expect(element(by.id('room-actions-members'))).toBeVisible();
+			await expect(element(by.id('room-actions-files'))).toBeVisible();
+			await expect(element(by.id('room-actions-mentioned'))).toBeVisible();
+			await expect(element(by.id('room-actions-starred'))).toBeVisible();
+			await expect(element(by.id('room-actions-search'))).toBeVisible();
+			await element(by.id('room-actions-list')).swipe('up');
+			await expect(element(by.id('room-actions-share'))).toBeVisible();
+			await expect(element(by.id('room-actions-pinned'))).toBeVisible();
+			await expect(element(by.id('room-actions-snippeted'))).toBeVisible();
+			await expect(element(by.id('room-actions-notifications'))).toBeVisible();
+			await expect(element(by.id('room-actions-leave-channel'))).toBeVisible();
+		});
+
+		it('should leave room', async() => {
+			await element(by.id('room-actions-leave-channel')).tap();
+			await waitFor(element(by.text('Yes, leave it!'))).toBeVisible().withTimeout(5000);
+			await expect(element(by.text('Yes, leave it!'))).toBeVisible();
+			await element(by.text('Yes, leave it!')).tap();
+			await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
+			await element(by.id('rooms-list-view-search')).replaceText('');
+			await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible().withTimeout(60000);
+			await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
+		});
+
+		it('should navigate to room and user should be joined', async() => {
+			await navigateToRoom();
+			await expect(element(by.id('room-view-join'))).toBeVisible();
+		})
+
+		after(async() => {
+			takeScreenshot();
+		});
+	});
+});
diff --git a/e2e/mocha.opts b/e2e/mocha.opts
index 31aa76cb2031a3129058fa97f3123ca6507b5e10..4c47bee44e4f0e66bad06d5f69edd295594d1b5f 100644
--- a/e2e/mocha.opts
+++ b/e2e/mocha.opts
@@ -1 +1 @@
---recursive --timeout 120000
\ No newline at end of file
+--recursive --timeout 120000 -b
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0aebf2ce2fdc5f354a7fa26d9ea6373c891cc4a6..4c5621966ba7dec020d5ad6f409578f23ccb2c37 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18282,21 +18282,21 @@
       }
     },
     "react-native-navigation": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.1.3.tgz",
-      "integrity": "sha512-CtjDhw7eaDWCqfhK6Fq6IUDq3agl3oGDd8SNaPF7318tLni1qTmmFdz/3CpoNlegNWhAMAjNu+ONDAbe7ksADw==",
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-2.2.1.tgz",
+      "integrity": "sha512-m0RyVQMiNNIoMlcy3zADgazRRU5qeJNOpRhx9ERA/V5t2uPq1vTGoG3bvoYbkFmsrsgyWZax4l+5AdnygDzNKg==",
       "requires": {
         "hoist-non-react-statics": "3.x.x",
-        "lodash": "4.x.x",
+        "lodash": "4.17.x",
         "prop-types": "15.x.x",
         "react-lifecycles-compat": "2.0.0",
         "tslib": "1.9.3"
       },
       "dependencies": {
         "hoist-non-react-statics": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.1.0.tgz",
-          "integrity": "sha512-MYcYuROh7SBM69xHGqXEwQqDux34s9tz+sCnxJmN18kgWh6JFdTw/5YdZtqsOdZJXddE/wUpCzfEdDrJj8p0Iw==",
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz",
+          "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==",
           "requires": {
             "react-is": "^16.3.2"
           }
@@ -19958,9 +19958,9 @@
       }
     },
     "semver": {
-      "version": "5.4.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
-      "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
     },
     "send": {
       "version": "0.16.2",
diff --git a/package.json b/package.json
index c43bf40e02698fa0c5473975f7fb92b828711b5a..4a6b3ab4106ab919bf9224c2b1d27a982fb2a397 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
     "react-native-keyboard-tracking-view": "^5.5.0",
     "react-native-markdown-renderer": "^3.2.8",
     "react-native-modal": "^7.0.0",
-    "react-native-navigation": "^2.1.3",
+    "react-native-navigation": "^2.2.1",
     "react-native-notifications": "^1.1.21",
     "react-native-optimized-flatlist": "^1.0.4",
     "react-native-picker-select": "^5.1.0",
@@ -70,6 +70,7 @@
     "redux-immutable-state-invariant": "^2.1.0",
     "redux-saga": "^0.16.2",
     "rn-fetch-blob": "^0.10.13",
+    "semver": "^5.6.0",
     "snyk": "^1.109.0",
     "strip-ansi": "^4.0.0"
   },