From b1bb815b07e6a632ff8dcc043ccbea7e1569160d Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Mon, 19 Feb 2018 18:19:39 -0300
Subject: [PATCH] Room actions (#231)

* Layout


* Empty starred list


* Favorite room

* Pinned messages

* fix last messages

* fix date on pinned messages
---
 .circleci/config.yml                    |   4 +-
 app/actions/actionsTypes.js             |   2 +
 app/actions/pinnedMessages.js           |  28 ++++
 app/actions/starredMessages.js          |  28 ++++
 app/containers/message/index.js         |  15 +-
 app/containers/routes/AuthRoutes.js     |  24 ++++
 app/lib/createStore.js                  |   7 +-
 app/lib/realm.js                        |   1 +
 app/lib/rocketchat.js                   |  44 +++++-
 app/reducers/index.js                   |   6 +-
 app/reducers/pinnedMessages.js          |  24 ++++
 app/reducers/starredMessages.js         |  24 ++++
 app/sagas/index.js                      |   6 +-
 app/sagas/pinnedMessages.js             |  14 ++
 app/sagas/starredMessages.js            |  14 ++
 app/views/PinnedMessagesView/index.js   | 111 +++++++++++++++
 app/views/PinnedMessagesView/styles.js  |  17 +++
 app/views/RoomActionsView/index.js      | 175 ++++++++++++++++++++++++
 app/views/RoomActionsView/styles.js     |  54 ++++++++
 app/views/RoomView/Header/index.js      |  21 ++-
 app/views/RoomView/Header/styles.js     |   2 +-
 app/views/RoomView/index.js             |  15 +-
 app/views/StarredMessagesView/index.js  | 111 +++++++++++++++
 app/views/StarredMessagesView/styles.js |  17 +++
 24 files changed, 739 insertions(+), 25 deletions(-)
 create mode 100644 app/actions/pinnedMessages.js
 create mode 100644 app/actions/starredMessages.js
 create mode 100644 app/reducers/pinnedMessages.js
 create mode 100644 app/reducers/starredMessages.js
 create mode 100644 app/sagas/pinnedMessages.js
 create mode 100644 app/sagas/starredMessages.js
 create mode 100644 app/views/PinnedMessagesView/index.js
 create mode 100644 app/views/PinnedMessagesView/styles.js
 create mode 100644 app/views/RoomActionsView/index.js
 create mode 100644 app/views/RoomActionsView/styles.js
 create mode 100644 app/views/StarredMessagesView/index.js
 create mode 100644 app/views/StarredMessagesView/styles.js

diff --git a/.circleci/config.yml b/.circleci/config.yml
index df571e695..927bbc008 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -41,8 +41,8 @@ jobs:
       - image: circleci/android:api-26-alpha
 
     environment:
-      GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError"
-      JVM_OPTS: -Xmx2048m
+      GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"
+      JVM_OPTS: -Xmx4096m
       TERM: dumb
       BASH_ENV: "~/.nvm/nvm.sh"
 
diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 2422a207a..bb737dac1 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -89,6 +89,8 @@ export const SERVER = createRequestTypes('SERVER', [
 export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT', 'DISCONNECT_BY_USER']);
 export const LOGOUT = 'LOGOUT'; // logout is always success
 export const ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET', 'REQUEST']);
+export const STARRED_MESSAGES = createRequestTypes('STARRED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGE_RECEIVED', 'MESSAGE_UNSTARRED']);
+export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGE_RECEIVED', 'MESSAGE_UNPINNED']);
 
 export const INCREMENT = 'INCREMENT';
 export const DECREMENT = 'DECREMENT';
diff --git a/app/actions/pinnedMessages.js b/app/actions/pinnedMessages.js
new file mode 100644
index 000000000..a8a8f228e
--- /dev/null
+++ b/app/actions/pinnedMessages.js
@@ -0,0 +1,28 @@
+import * as types from './actionsTypes';
+
+export function openPinnedMessages(rid) {
+	return {
+		type: types.PINNED_MESSAGES.OPEN,
+		rid
+	};
+}
+
+export function closePinnedMessages() {
+	return {
+		type: types.PINNED_MESSAGES.CLOSE
+	};
+}
+
+export function pinnedMessageReceived(message) {
+	return {
+		type: types.PINNED_MESSAGES.MESSAGE_RECEIVED,
+		message
+	};
+}
+
+export function pinnedMessageUnpinned(messageId) {
+	return {
+		type: types.PINNED_MESSAGES.MESSAGE_UNPINNED,
+		messageId
+	};
+}
diff --git a/app/actions/starredMessages.js b/app/actions/starredMessages.js
new file mode 100644
index 000000000..d050d84c1
--- /dev/null
+++ b/app/actions/starredMessages.js
@@ -0,0 +1,28 @@
+import * as types from './actionsTypes';
+
+export function openStarredMessages(rid) {
+	return {
+		type: types.STARRED_MESSAGES.OPEN,
+		rid
+	};
+}
+
+export function closeStarredMessages() {
+	return {
+		type: types.STARRED_MESSAGES.CLOSE
+	};
+}
+
+export function starredMessageReceived(message) {
+	return {
+		type: types.STARRED_MESSAGES.MESSAGE_RECEIVED,
+		message
+	};
+}
+
+export function starredMessageUnstarred(messageId) {
+	return {
+		type: types.STARRED_MESSAGES.MESSAGE_UNSTARRED,
+		messageId
+	};
+}
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index a361ef57c..f6ce250aa 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -1,6 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
-import { View, TouchableHighlight, Text, TouchableOpacity, Vibration } from 'react-native';
+import { View, TouchableHighlight, Text, TouchableOpacity, Vibration, ViewPropTypes } from 'react-native';
 import { connect } from 'react-redux';
 import Icon from 'react-native-vector-icons/MaterialIcons';
 import moment from 'moment';
@@ -33,17 +33,18 @@ import styles from './styles';
 export default class Message extends React.Component {
 	static propTypes = {
 		item: PropTypes.object.isRequired,
-		reactions: PropTypes.object.isRequired,
+		reactions: PropTypes.any.isRequired,
 		baseUrl: PropTypes.string.isRequired,
 		Message_TimeFormat: PropTypes.string.isRequired,
 		message: PropTypes.object.isRequired,
 		user: PropTypes.object.isRequired,
 		editing: PropTypes.bool,
-		actionsShow: PropTypes.func,
 		errorActionsShow: PropTypes.func,
 		customEmojis: PropTypes.object,
 		toggleReactionPicker: PropTypes.func,
-		onReactionPress: PropTypes.func
+		onReactionPress: PropTypes.func,
+		style: ViewPropTypes.style,
+		onLongPress: PropTypes.func
 	}
 
 	constructor(props) {
@@ -73,7 +74,7 @@ export default class Message extends React.Component {
 	}
 
 	onLongPress() {
-		this.props.actionsShow(this.parseMessage());
+		this.props.onLongPress(this.parseMessage());
 	}
 
 	onErrorPress() {
@@ -222,7 +223,7 @@ export default class Message extends React.Component {
 
 	render() {
 		const {
-			item, message, editing, baseUrl, customEmojis
+			item, message, editing, baseUrl, customEmojis, style
 		} = this.props;
 		const username = item.alias || item.u.username;
 		const isEditing = message._id === item._id && editing;
@@ -235,7 +236,7 @@ export default class Message extends React.Component {
 				disabled={this.isDeleted() || this.hasError()}
 				underlayColor='#FFFFFF'
 				activeOpacity={0.3}
-				style={[styles.message, isEditing ? styles.editing : null]}
+				style={[styles.message, isEditing ? styles.editing : null, style]}
 				accessibilityLabel={accessibilityLabel}
 			>
 				<View style={styles.flex}>
diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js
index 15bbde567..2a166f424 100644
--- a/app/containers/routes/AuthRoutes.js
+++ b/app/containers/routes/AuthRoutes.js
@@ -4,9 +4,12 @@ import { StackNavigator, DrawerNavigator } from 'react-navigation';
 import Sidebar from '../../containers/Sidebar';
 import RoomsListView from '../../views/RoomsListView';
 import RoomView from '../../views/RoomView';
+import RoomActionsView from '../../views/RoomActionsView';
 import CreateChannelView from '../../views/CreateChannelView';
 import SelectUsersView from '../../views/SelectUsersView';
 import NewServerView from '../../views/NewServerView';
+import StarredMessagesView from '../../views/StarredMessagesView';
+import PinnedMessagesView from '../../views/PinnedMessagesView';
 
 const AuthRoutes = StackNavigator(
 	{
@@ -33,6 +36,27 @@ const AuthRoutes = StackNavigator(
 			navigationOptions: {
 				title: 'New server'
 			}
+		},
+		RoomActions: {
+			screen: RoomActionsView,
+			navigationOptions: {
+				title: 'Actions',
+				headerTintColor: '#292E35'
+			}
+		},
+		StarredMessages: {
+			screen: StarredMessagesView,
+			navigationOptions: {
+				title: 'Starred Messages',
+				headerTintColor: '#292E35'
+			}
+		},
+		PinnedMessages: {
+			screen: PinnedMessagesView,
+			navigationOptions: {
+				title: 'Pinned Messages',
+				headerTintColor: '#292E35'
+			}
 		}
 	},
 	{
diff --git a/app/lib/createStore.js b/app/lib/createStore.js
index b8d933e98..27f75b927 100644
--- a/app/lib/createStore.js
+++ b/app/lib/createStore.js
@@ -1,4 +1,4 @@
-import { createStore, applyMiddleware } from 'redux';
+import { createStore, applyMiddleware, compose } from 'redux';
 import createSagaMiddleware from 'redux-saga';
 import logger from 'redux-logger';
 import { composeWithDevTools } from 'remote-redux-devtools';
@@ -13,14 +13,15 @@ if (__DEV__) {
 	/* eslint-disable global-require */
 	const reduxImmutableStateInvariant = require('redux-immutable-state-invariant').default();
 
-	enhacers = composeWithDevTools(
+	const devComposer = composeWithDevTools({ hostname: 'localhost', port: 8000 });
+	enhacers = devComposer(
 		applyAppStateListener(),
 		applyMiddleware(reduxImmutableStateInvariant),
 		applyMiddleware(sagaMiddleware),
 		applyMiddleware(logger)
 	);
 } else {
-	enhacers = composeWithDevTools(
+	enhacers = compose(
 		applyAppStateListener(),
 		applyMiddleware(sagaMiddleware)
 	);
diff --git a/app/lib/realm.js b/app/lib/realm.js
index bca5a7a11..ea1338bda 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -82,6 +82,7 @@ const subscriptionSchema = {
 		// groupMentions: 0,
 		roomUpdatedAt: { type: 'date', optional: true },
 		ro: { type: 'bool', optional: true },
+		lastOpen: { type: 'date', optional: true },
 		lastMessage: { type: 'messages', optional: true }
 	}
 };
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index d2c0d9403..0a4ac8a1a 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -13,6 +13,8 @@ import { someoneTyping, roomMessageReceived } from '../actions/room';
 import { setUser } from '../actions/login';
 import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect';
 import { requestActiveUser } from '../actions/activeUsers';
+import { starredMessageReceived, starredMessageUnstarred } from '../actions/starredMessages';
+import { pinnedMessageReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages';
 import Ddp from './ddp';
 
 export { Accounts } from 'react-native-meteor';
@@ -145,6 +147,30 @@ const RocketChat = {
 					});
 				}
 			});
+
+			this.ddp.on('rocketchat_starred_message', (ddpMessage) => {
+				if (ddpMessage.msg === 'added') {
+					const message = ddpMessage.fields;
+					message._id = ddpMessage.id;
+					const starredMessage = this._buildMessage(message);
+					return reduxStore.dispatch(starredMessageReceived(starredMessage));
+				}
+				if (ddpMessage.msg === 'removed') {
+					return reduxStore.dispatch(starredMessageUnstarred(ddpMessage.id));
+				}
+			});
+
+			this.ddp.on('rocketchat_pinned_message', (ddpMessage) => {
+				if (ddpMessage.msg === 'added') {
+					const message = ddpMessage.fields;
+					message._id = ddpMessage.id;
+					const pinnedMessage = this._buildMessage(message);
+					return reduxStore.dispatch(pinnedMessageReceived(pinnedMessage));
+				}
+				if (ddpMessage.msg === 'removed') {
+					return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id));
+				}
+			});
 		}).catch(console.log);
 	},
 
@@ -272,9 +298,7 @@ const RocketChat = {
 	_buildMessage(message) {
 		message.status = messagesStatus.SENT;
 		normalizeMessage(message);
-		if (message.urls) {
-			message.urls = RocketChat._parseUrls(message.urls);
-		}
+		message.urls = message.urls ? RocketChat._parseUrls(message.urls) : [];
 		// loadHistory returns message.starred as object
 		// stream-room-messages returns message.starred as an array
 		message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred);
@@ -358,8 +382,15 @@ const RocketChat = {
 	createDirectMessage(username) {
 		return call('createDirectMessage', username);
 	},
-	readMessages(rid) {
-		return call('readMessages', rid);
+	async readMessages(rid) {
+		const ret = await call('readMessages', rid);
+
+		const [subscription] = database.objects('subscriptions').filtered('rid = $0', rid);
+		database.write(() => {
+			subscription.lastOpen = new Date();
+		});
+
+		return ret;
 	},
 	joinRoom(rid) {
 		return call('joinRoom', rid);
@@ -610,6 +641,9 @@ const RocketChat = {
 	},
 	setReaction(emoji, messageId) {
 		return call('setReaction', emoji, messageId);
+	},
+	toggleFavorite(rid, f) {
+		return call('toggleFavorite', rid, !f);
 	}
 };
 
diff --git a/app/reducers/index.js b/app/reducers/index.js
index f7872a539..f9a53f2a6 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -12,6 +12,8 @@ import app from './app';
 import permissions from './permissions';
 import customEmojis from './customEmojis';
 import activeUsers from './activeUsers';
+import starredMessages from './starredMessages';
+import pinnedMessages from './pinnedMessages';
 
 export default combineReducers({
 	settings,
@@ -26,5 +28,7 @@ export default combineReducers({
 	rooms,
 	permissions,
 	customEmojis,
-	activeUsers
+	activeUsers,
+	starredMessages,
+	pinnedMessages
 });
diff --git a/app/reducers/pinnedMessages.js b/app/reducers/pinnedMessages.js
new file mode 100644
index 000000000..7b9e1460a
--- /dev/null
+++ b/app/reducers/pinnedMessages.js
@@ -0,0 +1,24 @@
+import { PINNED_MESSAGES } from '../actions/actionsTypes';
+
+const initialState = {
+	messages: []
+};
+
+export default function server(state = initialState, action) {
+	switch (action.type) {
+		case PINNED_MESSAGES.MESSAGE_RECEIVED:
+			return {
+				...state,
+				messages: [...state.messages, action.message]
+			};
+		case PINNED_MESSAGES.MESSAGE_UNPINNED:
+			return {
+				...state,
+				messages: state.messages.filter(message => message._id !== action.messageId)
+			};
+		case PINNED_MESSAGES.CLOSE:
+			return initialState;
+		default:
+			return state;
+	}
+}
diff --git a/app/reducers/starredMessages.js b/app/reducers/starredMessages.js
new file mode 100644
index 000000000..8132a7d45
--- /dev/null
+++ b/app/reducers/starredMessages.js
@@ -0,0 +1,24 @@
+import { STARRED_MESSAGES } from '../actions/actionsTypes';
+
+const initialState = {
+	messages: []
+};
+
+export default function server(state = initialState, action) {
+	switch (action.type) {
+		case STARRED_MESSAGES.MESSAGE_RECEIVED:
+			return {
+				...state,
+				messages: [...state.messages, action.message]
+			};
+		case STARRED_MESSAGES.MESSAGE_UNSTARRED:
+			return {
+				...state,
+				messages: state.messages.filter(message => message._id !== action.messageId)
+			};
+		case STARRED_MESSAGES.CLOSE:
+			return initialState;
+		default:
+			return state;
+	}
+}
diff --git a/app/sagas/index.js b/app/sagas/index.js
index a27f59a54..2c24f7a84 100644
--- a/app/sagas/index.js
+++ b/app/sagas/index.js
@@ -9,6 +9,8 @@ import createChannel from './createChannel';
 import init from './init';
 import state from './state';
 import activeUsers from './activeUsers';
+import starredMessages from './starredMessages';
+import pinnedMessages from './pinnedMessages';
 
 const root = function* root() {
 	yield all([
@@ -21,7 +23,9 @@ const root = function* root() {
 		messages(),
 		selectServer(),
 		state(),
-		activeUsers()
+		activeUsers(),
+		starredMessages(),
+		pinnedMessages()
 	]);
 };
 
diff --git a/app/sagas/pinnedMessages.js b/app/sagas/pinnedMessages.js
new file mode 100644
index 000000000..95020a1a3
--- /dev/null
+++ b/app/sagas/pinnedMessages.js
@@ -0,0 +1,14 @@
+import { take, takeLatest } from 'redux-saga/effects';
+import * as types from '../actions/actionsTypes';
+import RocketChat from '../lib/rocketchat';
+
+const watchPinnedMessagesRoom = function* watchPinnedMessagesRoom({ rid }) {
+	const sub = yield RocketChat.subscribe('pinnedMessages', rid, 50);
+	yield take(types.PINNED_MESSAGES.CLOSE);
+	sub.unsubscribe().catch(e => alert(e));
+};
+
+const root = function* root() {
+	yield takeLatest(types.PINNED_MESSAGES.OPEN, watchPinnedMessagesRoom);
+};
+export default root;
diff --git a/app/sagas/starredMessages.js b/app/sagas/starredMessages.js
new file mode 100644
index 000000000..4d07d65ed
--- /dev/null
+++ b/app/sagas/starredMessages.js
@@ -0,0 +1,14 @@
+import { take, takeLatest } from 'redux-saga/effects';
+import * as types from '../actions/actionsTypes';
+import RocketChat from '../lib/rocketchat';
+
+const watchStarredMessagesRoom = function* watchStarredMessagesRoom({ rid }) {
+	const sub = yield RocketChat.subscribe('starredMessages', rid, 50);
+	yield take(types.STARRED_MESSAGES.CLOSE);
+	sub.unsubscribe().catch(e => alert(e));
+};
+
+const root = function* root() {
+	yield takeLatest(types.STARRED_MESSAGES.OPEN, watchStarredMessagesRoom);
+};
+export default root;
diff --git a/app/views/PinnedMessagesView/index.js b/app/views/PinnedMessagesView/index.js
new file mode 100644
index 000000000..d737b3af7
--- /dev/null
+++ b/app/views/PinnedMessagesView/index.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FlatList, Text, View } from 'react-native';
+import { connect } from 'react-redux';
+import ActionSheet from 'react-native-actionsheet';
+
+import { openPinnedMessages, closePinnedMessages } from '../../actions/pinnedMessages';
+import styles from './styles';
+import Message from '../../containers/message';
+import { togglePinRequest } from '../../actions/messages';
+
+const PIN_INDEX = 0;
+const CANCEL_INDEX = 1;
+const options = ['Unpin', 'Cancel'];
+
+@connect(
+	state => ({
+		messages: state.pinnedMessages.messages,
+		user: state.login.user,
+		baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
+	}),
+	dispatch => ({
+		openPinnedMessages: rid => dispatch(openPinnedMessages(rid)),
+		closePinnedMessages: () => dispatch(closePinnedMessages()),
+		togglePinRequest: message => dispatch(togglePinRequest(message))
+	})
+)
+export default class PinnedMessagesView extends React.PureComponent {
+	static propTypes = {
+		navigation: PropTypes.object,
+		messages: PropTypes.array,
+		user: PropTypes.object,
+		baseUrl: PropTypes.string,
+		openPinnedMessages: PropTypes.func,
+		closePinnedMessages: PropTypes.func,
+		togglePinRequest: PropTypes.func
+	}
+
+	constructor(props) {
+		super(props);
+		this.state = {
+			message: {}
+		};
+	}
+
+	componentWillMount() {
+		this.props.openPinnedMessages(this.props.navigation.state.params.rid);
+	}
+
+	componentWillUnmount() {
+		this.props.closePinnedMessages();
+	}
+
+	onLongPress = (message) => {
+		this.setState({ message });
+		this.actionSheet.show();
+	}
+
+	handleActionPress = (actionIndex) => {
+		switch (actionIndex) {
+			case PIN_INDEX:
+				this.props.togglePinRequest(this.state.message);
+				break;
+			default:
+				break;
+		}
+	}
+
+	renderEmpty = () => (
+		<View style={styles.listEmptyContainer}>
+			<Text>No pinned messages</Text>
+		</View>
+	)
+
+	renderItem = ({ item }) => (
+		<Message
+			item={item}
+			style={styles.message}
+			reactions={item.reactions}
+			user={this.props.user}
+			baseUrl={this.props.baseUrl}
+			Message_TimeFormat='MMMM Do YYYY, h:mm:ss a'
+			onLongPress={this.onLongPress}
+		/>
+	)
+
+	render() {
+		if (this.props.messages.length === 0) {
+			return this.renderEmpty();
+		}
+		return (
+			[
+				<FlatList
+					key='pinned-messages-view-list'
+					data={this.props.messages}
+					renderItem={this.renderItem}
+					style={styles.list}
+					keyExtractor={item => item._id}
+				/>,
+				<ActionSheet
+					key='pinned-messages-view-action-sheet'
+					ref={o => this.actionSheet = o}
+					title='Actions'
+					options={options}
+					cancelButtonIndex={CANCEL_INDEX}
+					onPress={this.handleActionPress}
+				/>
+			]
+		);
+	}
+}
diff --git a/app/views/PinnedMessagesView/styles.js b/app/views/PinnedMessagesView/styles.js
new file mode 100644
index 000000000..33a5e8d11
--- /dev/null
+++ b/app/views/PinnedMessagesView/styles.js
@@ -0,0 +1,17 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+	list: {
+		flex: 1,
+		backgroundColor: '#ffffff'
+	},
+	message: {
+		transform: [{ scaleY: 1 }]
+	},
+	listEmptyContainer: {
+		flex: 1,
+		alignItems: 'center',
+		justifyContent: 'center',
+		backgroundColor: '#ffffff'
+	}
+});
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
new file mode 100644
index 000000000..098bf3469
--- /dev/null
+++ b/app/views/RoomActionsView/index.js
@@ -0,0 +1,175 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { View, SectionList, Text, StyleSheet } from 'react-native';
+import Icon from 'react-native-vector-icons/Ionicons';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+import { connect } from 'react-redux';
+
+import styles from './styles';
+import Avatar from '../../containers/Avatar';
+import Touch from '../../utils/touch';
+import database from '../../lib/realm';
+
+@connect(state => ({
+	baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
+}))
+export default class RoomActionsView extends React.PureComponent {
+	static propTypes = {
+		baseUrl: PropTypes.string,
+		navigation: PropTypes.object
+	}
+
+	constructor(props) {
+		super(props);
+		const { rid } = props.navigation.state.params;
+		this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
+		this.state = {
+			sections: [],
+			room: {}
+		};
+	}
+
+	componentWillMount() {
+		this.updateRoom();
+		this.updateSections();
+	}
+
+	componentDidMount() {
+		this.rooms.addListener(this.updateRoom);
+	}
+
+	updateRoom = () => {
+		const [room] = this.rooms;
+		this.setState({ room });
+		this.props.navigation.setParams({
+			f: room.f
+		});
+		this.updateSections();
+	}
+
+	updateSections = () => {
+		const { rid, t } = this.state.room;
+		const sections = [{
+			data: [{ icon: 'ios-star', name: 'USER' }],
+			renderItem: this.renderRoomInfo
+		}, {
+			data: [
+				{ icon: 'ios-call-outline', name: 'Voice call' },
+				{ icon: 'ios-videocam-outline', name: 'Video call' }
+			],
+			renderItem: this.renderItem
+		}, {
+			data: [
+				{ icon: 'ios-attach', name: 'Files' },
+				{ icon: 'ios-at-outline', name: 'Mentions' },
+				{
+					icon: 'ios-star-outline',
+					name: 'Starred',
+					route: 'StarredMessages',
+					params: { rid }
+				},
+				{ icon: 'ios-search', name: 'Search' },
+				{ icon: 'ios-share-outline', name: 'Share' },
+				{
+					icon: 'ios-pin',
+					name: 'Pinned',
+					route: 'PinnedMessages',
+					params: { rid }
+				},
+				{ icon: 'ios-code', name: 'Snippets' },
+				{ icon: 'ios-notifications-outline', name: 'Notifications preferences' }
+			],
+			renderItem: this.renderItem
+		}];
+		if (t === 'd') {
+			sections.push({
+				data: [
+					{ icon: 'ios-volume-off', name: 'Mute user' },
+					{ icon: 'block', name: 'Block user', type: 'danger' }
+				],
+				renderItem: this.renderItem
+			});
+		} else if (t === 'c' || t === 'p') {
+			sections[2].data.unshift({ icon: 'ios-people', name: 'Members', description: '42 members' });
+			sections.push({
+				data: [
+					{ icon: 'ios-volume-off', name: 'Mute channel' },
+					{ icon: 'block', name: 'Leave channel', type: 'danger' }
+				],
+				renderItem: this.renderItem
+			});
+		}
+		this.setState({ sections });
+	}
+
+	renderRoomInfo = ({ item }) => this.renderTouchableItem([
+		<Avatar
+			key='avatar'
+			text={this.state.room.name}
+			size={50}
+			style={StyleSheet.flatten(styles.avatar)}
+			baseUrl={this.props.baseUrl}
+			type={this.state.room.t}
+		/>,
+		<View key='name' style={styles.roomTitleContainer}>
+			<Text style={styles.roomTitle}>{this.state.room.fname}</Text>
+			<Text style={styles.roomDescription}>@{this.state.room.name}</Text>
+		</View>,
+		<Icon key='icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#cbced1' />
+	], item)
+
+	renderTouchableItem = (subview, item) => (
+		<Touch
+			onPress={() => item.route && this.props.navigation.navigate(item.route, item.params)}
+			underlayColor='#FFFFFF'
+			activeOpacity={0.5}
+			accessibilityLabel={item.name}
+			accessibilityTraits='button'
+		>
+			<View style={styles.sectionItem}>
+				{subview}
+			</View>
+		</Touch>
+	)
+
+	renderItem = ({ item }) => {
+		if (item.type === 'danger') {
+			const subview = [
+				<MaterialIcon key='icon' name={item.icon} size={20} style={[styles.sectionItemIcon, styles.textColorDanger]} />,
+				<Text key='name' style={[styles.sectionItemName, styles.textColorDanger]}>{ item.name }</Text>
+			];
+			return this.renderTouchableItem(subview, item);
+		}
+		const subview = [
+			<Icon key='left-icon' name={item.icon} size={24} style={styles.sectionItemIcon} />,
+			<Text key='name' style={styles.sectionItemName}>{ item.name }</Text>,
+			item.description && <Text key='description' style={styles.sectionItemDescription}>{ item.description }</Text>,
+			<Icon key='right-icon' name='ios-arrow-forward' size={20} style={styles.sectionItemIcon} color='#cbced1' />
+		];
+		return this.renderTouchableItem(subview, item);
+	}
+
+	renderSectionSeparator = (data) => {
+		if (!data.trailingItem) {
+			if (!data.trailingSection) {
+				return <View style={styles.sectionSeparatorBorder} />;
+			}
+			return null;
+		}
+		return (
+			<View style={[styles.sectionSeparator, data.leadingSection && styles.sectionSeparatorBorder]} />
+		);
+	}
+
+	render() {
+		return (
+			<SectionList
+				style={styles.container}
+				stickySectionHeadersEnabled={false}
+				sections={this.state.sections}
+				SectionSeparatorComponent={this.renderSectionSeparator}
+				keyExtractor={(item, index) => index}
+			/>
+		);
+	}
+}
diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js
new file mode 100644
index 000000000..dba0c3b93
--- /dev/null
+++ b/app/views/RoomActionsView/styles.js
@@ -0,0 +1,54 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+	container: {
+		backgroundColor: '#F6F7F9'
+	},
+	headerButton: {
+		backgroundColor: 'transparent',
+		height: 44,
+		width: 44,
+		alignItems: 'center',
+		justifyContent: 'center'
+	},
+	sectionItem: {
+		backgroundColor: '#ffffff',
+		paddingVertical: 10,
+		flexDirection: 'row',
+		alignItems: 'center'
+	},
+	sectionItemIcon: {
+		width: 45,
+		textAlign: 'center'
+	},
+	sectionItemName: {
+		flex: 1
+	},
+	sectionItemDescription: {
+		color: '#cbced1'
+	},
+	sectionSeparator: {
+		height: 10,
+		backgroundColor: '#F6F7F9'
+	},
+	sectionSeparatorBorder: {
+		borderColor: '#EBEDF1',
+		borderTopWidth: 1
+	},
+	textColorDanger: {
+		color: '#f5455c'
+	},
+	avatar: {
+		marginHorizontal: 10
+	},
+	roomTitleContainer: {
+		flex: 1
+	},
+	roomTitle: {
+		fontSize: 16
+	},
+	roomDescription: {
+		fontSize: 12,
+		color: '#cbced1'
+	}
+});
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 1402712f4..adb8f8d76 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -5,11 +5,14 @@ import PropTypes from 'prop-types';
 import { connect } from 'react-redux';
 import { HeaderBackButton } from 'react-navigation';
 
+import RocketChat from '../../../lib/rocketchat';
 import realm from '../../../lib/realm';
 import Avatar from '../../../containers/Avatar';
 import { STATUS_COLORS } from '../../../constants/colors';
 import styles from './styles';
 import { closeRoom } from '../../../actions/room';
+import Touch from '../../../utils/touch';
+
 
 @connect(state => ({
 	user: state.login.user,
@@ -108,9 +111,25 @@ export default class RoomHeaderView extends React.PureComponent {
 
 	renderRight = () => (
 		<View style={styles.right}>
+			<Touch
+				onPress={() => RocketChat.toggleFavorite(this.room[0].rid, this.room[0].f)}
+				accessibilityLabel='Star room'
+				accessibilityTraits='button'
+				underlayColor='#FFFFFF'
+				activeOpacity={0.5}
+			>
+				<View style={styles.headerButton}>
+					<Icon
+						name={`${ Platform.OS === 'ios' ? 'ios' : 'md' }-star${ this.room[0].f ? '' : '-outline' }`}
+						color='#f6c502'
+						size={24}
+						backgroundColor='transparent'
+					/>
+				</View>
+			</Touch>
 			<TouchableOpacity
 				style={styles.headerButton}
-				onPress={() => {}}
+				onPress={() => this.props.navigation.navigate('RoomActions', { rid: this.room[0].rid })}
 				accessibilityLabel='Room actions'
 				accessibilityTraits='button'
 			>
diff --git a/app/views/RoomView/Header/styles.js b/app/views/RoomView/Header/styles.js
index 794dc83ea..c6439e2b4 100644
--- a/app/views/RoomView/Header/styles.js
+++ b/app/views/RoomView/Header/styles.js
@@ -42,7 +42,7 @@ export default StyleSheet.create({
 	headerButton: {
 		backgroundColor: 'transparent',
 		height: 44,
-		width: 44,
+		width: 40,
 		alignItems: 'center',
 		justifyContent: 'center'
 	}
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index ee0594d34..974112c8c 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -8,7 +8,7 @@ import equal from 'deep-equal';
 import { List } from './ListView';
 import * as actions from '../../actions';
 import { openRoom, setLastOpen } from '../../actions/room';
-import { editCancel, toggleReactionPicker } from '../../actions/messages';
+import { editCancel, toggleReactionPicker, actionsShow } from '../../actions/messages';
 import database from '../../lib/realm';
 import RocketChat from '../../lib/rocketchat';
 import Message from '../../containers/message';
@@ -35,7 +35,8 @@ import styles from './styles';
 		openRoom: room => dispatch(openRoom(room)),
 		editCancel: () => dispatch(editCancel()),
 		setLastOpen: date => dispatch(setLastOpen(date)),
-		toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
+		toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
+		actionsShow: actionMessage => dispatch(actionsShow(actionMessage))
 	})
 )
 export default class RoomView extends React.Component {
@@ -51,8 +52,9 @@ export default class RoomView extends React.Component {
 		Message_TimeFormat: PropTypes.string,
 		loading: PropTypes.bool,
 		actionMessage: PropTypes.object,
-		toggleReactionPicker: PropTypes.func.isRequired
-		// layoutAnimation: PropTypes.instanceOf(Date)
+		toggleReactionPicker: PropTypes.func.isRequired,
+		// layoutAnimation: PropTypes.instanceOf(Date),
+		actionsShow: PropTypes.func
 	};
 
 	static navigationOptions = ({ navigation }) => ({
@@ -124,6 +126,10 @@ export default class RoomView extends React.Component {
 		});
 	}
 
+	onMessageLongPress = (message) => {
+		this.props.actionsShow(message);
+	}
+
 	onReactionPress = (shortname, messageId) => {
 		if (!messageId) {
 			RocketChat.setReaction(shortname, this.props.actionMessage._id);
@@ -158,6 +164,7 @@ export default class RoomView extends React.Component {
 			Message_TimeFormat={this.props.Message_TimeFormat}
 			user={this.props.user}
 			onReactionPress={this.onReactionPress}
+			onLongPress={this.onMessageLongPress}
 		/>
 	);
 
diff --git a/app/views/StarredMessagesView/index.js b/app/views/StarredMessagesView/index.js
new file mode 100644
index 000000000..c1fd7d578
--- /dev/null
+++ b/app/views/StarredMessagesView/index.js
@@ -0,0 +1,111 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FlatList, Text, View } from 'react-native';
+import { connect } from 'react-redux';
+import ActionSheet from 'react-native-actionsheet';
+
+import { openStarredMessages, closeStarredMessages } from '../../actions/starredMessages';
+import styles from './styles';
+import Message from '../../containers/message';
+import { toggleStarRequest } from '../../actions/messages';
+
+const STAR_INDEX = 0;
+const CANCEL_INDEX = 1;
+const options = ['Unstar', 'Cancel'];
+
+@connect(
+	state => ({
+		messages: state.starredMessages.messages,
+		user: state.login.user,
+		baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
+	}),
+	dispatch => ({
+		openStarredMessages: rid => dispatch(openStarredMessages(rid)),
+		closeStarredMessages: () => dispatch(closeStarredMessages()),
+		toggleStarRequest: message => dispatch(toggleStarRequest(message))
+	})
+)
+export default class StarredMessagesView extends React.PureComponent {
+	static propTypes = {
+		navigation: PropTypes.object,
+		messages: PropTypes.array,
+		user: PropTypes.object,
+		baseUrl: PropTypes.string,
+		openStarredMessages: PropTypes.func,
+		closeStarredMessages: PropTypes.func,
+		toggleStarRequest: PropTypes.func
+	}
+
+	constructor(props) {
+		super(props);
+		this.state = {
+			message: {}
+		};
+	}
+
+	componentWillMount() {
+		this.props.openStarredMessages(this.props.navigation.state.params.rid);
+	}
+
+	componentWillUnmount() {
+		this.props.closeStarredMessages();
+	}
+
+	onLongPress = (message) => {
+		this.setState({ message });
+		this.actionSheet.show();
+	}
+
+	handleActionPress = (actionIndex) => {
+		switch (actionIndex) {
+			case STAR_INDEX:
+				this.props.toggleStarRequest(this.state.message);
+				break;
+			default:
+				break;
+		}
+	}
+
+	renderEmpty = () => (
+		<View style={styles.listEmptyContainer}>
+			<Text>No starred messages</Text>
+		</View>
+	)
+
+	renderItem = ({ item }) => (
+		<Message
+			item={item}
+			style={styles.message}
+			reactions={item.reactions}
+			user={this.props.user}
+			baseUrl={this.props.baseUrl}
+			Message_TimeFormat='MMMM Do YYYY, h:mm:ss a'
+			onLongPress={this.onLongPress}
+		/>
+	)
+
+	render() {
+		if (this.props.messages.length === 0) {
+			return this.renderEmpty();
+		}
+		return (
+			[
+				<FlatList
+					key='starred-messages-view-list'
+					data={this.props.messages}
+					renderItem={this.renderItem}
+					style={styles.list}
+					keyExtractor={item => item._id}
+				/>,
+				<ActionSheet
+					key='starred-messages-view-action-sheet'
+					ref={o => this.actionSheet = o}
+					title='Actions'
+					options={options}
+					cancelButtonIndex={CANCEL_INDEX}
+					onPress={this.handleActionPress}
+				/>
+			]
+		);
+	}
+}
diff --git a/app/views/StarredMessagesView/styles.js b/app/views/StarredMessagesView/styles.js
new file mode 100644
index 000000000..33a5e8d11
--- /dev/null
+++ b/app/views/StarredMessagesView/styles.js
@@ -0,0 +1,17 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+	list: {
+		flex: 1,
+		backgroundColor: '#ffffff'
+	},
+	message: {
+		transform: [{ scaleY: 1 }]
+	},
+	listEmptyContainer: {
+		flex: 1,
+		alignItems: 'center',
+		justifyContent: 'center',
+		backgroundColor: '#ffffff'
+	}
+});
-- 
GitLab