From 9e4ca34a800f77bb8a0d863551ff1aca891991b6 Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Mon, 18 Jun 2018 10:30:36 -0300
Subject: [PATCH] Settings/Permissions improvements (#325)

* Changed the way we read RocketChat settings since setting.type won't be returned from server anymore

* Permissions

* Unnecessary action sheet render
---
 app/actions/actionsTypes.js             |   4 -
 app/actions/index.js                    |   7 -
 app/actions/messages.js                 |  27 ----
 app/constants/permissions.js            |  17 +++
 app/constants/settings.js               |  91 +++++++++--
 app/constants/types.js                  |   1 -
 app/containers/MessageActions.js        | 195 +++++++++++-------------
 app/containers/MessageErrorActions.js   |   9 +-
 app/containers/Routes.js                |   4 +
 app/lib/methods/getPermissions.js       |  27 ++--
 app/lib/methods/getSettings.js          |   9 +-
 app/lib/realm.js                        |   3 +-
 app/lib/rocketchat.js                   |  43 +++---
 app/reducers/index.js                   |   2 -
 app/reducers/messages.js                |  11 --
 app/reducers/permissions.js             |  24 ---
 app/sagas/messages.js                   |  13 --
 app/sagas/rooms.js                      |   3 +-
 app/sagas/selectServer.js               |   2 -
 app/views/RoomInfoView/index.js         |   9 +-
 app/views/RoomView/index.js             |  23 +--
 app/views/RoomsListView/Header/index.js |  10 +-
 e2e/09-roominfo.spec.js                 |   2 +-
 package-lock.json                       |   2 +-
 24 files changed, 255 insertions(+), 283 deletions(-)
 create mode 100644 app/constants/permissions.js
 delete mode 100644 app/reducers/permissions.js

diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index f2d87dd23..2b2429a38 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -62,10 +62,6 @@ export const MESSAGES = createRequestTypes('MESSAGES', [
 	'TOGGLE_STAR_REQUEST',
 	'TOGGLE_STAR_SUCCESS',
 	'TOGGLE_STAR_FAILURE',
-	'PERMALINK_REQUEST',
-	'PERMALINK_SUCCESS',
-	'PERMALINK_FAILURE',
-	'PERMALINK_CLEAR',
 	'TOGGLE_PIN_REQUEST',
 	'TOGGLE_PIN_SUCCESS',
 	'TOGGLE_PIN_FAILURE',
diff --git a/app/actions/index.js b/app/actions/index.js
index b597699b4..8d117c711 100644
--- a/app/actions/index.js
+++ b/app/actions/index.js
@@ -32,13 +32,6 @@ export function setAllSettings(settings) {
 	};
 }
 
-export function setAllPermissions(permissions) {
-	return {
-		type: types.SET_ALL_PERMISSIONS,
-		payload: permissions
-	};
-}
-
 export function setCustomEmojis(emojis) {
 	return {
 		type: types.SET_CUSTOM_EMOJIS,
diff --git a/app/actions/messages.js b/app/actions/messages.js
index edaaefb17..b7b3ee690 100644
--- a/app/actions/messages.js
+++ b/app/actions/messages.js
@@ -117,33 +117,6 @@ export function toggleStarFailure() {
 	};
 }
 
-export function permalinkRequest(message) {
-	return {
-		type: types.MESSAGES.PERMALINK_REQUEST,
-		message
-	};
-}
-
-export function permalinkSuccess(permalink) {
-	return {
-		type: types.MESSAGES.PERMALINK_SUCCESS,
-		permalink
-	};
-}
-
-export function permalinkFailure(err) {
-	return {
-		type: types.MESSAGES.PERMALINK_FAILURE,
-		err
-	};
-}
-
-export function permalinkClear() {
-	return {
-		type: types.MESSAGES.PERMALINK_CLEAR
-	};
-}
-
 export function togglePinRequest(message) {
 	return {
 		type: types.MESSAGES.TOGGLE_PIN_REQUEST,
diff --git a/app/constants/permissions.js b/app/constants/permissions.js
new file mode 100644
index 000000000..16ce1cfc3
--- /dev/null
+++ b/app/constants/permissions.js
@@ -0,0 +1,17 @@
+export default [
+	'add-user-to-any-c-room',
+	'add-user-to-any-p-room',
+	'add-user-to-joined-room',
+	'archive-room',
+	'delete-c',
+	'delete-message',
+	'delete-p',
+	'edit-message',
+	'edit-room',
+	'force-delete-message',
+	'mute-user',
+	'set-react-when-readonly',
+	'set-readonly',
+	'unarchive-room',
+	'view-broadcast-member-list'
+];
diff --git a/app/constants/settings.js b/app/constants/settings.js
index 8f8a714e6..048aa00d4 100644
--- a/app/constants/settings.js
+++ b/app/constants/settings.js
@@ -1,15 +1,80 @@
 export default {
-	boolean: 'valueAsBoolean',
-	int: 'valueAsNumber',
-	string: 'valueAsString',
-	select: 'valueAsString',
-	code: 'valueAsString',
-	relativeUrl: 'valueAsString',
-	language: 'valueAsString',
-	action: 'valueAsString',
-	password: 'valueAsString',
-	// asset: ' Object',
-	color: 'valueAsString',
-	font: 'valueAsString',
-	roomPick: 'valueAsString'
+	Accounts_CustomFields: {
+		type: 'valueAsString'
+	},
+	Accounts_EmailOrUsernamePlaceholder: {
+		type: 'valueAsString'
+	},
+	Accounts_NamePlaceholder: {
+		type: 'valueAsString'
+	},
+	Accounts_OAuth_Facebook: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_OAuth_Github: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_OAuth_Gitlab: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_OAuth_Google: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_OAuth_Linkedin: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_OAuth_Meteor: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_OAuth_Twitter: {
+		type: 'valueAsBoolean'
+	},
+	Accounts_PasswordPlaceholder: {
+		type: 'valueAsString'
+	},
+	Accounts_RepeatPasswordPlaceholder: {
+		type: 'valueAsString'
+	},
+	CROWD_Enable: {
+		type: 'valueAsBoolean'
+	},
+	Layout_Privacy_Policy: {
+		type: 'valueAsString'
+	},
+	Layout_Terms_of_Service: {
+		type: 'valueAsString'
+	},
+	LDAP_Enable: {
+		type: 'valueAsBoolean'
+	},
+	Message_AllowDeleting: {
+		type: 'valueAsBoolean'
+	},
+	Message_AllowDeleting_BlockDeleteInMinutes: {
+		type: 'valueAsNumber'
+	},
+	Message_AllowEditing: {
+		type: 'valueAsBoolean'
+	},
+	Message_AllowEditing_BlockEditInMinutes: {
+		type: 'valueAsNumber'
+	},
+	Message_AllowPinning: {
+		type: 'valueAsBoolean'
+	},
+	Message_AllowStarring: {
+		type: 'valueAsBoolean'
+	},
+	Message_GroupingPeriod: {
+		type: 'valueAsNumber'
+	},
+	Message_TimeFormat: {
+		type: 'valueAsString'
+	},
+	Site_Url: {
+		type: 'valueAsString'
+	},
+	Store_Last_Message: {
+		type: 'valueAsBoolean'
+	}
 };
diff --git a/app/constants/types.js b/app/constants/types.js
index 3e65838cf..8c5a6c8ee 100644
--- a/app/constants/types.js
+++ b/app/constants/types.js
@@ -1,5 +1,4 @@
 export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
 export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS';
-export const SET_ALL_PERMISSIONS = 'SET_ALL_PERMISSIONS';
 export const SET_CUSTOM_EMOJIS = 'SET_CUSTOM_EMOJIS';
 export const ADD_SETTINGS = 'ADD_SETTINGS';
diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js
index ee63c6ad9..7dc91f028 100644
--- a/app/containers/MessageActions.js
+++ b/app/containers/MessageActions.js
@@ -9,8 +9,6 @@ import {
 	deleteRequest,
 	editInit,
 	toggleStarRequest,
-	permalinkRequest,
-	permalinkClear,
 	togglePinRequest,
 	setInput,
 	actionsHide,
@@ -22,11 +20,8 @@ import I18n from '../i18n';
 
 @connect(
 	state => ({
-		showActions: state.messages.showActions,
 		actionMessage: state.messages.actionMessage,
 		user: state.login.user,
-		permissions: state.permissions,
-		permalink: state.messages.permalink,
 		Message_AllowDeleting: state.settings.Message_AllowDeleting,
 		Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes,
 		Message_AllowEditing: state.settings.Message_AllowEditing,
@@ -39,8 +34,6 @@ import I18n from '../i18n';
 		deleteRequest: message => dispatch(deleteRequest(message)),
 		editInit: message => dispatch(editInit(message)),
 		toggleStarRequest: message => dispatch(toggleStarRequest(message)),
-		permalinkRequest: message => dispatch(permalinkRequest(message)),
-		permalinkClear: () => dispatch(permalinkClear()),
 		togglePinRequest: message => dispatch(togglePinRequest(message)),
 		setInput: message => dispatch(setInput(message)),
 		toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
@@ -49,19 +42,14 @@ import I18n from '../i18n';
 export default class MessageActions extends React.Component {
 	static propTypes = {
 		actionsHide: PropTypes.func.isRequired,
-		showActions: PropTypes.bool.isRequired,
 		room: PropTypes.object,
 		actionMessage: PropTypes.object,
 		user: PropTypes.object,
-		permissions: PropTypes.object.isRequired,
 		deleteRequest: PropTypes.func.isRequired,
 		editInit: PropTypes.func.isRequired,
 		toggleStarRequest: PropTypes.func.isRequired,
-		permalinkRequest: PropTypes.func.isRequired,
-		permalinkClear: PropTypes.func.isRequired,
 		togglePinRequest: PropTypes.func.isRequired,
 		setInput: PropTypes.func.isRequired,
-		permalink: PropTypes.string,
 		toggleReactionPicker: PropTypes.func.isRequired,
 		Message_AllowDeleting: PropTypes.bool,
 		Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number,
@@ -73,99 +61,70 @@ export default class MessageActions extends React.Component {
 
 	constructor(props) {
 		super(props);
-		this.state = {
-			copyPermalink: false,
-			reply: false,
-			quote: false
-		};
 		this.handleActionPress = this.handleActionPress.bind(this);
-		this.options = [''];
-		this.setPermissions(this.props.permissions);
-	}
+		this.setPermissions();
 
-	async componentWillReceiveProps(nextProps) {
-		if (nextProps.showActions !== this.props.showActions && nextProps.showActions) {
-			const { actionMessage } = nextProps;
-			// Cancel
-			this.options = [I18n.t('Cancel')];
-			this.CANCEL_INDEX = 0;
-			// Reply
-			if (!this.isRoomReadOnly()) {
-				this.options.push(I18n.t('Reply'));
-				this.REPLY_INDEX = this.options.length - 1;
-			}
-			// Edit
-			if (this.allowEdit(nextProps)) {
-				this.options.push(I18n.t('Edit'));
-				this.EDIT_INDEX = this.options.length - 1;
-			}
-			// Permalink
-			this.options.push(I18n.t('Copy_Permalink'));
-			this.PERMALINK_INDEX = this.options.length - 1;
-			// Copy
-			this.options.push(I18n.t('Copy_Message'));
-			this.COPY_INDEX = this.options.length - 1;
-			// Share
-			this.options.push(I18n.t('Share_Message'));
-			this.SHARE_INDEX = this.options.length - 1;
-			// Quote
-			if (!this.isRoomReadOnly()) {
-				this.options.push(I18n.t('Quote'));
-				this.QUOTE_INDEX = this.options.length - 1;
-			}
-			// Star
-			if (this.props.Message_AllowStarring) {
-				this.options.push(I18n.t(actionMessage.starred ? 'Unstar' : 'Star'));
-				this.STAR_INDEX = this.options.length - 1;
-			}
-			// Pin
-			if (this.props.Message_AllowPinning) {
-				this.options.push(I18n.t(actionMessage.pinned ? 'Unpin' : 'Pin'));
-				this.PIN_INDEX = this.options.length - 1;
-			}
-			// Reaction
-			if (!this.isRoomReadOnly() || this.canReactWhenReadOnly()) {
-				this.options.push(I18n.t('Add_Reaction'));
-				this.REACTION_INDEX = this.options.length - 1;
-			}
-			// Delete
-			if (this.allowDelete(nextProps)) {
-				this.options.push(I18n.t('Delete'));
-				this.DELETE_INDEX = this.options.length - 1;
-			}
-			setTimeout(() => {
-				this.ActionSheet.show();
-				Vibration.vibrate(50);
-			});
-		} else if (this.props.permalink !== nextProps.permalink && nextProps.permalink) {
-			// copy permalink
-			if (this.state.copyPermalink) {
-				this.setState({ copyPermalink: false });
-				await Clipboard.setString(nextProps.permalink);
-				showToast(I18n.t('Permalink_copied_to_clipboard'));
-				this.props.permalinkClear();
-			// quote
-			} else if (this.state.quote) {
-				this.setState({ quote: false });
-				const msg = `[ ](${ nextProps.permalink }) `;
-				this.props.setInput({ msg });
-
-			// reply
-			} else if (this.state.reply) {
-				this.setState({ reply: false });
-				let msg = `[ ](${ nextProps.permalink }) `;
-
-				// if original message wasn't sent by current user and neither from a direct room
-				if (this.props.user.username !== this.props.actionMessage.u.username && this.props.room.t !== 'd') {
-					msg += `@${ this.props.actionMessage.u.username } `;
-				}
-				this.props.setInput({ msg });
-			}
+		// Cancel
+		this.options = [I18n.t('Cancel')];
+		this.CANCEL_INDEX = 0;
+
+		// Reply
+		if (!this.isRoomReadOnly()) {
+			this.options.push(I18n.t('Reply'));
+			this.REPLY_INDEX = this.options.length - 1;
 		}
-	}
 
-	componentDidUpdate() {
-		this.setPermissions(this.props.permissions);
+		// Edit
+		if (this.allowEdit(props)) {
+			this.options.push(I18n.t('Edit'));
+			this.EDIT_INDEX = this.options.length - 1;
+		}
+
+		// Permalink
+		this.options.push(I18n.t('Copy_Permalink'));
+		this.PERMALINK_INDEX = this.options.length - 1;
+
+		// Copy
+		this.options.push(I18n.t('Copy_Message'));
+		this.COPY_INDEX = this.options.length - 1;
+
+		// Share
+		this.options.push(I18n.t('Share_Message'));
+		this.SHARE_INDEX = this.options.length - 1;
+
+		// Quote
+		if (!this.isRoomReadOnly()) {
+			this.options.push(I18n.t('Quote'));
+			this.QUOTE_INDEX = this.options.length - 1;
+		}
+
+		// Star
+		if (this.props.Message_AllowStarring) {
+			this.options.push(I18n.t(props.actionMessage.starred ? 'Unstar' : 'Star'));
+			this.STAR_INDEX = this.options.length - 1;
+		}
+
+		// Pin
+		if (this.props.Message_AllowPinning) {
+			this.options.push(I18n.t(props.actionMessage.pinned ? 'Unpin' : 'Pin'));
+			this.PIN_INDEX = this.options.length - 1;
+		}
+
+		// Reaction
+		if (!this.isRoomReadOnly() || this.canReactWhenReadOnly()) {
+			this.options.push(I18n.t('Add_Reaction'));
+			this.REACTION_INDEX = this.options.length - 1;
+		}
+
+		// Delete
+		if (this.allowDelete(props)) {
+			this.options.push(I18n.t('Delete'));
+			this.DELETE_INDEX = this.options.length - 1;
+		}
+		setTimeout(() => {
+			this.ActionSheet.show();
+			Vibration.vibrate(50);
+		});
 	}
 
 	setPermissions() {
@@ -176,6 +135,14 @@ export default class MessageActions extends React.Component {
 		this.hasForceDeletePermission = result[permissions[2]];
 	}
 
+	getPermalink = async(message) => {
+		try {
+			return await RocketChat.getPermalink(message);
+		} catch (error) {
+			return null;
+		}
+	}
+
 	isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id;
 
 	isRoomReadOnly = () => this.props.room.ro;
@@ -272,23 +239,31 @@ export default class MessageActions extends React.Component {
 		this.props.toggleStarRequest(this.props.actionMessage);
 	}
 
-	handlePermalink() {
-		this.setState({ copyPermalink: true });
-		this.props.permalinkRequest(this.props.actionMessage);
+	async handlePermalink() {
+		const permalink = await this.getPermalink(this.props.actionMessage);
+		Clipboard.setString(permalink);
+		showToast(I18n.t('Permalink_copied_to_clipboard'));
 	}
 
 	handlePin() {
 		this.props.togglePinRequest(this.props.actionMessage);
 	}
 
-	handleReply() {
-		this.setState({ reply: true });
-		this.props.permalinkRequest(this.props.actionMessage);
+	async handleReply() {
+		const permalink = await this.getPermalink(this.props.actionMessage);
+		let msg = `[ ](${ permalink }) `;
+
+		// if original message wasn't sent by current user and neither from a direct room
+		if (this.props.user.username !== this.props.actionMessage.u.username && this.props.room.t !== 'd') {
+			msg += `@${ this.props.actionMessage.u.username } `;
+		}
+		this.props.setInput({ msg });
 	}
 
-	handleQuote() {
-		this.setState({ quote: true });
-		this.props.permalinkRequest(this.props.actionMessage);
+	async handleQuote() {
+		const permalink = await this.getPermalink(this.props.actionMessage);
+		const msg = `[ ](${ permalink }) `;
+		this.props.setInput({ msg });
 	}
 
 	handleReaction() {
diff --git a/app/containers/MessageErrorActions.js b/app/containers/MessageErrorActions.js
index 4a8a9eeb5..3f89bf86d 100644
--- a/app/containers/MessageErrorActions.js
+++ b/app/containers/MessageErrorActions.js
@@ -11,7 +11,6 @@ import I18n from '../i18n';
 
 @connect(
 	state => ({
-		showErrorActions: state.messages.showErrorActions,
 		actionMessage: state.messages.actionMessage
 	}),
 	dispatch => ({
@@ -21,7 +20,6 @@ import I18n from '../i18n';
 export default class MessageErrorActions extends React.Component {
 	static propTypes = {
 		errorActionsHide: PropTypes.func.isRequired,
-		showErrorActions: PropTypes.bool.isRequired,
 		actionMessage: PropTypes.object
 	};
 
@@ -32,12 +30,7 @@ export default class MessageErrorActions extends React.Component {
 		this.CANCEL_INDEX = 0;
 		this.DELETE_INDEX = 1;
 		this.RESEND_INDEX = 2;
-	}
-
-	componentWillReceiveProps(nextProps) {
-		if (nextProps.showErrorActions !== this.props.showErrorActions && nextProps.showErrorActions) {
-			this.ActionSheet.show();
-		}
+		this.ActionSheet.show();
 	}
 
 	handleResend = protectedFunction(() => RocketChat.resendMessage(this.props.actionMessage._id));
diff --git a/app/containers/Routes.js b/app/containers/Routes.js
index bb20c90b8..b4f0e0080 100644
--- a/app/containers/Routes.js
+++ b/app/containers/Routes.js
@@ -57,6 +57,10 @@ export default class Routes extends React.Component {
 		NavigationService.setNavigator(this.navigator);
 	}
 
+	componentWillUnmount() {
+		Linking.removeAllListeners();
+	}
+
 	handleOpenURL({ url }) {
 		if (url) {
 			url = url.replace(/rocketchat:\/\/|https:\/\/go.rocket.chat\//, '');
diff --git a/app/lib/methods/getPermissions.js b/app/lib/methods/getPermissions.js
index cfdc21c3d..6772c775a 100644
--- a/app/lib/methods/getPermissions.js
+++ b/app/lib/methods/getPermissions.js
@@ -1,26 +1,29 @@
 import { InteractionManager } from 'react-native';
 
-import reduxStore from '../createStore';
-// import { get } from './helpers/rest';
-
 import database from '../realm';
-import * as actions from '../../actions';
 import log from '../../utils/log';
+import defaultPermissions from '../../constants/permissions';
 
-const getLastMessage = () => {
+const getLastUpdate = () => {
 	const setting = database.objects('permissions').sorted('_updatedAt', true)[0];
 	return setting && setting._updatedAt;
 };
 
-
 export default async function() {
 	try {
-		const lastMessage = getLastMessage();
-		const result = await (!lastMessage ? this.ddp.call('permissions/get') : this.ddp.call('permissions/get', new Date(lastMessage)));
-		const permissions = this._preparePermissions(result.update || result);
-		InteractionManager.runAfterInteractions(() => database.write(() =>
-			permissions.forEach(permission => database.create('permissions', permission, true))));
-		reduxStore.dispatch(actions.setAllPermissions(this.parsePermissions(permissions)));
+		const lastUpdate = getLastUpdate();
+		const result = await (!lastUpdate ? this.ddp.call('permissions/get') : this.ddp.call('permissions/get', new Date(lastUpdate)));
+		const permissions = (result.update || result).filter(permission => defaultPermissions.includes(permission._id));
+		permissions
+			.map((permission) => {
+				permission._updatedAt = new Date();
+				permission.roles = permission.roles.map(role => ({ value: role }));
+				return permission;
+			});
+
+		InteractionManager.runAfterInteractions(() =>
+			database.write(() =>
+				permissions.forEach(permission => database.create('permissions', permission, true))));
 	} catch (e) {
 		log('getPermissions', e);
 	}
diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js
index 63ed4bd81..ae4fc63e7 100644
--- a/app/lib/methods/getSettings.js
+++ b/app/lib/methods/getSettings.js
@@ -6,21 +6,22 @@ import database from '../realm';
 import * as actions from '../../actions';
 import log from '../../utils/log';
 
-const getLastMessage = () => {
+const getLastUpdate = () => {
 	const [setting] = database.objects('settings').sorted('_updatedAt', true);
 	return setting && setting._updatedAt;
 };
 
 export default async function() {
 	try {
-		const lastMessage = getLastMessage();
-		const result = await (!lastMessage ? this.ddp.call('public-settings/get') : this.ddp.call('public-settings/get', new Date(lastMessage)));
+		const lastUpdate = getLastUpdate();
+		const result = await (!lastUpdate ? this.ddp.call('public-settings/get') : this.ddp.call('public-settings/get', new Date(lastUpdate)));
 
 		const filteredSettings = this._prepareSettings(this._filterSettings(result.update || result));
 
 		InteractionManager.runAfterInteractions(() =>
 			database.write(() =>
-				filteredSettings.forEach(setting => database.create('settings', setting, true))));
+				filteredSettings.forEach(setting =>
+					database.create('settings', { ...setting, _updatedAt: new Date() }, true))));
 		reduxStore.dispatch(actions.addSettings(this.parseSettings(filteredSettings)));
 	} catch (e) {
 		log('getSettings', e);
diff --git a/app/lib/realm.js b/app/lib/realm.js
index ee7b84640..260b160aa 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -106,8 +106,9 @@ const subscriptionSchema = {
 
 const usersSchema = {
 	name: 'users',
-	primaryKey: 'username',
+	primaryKey: '_id',
 	properties: {
+		_id: 'string',
 		username: 'string',
 		name: { type: 'string', optional: true },
 		avatarVersion: { type: 'int', optional: true }
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 420768755..715f2ff4d 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -1,10 +1,10 @@
-import { AsyncStorage, Platform } from 'react-native';
+import { AsyncStorage, Platform, InteractionManager } from 'react-native';
 import { hashPassword } from 'react-native-meteor/lib/utils';
 import foreach from 'lodash/forEach';
 import RNFetchBlob from 'react-native-fetch-blob';
 
 import reduxStore from './createStore';
-import settingsType from '../constants/settings';
+import defaultSettings from '../constants/settings';
 import messagesStatus from '../constants/messagesStatus';
 import database from './realm';
 import log from '../utils/log';
@@ -104,7 +104,7 @@ const RocketChat = {
 			reduxStore.dispatch(setActiveUser(this.activeUsers));
 			this._setUserTimer = null;
 			return this.activeUsers = {};
-		}, 3000);
+		}, 5000);
 
 		const activeUser = reduxStore.getState().activeUsers[ddpMessage.id];
 		if (!ddpMessage.fields) {
@@ -190,16 +190,13 @@ const RocketChat = {
 				// we're using it only because our image cache lib doesn't support clear cache
 				if (ddpMessage.fields && ddpMessage.fields.eventName === 'updateAvatar') {
 					const { args } = ddpMessage.fields;
-					database.write(() => {
+					InteractionManager.runAfterInteractions(() =>
 						args.forEach((arg) => {
 							const user = database.objects('users').filtered('username = $0', arg.username);
-							if (!user.length) {
-								database.create('users', { username: arg.username, avatarVersion: 0 });
-							} else {
+							if (user.length > 0) {
 								user[0].avatarVersion += 1;
 							}
-						});
-					});
+						}));
 				}
 			});
 
@@ -687,27 +684,17 @@ const RocketChat = {
 	getPermissions,
 	getCustomEmoji,
 	parseSettings: settings => settings.reduce((ret, item) => {
-		ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.valueAsNumber ||
+		ret[item._id] = item[defaultSettings[item._id].type] || item.valueAsString || item.valueAsNumber ||
 			item.valueAsBoolean || item.value;
 		return ret;
 	}, {}),
 	_prepareSettings(settings) {
 		return settings.map((setting) => {
-			setting[settingsType[setting.type]] = setting.value;
+			setting[defaultSettings[setting._id].type] = setting.value;
 			return setting;
 		});
 	},
-	_filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value),
-	parsePermissions: permissions => permissions.reduce((ret, item) => {
-		ret[item._id] = item.roles.reduce((roleRet, role) => [...roleRet, role.value], []);
-		return ret;
-	}, {}),
-	_preparePermissions(permissions) {
-		permissions.forEach((permission) => {
-			permission.roles = permission.roles.map(role => ({ value: role }));
-		});
-		return permissions;
-	},
+	_filterSettings: settings => settings.filter(setting => defaultSettings[setting._id] && setting.value),
 	parseEmojis: emojis => emojis.reduce((ret, item) => {
 		ret[item.name] = item.extension;
 		item.aliases.forEach((alias) => {
@@ -843,22 +830,26 @@ const RocketChat = {
 	hasPermission(permissions, rid) {
 		// get the room from realm
 		const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
+		// 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
 		const userRoles = reduxStore.getState().login.user.roles || [];
-		// get all permissions from redux
-		const allPermissions = reduxStore.getState().permissions;
 		// merge both roles
 		const mergedRoles = [...new Set([...roomRoles, ...userRoles])];
 
 		// return permissions in object format
 		// e.g. { 'edit-room': true, 'set-readonly': false }
 		return permissions.reduce((result, permission) => {
-			result[permission] = returnAnArray(allPermissions[permission])
-				.some(item => mergedRoles.indexOf(item) !== -1);
+			result[permission] = false;
+			const permissionFound = permissionsFiltered.find(p => p._id === permission);
+			if (permissionFound) {
+				result[permission] = returnAnArray(permissionFound.roles).some(r => mergedRoles.includes(r.value));
+			}
 			return result;
 		}, {});
 	},
diff --git a/app/reducers/index.js b/app/reducers/index.js
index 6f66738e6..27d07f44b 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -10,7 +10,6 @@ import navigator from './navigator';
 import selectedUsers from './selectedUsers';
 import createChannel from './createChannel';
 import app from './app';
-import permissions from './permissions';
 import customEmojis from './customEmojis';
 import activeUsers from './activeUsers';
 import roles from './roles';
@@ -32,7 +31,6 @@ export default combineReducers({
 	app,
 	room,
 	rooms,
-	permissions,
 	customEmojis,
 	activeUsers,
 	roles,
diff --git a/app/reducers/messages.js b/app/reducers/messages.js
index ae17f395e..caec481d2 100644
--- a/app/reducers/messages.js
+++ b/app/reducers/messages.js
@@ -6,7 +6,6 @@ const initialState = {
 	message: {},
 	actionMessage: {},
 	editing: false,
-	permalink: '',
 	showActions: false,
 	showErrorActions: false,
 	showReactionPicker: false
@@ -77,16 +76,6 @@ export default function messages(state = initialState, action) {
 				message: {},
 				editing: false
 			};
-		case types.MESSAGES.PERMALINK_SUCCESS:
-			return {
-				...state,
-				permalink: action.permalink
-			};
-		case types.MESSAGES.PERMALINK_CLEAR:
-			return {
-				...state,
-				permalink: ''
-			};
 		case types.MESSAGES.SET_INPUT:
 			return {
 				...state,
diff --git a/app/reducers/permissions.js b/app/reducers/permissions.js
deleted file mode 100644
index 58d749907..000000000
--- a/app/reducers/permissions.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as types from '../constants/types';
-
-const initialState = {
-	permissions: {}
-};
-
-
-export default function permissions(state = initialState.permissions, action) {
-	if (action.type === types.SET_ALL_PERMISSIONS) {
-		return {
-			...state,
-			...action.payload
-		};
-	}
-
-	if (action.type === types.ADD_PERMISSIONS) {
-		return {
-			...state,
-			...action.payload
-		};
-	}
-
-	return state;
-}
diff --git a/app/sagas/messages.js b/app/sagas/messages.js
index 0dff63c69..5792ba324 100644
--- a/app/sagas/messages.js
+++ b/app/sagas/messages.js
@@ -10,8 +10,6 @@ import {
 	editFailure,
 	toggleStarSuccess,
 	toggleStarFailure,
-	permalinkSuccess,
-	permalinkFailure,
 	togglePinSuccess,
 	togglePinFailure,
 	setInput
@@ -24,7 +22,6 @@ import log from '../utils/log';
 const deleteMessage = message => RocketChat.deleteMessage(message);
 const editMessage = message => RocketChat.editMessage(message);
 const toggleStarMessage = message => RocketChat.toggleStarMessage(message);
-const getPermalink = message => RocketChat.getPermalink(message);
 const togglePinMessage = message => RocketChat.togglePinMessage(message);
 
 const get = function* get({ room }) {
@@ -68,15 +65,6 @@ const handleToggleStarRequest = function* handleToggleStarRequest({ message }) {
 	}
 };
 
-const handlePermalinkRequest = function* handlePermalinkRequest({ message }) {
-	try {
-		const permalink = yield call(getPermalink, message);
-		yield put(permalinkSuccess(permalink));
-	} catch (error) {
-		yield put(permalinkFailure(error));
-	}
-};
-
 const handleTogglePinRequest = function* handleTogglePinRequest({ message }) {
 	try {
 		yield call(togglePinMessage, message);
@@ -110,7 +98,6 @@ const root = function* root() {
 	yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
 	yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest);
 	yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest);
-	yield takeLatest(MESSAGES.PERMALINK_REQUEST, handlePermalinkRequest);
 	yield takeLatest(MESSAGES.TOGGLE_PIN_REQUEST, handleTogglePinRequest);
 	yield takeLatest(MESSAGES.REPLY_BROADCAST, handleReplyBroadcast);
 };
diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js
index c3808310f..0f9ceaf17 100644
--- a/app/sagas/rooms.js
+++ b/app/sagas/rooms.js
@@ -6,7 +6,7 @@ import { BACKGROUND } from 'redux-enhancer-react-native-appstate';
 import * as types from '../actions/actionsTypes';
 // import { roomsSuccess, roomsFailure } from '../actions/rooms';
 import { addUserTyping, removeUserTyping, setLastOpen } from '../actions/room';
-import { messagesRequest } from '../actions/messages';
+import { messagesRequest, editCancel } from '../actions/messages';
 import RocketChat from '../lib/rocketchat';
 import database from '../lib/realm';
 import * as NavigationService from '../containers/routes/NavigationService';
@@ -92,6 +92,7 @@ const watchRoomOpen = function* watchRoomOpen({ room }) {
 		});
 		cancel(thread);
 		sub.stop();
+		yield put(editCancel());
 
 		// subscriptions.forEach((sub) => {
 		// 	sub.unsubscribe().catch(e => alert(e));
diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js
index a95a9517f..973677f22 100644
--- a/app/sagas/selectServer.js
+++ b/app/sagas/selectServer.js
@@ -26,8 +26,6 @@ const selectServer = function* selectServer({ server }) {
 		// yield AsyncStorage.removeItem(RocketChat.TOKEN_KEY);
 		const settings = database.objects('settings');
 		yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
-		const permissions = database.objects('permissions');
-		yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length))));
 		const emojis = database.objects('customEmojis');
 		yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
 		const roles = database.objects('roles');
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
index 0047354d9..b0ed341e1 100644
--- a/app/views/RoomInfoView/index.js
+++ b/app/views/RoomInfoView/index.js
@@ -31,7 +31,6 @@ const getRoomTitle = room => (room.t === 'd' ?
 @connect(state => ({
 	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
 	user: state.login.user,
-	permissions: state.permissions,
 	activeUsers: state.activeUsers,
 	Message_TimeFormat: state.settings.Message_TimeFormat,
 	roles: state.roles
@@ -123,8 +122,12 @@ export default class RoomInfoView extends LoggedView {
 	}
 
 	getFullUserData = async(username) => {
-		const result = await RocketChat.subscribe('fullUserData', username);
-		this.sub = result;
+		try {
+			const result = await RocketChat.subscribe('fullUserData', username);
+			this.sub = result;
+		} catch (e) {
+			log('getFullUserData', e);
+		}
 	}
 
 	isDirect = () => this.state.room.t === 'd';
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index db86edd13..bb56f99d7 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -9,7 +9,7 @@ import LoggedView from '../View';
 import { List } from './ListView';
 // import * as actions from '../../actions';
 import { openRoom, setLastOpen } from '../../actions/room';
-import { editCancel, toggleReactionPicker, actionsShow } from '../../actions/messages';
+import { toggleReactionPicker, actionsShow } from '../../actions/messages';
 import database from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import Message from '../../containers/message';
@@ -22,6 +22,7 @@ import ReactionPicker from './ReactionPicker';
 import styles from './styles';
 import log from '../../utils/log';
 import I18n from '../../i18n';
+import debounce from '../../utils/debounce';
 
 @connect(
 	state => ({
@@ -29,12 +30,14 @@ import I18n from '../../i18n';
 		// Message_TimeFormat: state.settings.Message_TimeFormat,
 		loading: state.messages.isFetching,
 		user: state.login.user,
-		actionMessage: state.messages.actionMessage
+		actionMessage: state.messages.actionMessage,
+		showActions: state.messages.showActions,
+		showErrorActions: state.messages.showErrorActions
 	}),
 	dispatch => ({
 		// actions: bindActionCreators(actions, dispatch),
 		openRoom: room => dispatch(openRoom(room)),
-		editCancel: () => dispatch(editCancel()),
+		// editCancel: () => dispatch(editCancel()),
 		setLastOpen: date => dispatch(setLastOpen(date)),
 		toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
 		actionsShow: actionMessage => dispatch(actionsShow(actionMessage))
@@ -46,7 +49,7 @@ export default class RoomView extends LoggedView {
 		openRoom: PropTypes.func.isRequired,
 		setLastOpen: PropTypes.func.isRequired,
 		user: PropTypes.object.isRequired,
-		editCancel: PropTypes.func,
+		// editCancel: PropTypes.func,
 		rid: PropTypes.string,
 		name: PropTypes.string,
 		// Site_Url: PropTypes.string,
@@ -85,10 +88,10 @@ export default class RoomView extends LoggedView {
 	}
 	componentWillUnmount() {
 		this.rooms.removeAllListeners();
-		this.props.editCancel();
+		this.onEndReached.stop();
 	}
 
-	onEndReached = (lastRowData) => {
+	onEndReached = debounce((lastRowData) => {
 		if (!lastRowData) {
 			this.setState({ end: true });
 			return;
@@ -102,7 +105,7 @@ export default class RoomView extends LoggedView {
 				log('RoomView.onEndReached', e);
 			}
 		});
-	}
+	})
 
 	onMessageLongPress = (message) => {
 		this.props.actionsShow(message);
@@ -186,8 +189,6 @@ export default class RoomView extends LoggedView {
 		/>
 	);
 
-	// renderSeparator = () => <View style={styles.separator} />;
-
 	renderFooter = () => {
 		if (!this.state.joined) {
 			return (
@@ -232,8 +233,8 @@ export default class RoomView extends LoggedView {
 					renderRow={this.renderItem}
 				/>
 				{this.renderFooter()}
-				{this.state.room._id ? <MessageActions room={this.state.room} /> : null}
-				<MessageErrorActions />
+				{this.state.room._id && this.props.showActions ? <MessageActions room={this.state.room} /> : null}
+				{this.props.showErrorActions ? <MessageErrorActions /> : null}
 				<ReactionPicker onEmojiSelected={this.onReactionPress} />
 			</View>
 		);
diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js
index f9272ac2b..180f455a9 100644
--- a/app/views/RoomsListView/Header/index.js
+++ b/app/views/RoomsListView/Header/index.js
@@ -6,6 +6,7 @@ import { connect } from 'react-redux';
 import Modal from 'react-native-modal';
 import FastImage from 'react-native-fast-image';
 import { HeaderBackButton } from 'react-navigation';
+import equal from 'deep-equal';
 
 import Avatar from '../../../containers/Avatar';
 import Status from '../../../containers/status';
@@ -50,7 +51,7 @@ const title = (offline, connecting, authenticating, logged) => {
 	setSearch: searchText => dispatch(setSearch(searchText))
 }))
 
-export default class RoomsListHeaderView extends React.PureComponent {
+export default class RoomsListHeaderView extends React.Component {
 	static propTypes = {
 		navigation: PropTypes.object.isRequired,
 		user: PropTypes.object.isRequired,
@@ -67,6 +68,13 @@ export default class RoomsListHeaderView extends React.PureComponent {
 		};
 	}
 
+	shouldComponentUpdate(nextProps) {
+		if (!equal(this.props, nextProps)) {
+			return true;
+		}
+		return false;
+	}
+
 	onPressModalButton(status) {
 		try {
 			RocketChat.setUserPresenceDefaultStatus(status);
diff --git a/e2e/09-roominfo.spec.js b/e2e/09-roominfo.spec.js
index 06037a7d5..e43891946 100644
--- a/e2e/09-roominfo.spec.js
+++ b/e2e/09-roominfo.spec.js
@@ -318,7 +318,7 @@ describe('Room info screen', () => {
 				await expect(element(by.id(`rooms-list-view-item-${ room }`))).toBeNotVisible();
 			});
 	
-			after(async() => {
+			afterEach(async() => {
 				takeScreenshot();
 			});
 		});
diff --git a/package-lock.json b/package-lock.json
index 1458aa307..fa0052327 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15005,7 +15005,7 @@
           "integrity": "sha512-Bvq4FQPMAFijqjqNX6TxLgKOwdbruM6GvFwF9rb+mowbaFZVoYbHTKLaAbdPlrblgaZKWyOuuxBUoDx41+Xktg==",
           "requires": {
             "prop-types": "15.6.1",
-            "react-native-animatable": "1.2.4"
+            "react-native-animatable": "1.3.0"
           }
         }
       }
-- 
GitLab