diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 2f1f95a3..f4daf9a8 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -39,6 +39,7 @@ export const ROOM = createRequestTypes('ROOM', [
'OPEN',
'CLOSE',
'LEAVE',
+ 'ERASE',
'USER_TYPING',
'MESSAGE_RECEIVED',
'SET_LAST_OPEN',
@@ -93,7 +94,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 ACTIVE_USERS = createRequestTypes('ACTIVE_USERS', ['SET']);
+export const ROLES = createRequestTypes('ROLES', ['SET']);
export const STARRED_MESSAGES = createRequestTypes('STARRED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNSTARRED']);
export const PINNED_MESSAGES = createRequestTypes('PINNED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED', 'MESSAGE_UNPINNED']);
export const MENTIONED_MESSAGES = createRequestTypes('MENTIONED_MESSAGES', ['OPEN', 'CLOSE', 'MESSAGES_RECEIVED']);
diff --git a/app/actions/activeUsers.js b/app/actions/activeUsers.js
index a273d734..1e7c5ecb 100644
--- a/app/actions/activeUsers.js
+++ b/app/actions/activeUsers.js
@@ -1,12 +1,5 @@
import * as types from './actionsTypes';
-export function requestActiveUser(users) {
- return {
- type: types.ACTIVE_USERS.REQUEST,
- users
- };
-}
-
export function setActiveUser(data) {
return {
type: types.ACTIVE_USERS.SET,
diff --git a/app/actions/roles.js b/app/actions/roles.js
new file mode 100644
index 00000000..07411198
--- /dev/null
+++ b/app/actions/roles.js
@@ -0,0 +1,8 @@
+import * as types from './actionsTypes';
+
+export function setRoles(data) {
+ return {
+ type: types.ROLES.SET,
+ data
+ };
+}
diff --git a/app/actions/room.js b/app/actions/room.js
index 533fb4ab..ef6430c3 100644
--- a/app/actions/room.js
+++ b/app/actions/room.js
@@ -42,6 +42,13 @@ export function leaveRoom(rid) {
};
}
+export function eraseRoom(rid) {
+ return {
+ type: types.ROOM.ERASE,
+ rid
+ };
+}
+
export function userTyping(status = true) {
return {
type: types.ROOM.USER_TYPING,
diff --git a/app/constants/colors.js b/app/constants/colors.js
index 276ceda2..5caf08c5 100644
--- a/app/constants/colors.js
+++ b/app/constants/colors.js
@@ -1,8 +1,9 @@
export const AVATAR_COLORS = ['#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFC107', '#FF9800', '#FF5722', '#795548', '#9E9E9E', '#607D8B'];
export const ESLINT_FIX = null;
+export const COLOR_DANGER = '#f5455c';
export const STATUS_COLORS = {
online: '#2de0a5',
- busy: '#f5455c',
+ busy: COLOR_DANGER,
away: '#ffd21f',
offline: '#cbced1'
};
diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js
index 6a2de4b4..0a82d50f 100644
--- a/app/containers/MessageActions.js
+++ b/app/containers/MessageActions.js
@@ -17,8 +17,8 @@ import {
toggleReactionPicker
} from '../actions/messages';
import { showToast } from '../utils/info';
+import RocketChat from '../lib/rocketchat';
-const returnAnArray = obj => obj || [];
@connect(
state => ({
showActions: state.messages.showActions,
@@ -79,10 +79,6 @@ export default class MessageActions extends React.Component {
};
this.handleActionPress = this.handleActionPress.bind(this);
this.options = [''];
- const { roles } = this.props.room;
- const roomRoles = Array.from(Object.keys(roles), i => roles[i].value);
- const userRoles = this.props.user.roles || [];
- this.mergedRoles = [...new Set([...roomRoles, ...userRoles])];
this.setPermissions(this.props.permissions);
}
@@ -127,7 +123,7 @@ export default class MessageActions extends React.Component {
this.PIN_INDEX = this.options.length - 1;
}
// Reaction
- if (!this.isRoomReadOnly()) {
+ if (!this.isRoomReadOnly() || this.canReactWhenReadOnly()) {
this.options.push('Add Reaction');
this.REACTION_INDEX = this.options.length - 1;
}
@@ -171,19 +167,20 @@ export default class MessageActions extends React.Component {
this.setPermissions(this.props.permissions);
}
- setPermissions(permissions) {
- this.hasEditPermission = returnAnArray(permissions['edit-message'])
- .some(item => this.mergedRoles.indexOf(item) !== -1);
- this.hasDeletePermission = returnAnArray(permissions['delete-message'])
- .some(item => this.mergedRoles.indexOf(item) !== -1);
- this.hasForceDeletePermission = returnAnArray(permissions['force-delete-message'])
- .some(item => this.mergedRoles.indexOf(item) !== -1);
+ setPermissions() {
+ const permissions = ['edit-message', 'delete-message', 'force-delete-message'];
+ const result = RocketChat.hasPermission(permissions, this.props.room.rid);
+ this.hasEditPermission = result[permissions[0]];
+ this.hasDeletePermission = result[permissions[1]];
+ this.hasForceDeletePermission = result[permissions[2]];
}
isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id;
isRoomReadOnly = () => this.props.room.ro;
+ canReactWhenReadOnly = () => this.props.room.reactWhenReadOnly;
+
allowEdit = (props) => {
if (this.isRoomReadOnly()) {
return false;
diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js
index 313e7740..1a320fe4 100644
--- a/app/containers/MessageBox/index.js
+++ b/app/containers/MessageBox/index.js
@@ -489,6 +489,7 @@ export default class MessageBox extends React.PureComponent {
ref={component => this.component = component}
style={styles.textBoxInput}
returnKeyType='default'
+ keyboardType='twitter'
blurOnSubmit={false}
placeholder='New Message'
onChangeText={text => this.onChangeText(text)}
diff --git a/app/containers/TextInput.js b/app/containers/TextInput.js
new file mode 100644
index 00000000..73f2ccab
--- /dev/null
+++ b/app/containers/TextInput.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { View, StyleSheet, Text, TextInput } from 'react-native';
+import PropTypes from 'prop-types';
+
+import sharedStyles from '../views/Styles';
+import { COLOR_DANGER } from '../constants/colors';
+
+const styles = StyleSheet.create({
+ inputContainer: {
+ marginBottom: 20
+ },
+ label: {
+ marginBottom: 4,
+ fontSize: 16
+ },
+ input: {
+ paddingTop: 12,
+ paddingBottom: 12,
+ paddingHorizontal: 10,
+ borderWidth: 2,
+ borderRadius: 2,
+ backgroundColor: 'white',
+ borderColor: 'rgba(0,0,0,.15)',
+ color: 'black'
+ },
+ labelError: {
+ color: COLOR_DANGER
+ },
+ inputError: {
+ color: COLOR_DANGER,
+ borderColor: COLOR_DANGER
+ }
+});
+
+export default class RCTextInput extends React.PureComponent {
+ static propTypes = {
+ label: PropTypes.string,
+ value: PropTypes.string,
+ error: PropTypes.object,
+ inputProps: PropTypes.object,
+ inputRef: PropTypes.func,
+ onChangeText: PropTypes.func,
+ onSubmitEditing: PropTypes.func
+ }
+
+ static defaultProps = {
+ label: 'Label',
+ error: {}
+ }
+
+ render() {
+ const {
+ label, value, error, inputRef, onChangeText, onSubmitEditing, inputProps
+ } = this.props;
+ return (
+
+
+ {label}
+
+
+ {error.error && {error.reason}}
+
+ );
+ }
+}
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index ab3e9a3f..babf15ce 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -46,12 +46,14 @@ export default class Message extends React.Component {
onReactionPress: PropTypes.func,
style: ViewPropTypes.style,
onLongPress: PropTypes.func,
- _updatedAt: PropTypes.instanceOf(Date)
+ _updatedAt: PropTypes.instanceOf(Date),
+ archived: PropTypes.bool
}
static defaultProps = {
onLongPress: () => {},
- _updatedAt: new Date()
+ _updatedAt: new Date(),
+ archived: false
}
constructor(props) {
@@ -121,6 +123,14 @@ export default class Message extends React.Component {
message = `${ msg } was set ${ role } by ${ u.username }`;
} else if (t === 'subscription-role-removed') {
message = `${ msg } is no longer ${ role } by ${ u.username }`;
+ } else if (t === 'room_changed_description') {
+ message = `Room description changed to: ${ msg } by ${ u.username }`;
+ } else if (t === 'room_changed_announcement') {
+ message = `Room announcement changed to: ${ msg } by ${ u.username }`;
+ } else if (t === 'room_changed_topic') {
+ message = `Room topic changed to: ${ msg } by ${ u.username }`;
+ } else if (t === 'room_changed_privacy') {
+ message = `Room type changed to: ${ msg } by ${ u.username }`;
}
return message;
@@ -130,7 +140,21 @@ export default class Message extends React.Component {
isInfoMessage() {
return [
- 'r', 'au', 'ru', 'ul', 'uj', 'rm', 'user-muted', 'user-unmuted', 'message_pinned', 'subscription-role-added', 'subscription-role-removed'
+ 'r',
+ 'au',
+ 'ru',
+ 'ul',
+ 'uj',
+ 'rm',
+ 'user-muted',
+ 'user-unmuted',
+ 'message_pinned',
+ 'subscription-role-added',
+ 'subscription-role-removed',
+ 'room_changed_description',
+ 'room_changed_announcement',
+ 'room_changed_topic',
+ 'room_changed_privacy'
].includes(this.props.item.t);
}
@@ -236,7 +260,7 @@ export default class Message extends React.Component {
render() {
const {
- item, message, editing, baseUrl, customEmojis, style
+ item, message, editing, baseUrl, customEmojis, style, archived
} = this.props;
const username = item.alias || item.u.username;
const isEditing = message._id === item._id && editing;
@@ -246,7 +270,7 @@ export default class Message extends React.Component {
this.onPress()}
onLongPress={() => this.onLongPress()}
- disabled={this.isDeleted() || this.hasError()}
+ disabled={this.isDeleted() || this.hasError() || archived}
underlayColor='#FFFFFF'
activeOpacity={0.3}
style={[styles.message, isEditing ? styles.editing : null, style]}
diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js
index 9dd5cab4..e0d8c0d2 100644
--- a/app/containers/routes/AuthRoutes.js
+++ b/app/containers/routes/AuthRoutes.js
@@ -14,6 +14,8 @@ import MentionedMessagesView from '../../views/MentionedMessagesView';
import SnippetedMessagesView from '../../views/SnippetedMessagesView';
import RoomFilesView from '../../views/RoomFilesView';
import RoomMembersView from '../../views/RoomMembersView';
+import RoomInfoView from '../../views/RoomInfoView';
+import RoomInfoEditView from '../../views/RoomInfoEditView';
const AuthRoutes = StackNavigator(
{
@@ -89,6 +91,20 @@ const AuthRoutes = StackNavigator(
title: 'Room Members',
headerTintColor: '#292E35'
}
+ },
+ RoomInfo: {
+ screen: RoomInfoView,
+ navigationOptions: {
+ title: 'Room Info',
+ headerTintColor: '#292E35'
+ }
+ },
+ RoomInfoEdit: {
+ screen: RoomInfoEditView,
+ navigationOptions: {
+ title: 'Room Info Edit',
+ headerTintColor: '#292E35'
+ }
}
},
{
diff --git a/app/containers/status.js b/app/containers/status.js
index 55a78fcc..8a434d0c 100644
--- a/app/containers/status.js
+++ b/app/containers/status.js
@@ -25,12 +25,12 @@ export default class Status extends React.Component {
shouldComponentUpdate(nextProps) {
const userId = this.props.id;
- return this.status !== nextProps.activeUsers[userId];
+ return (nextProps.activeUsers[userId] && nextProps.activeUsers[userId].status) !== this.status;
}
get status() {
const userId = this.props.id;
- return (this.props.activeUsers && this.props.activeUsers[userId]) || 'offline';
+ return (this.props.activeUsers && this.props.activeUsers[userId] && this.props.activeUsers[userId].status) || 'offline';
}
render() {
diff --git a/app/lib/realm.js b/app/lib/realm.js
index ec0fa382..8ad8d5db 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -80,8 +80,6 @@ const subscriptionSchema = {
roles: { type: 'list', objectType: 'subscriptionRolesSchema' },
unread: { type: 'int', optional: true },
userMentions: { type: 'int', optional: true },
- // userMentions: 0,
- // groupMentions: 0,
roomUpdatedAt: { type: 'date', optional: true },
ro: { type: 'bool', optional: true },
lastOpen: { type: 'date', optional: true },
@@ -89,7 +87,10 @@ const subscriptionSchema = {
description: { type: 'string', optional: true },
announcement: { type: 'string', optional: true },
topic: { type: 'string', optional: true },
- blocked: { type: 'bool', optional: true }
+ blocked: { type: 'bool', optional: true },
+ reactWhenReadOnly: { type: 'bool', optional: true },
+ archived: { type: 'bool', optional: true },
+ joinCodeRequired: { type: 'bool', optional: true }
}
};
@@ -237,6 +238,15 @@ const customEmojisSchema = {
}
};
+const rolesSchema = {
+ name: 'roles',
+ primaryKey: '_id',
+ properties: {
+ _id: 'string',
+ description: { type: 'string', optional: true }
+ }
+};
+
const schema = [
settingsSchema,
subscriptionSchema,
@@ -254,7 +264,8 @@ const schema = [
customEmojiAliasesSchema,
customEmojisSchema,
messagesReactionsSchema,
- messagesReactionsUsernamesSchema
+ messagesReactionsUsernamesSchema,
+ rolesSchema
];
class DB {
databases = {
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 1ea5c6d6..97b00d3b 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -12,12 +12,13 @@ import * as actions from '../actions';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setUser, setLoginServices, removeLoginServices } from '../actions/login';
import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect';
-import { requestActiveUser } from '../actions/activeUsers';
+import { setActiveUser } from '../actions/activeUsers';
import { starredMessagesReceived, starredMessageUnstarred } from '../actions/starredMessages';
import { pinnedMessagesReceived, pinnedMessageUnpinned } from '../actions/pinnedMessages';
import { mentionedMessagesReceived } from '../actions/mentionedMessages';
import { snippetedMessagesReceived } from '../actions/snippetedMessages';
import { roomFilesReceived } from '../actions/roomFiles';
+import { setRoles } from '../actions/roles';
import Ddp from './ddp';
export { Accounts } from 'react-native-meteor';
@@ -26,6 +27,7 @@ const call = (method, ...params) => RocketChat.ddp.call(method, ...params); // e
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const SERVER_TIMEOUT = 30000;
+const returnAnArray = obj => obj || [];
const normalizeMessage = (lastMessage) => {
if (lastMessage) {
@@ -91,13 +93,12 @@ const RocketChat = {
this._setUserTimer = null;
}
-
this._setUserTimer = setTimeout(() => {
- reduxStore.dispatch(requestActiveUser(this.activeUsers));
+ reduxStore.dispatch(setActiveUser(this.activeUsers));
this._setUserTimer = null;
return this.activeUsers = {};
- }, 5000);
- this.activeUsers[ddpMessage.id] = status;
+ }, 3000);
+ this.activeUsers[ddpMessage.id] = ddpMessage.fields;
},
reconnect() {
if (this.ddp) {
@@ -124,6 +125,8 @@ const RocketChat = {
RocketChat.getSettings();
RocketChat.getPermissions();
RocketChat.getCustomEmoji();
+ this.ddp.subscribe('activeUsers');
+ this.ddp.subscribe('roles');
});
this.ddp.on('error', (err) => {
@@ -131,8 +134,6 @@ const RocketChat = {
reduxStore.dispatch(connectFailure());
});
- this.ddp.on('connected', () => this.ddp.subscribe('activeUsers', null, false));
-
this.ddp.on('users', ddpMessage => RocketChat._setUser(ddpMessage));
this.ddp.on('stream-room-messages', (ddpMessage) => {
@@ -171,6 +172,12 @@ const RocketChat = {
sub.roomUpdatedAt = data._updatedAt;
sub.lastMessage = normalizeMessage(data.lastMessage);
sub.ro = data.ro;
+ sub.description = data.description;
+ sub.topic = data.topic;
+ sub.announcement = data.announcement;
+ sub.reactWhenReadOnly = data.reactWhenReadOnly;
+ sub.archived = data.archived;
+ sub.joinCodeRequired = data.joinCodeRequired;
});
}
});
@@ -334,6 +341,28 @@ const RocketChat = {
this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000);
}
});
+
+ this.ddp.on('rocketchat_roles', (ddpMessage) => {
+ this.roles = this.roles || {};
+
+ if (this.roleTimer) {
+ clearTimeout(this.roleTimer);
+ this.roleTimer = null;
+ }
+ this.roleTimer = setTimeout(() => {
+ reduxStore.dispatch(setRoles(this.roles));
+
+ database.write(() => {
+ _.forEach(this.roles, (description, _id) => {
+ database.create('roles', { _id, description }, true);
+ });
+ });
+
+ this.roleTimer = null;
+ return this.roles = {};
+ }, 5000);
+ this.roles[ddpMessage.id] = ddpMessage.fields.description;
+ });
}).catch(console.log);
},
@@ -649,6 +678,9 @@ const RocketChat = {
subscription.description = room.description;
subscription.topic = room.topic;
subscription.announcement = room.announcement;
+ subscription.reactWhenReadOnly = room.reactWhenReadOnly;
+ subscription.archived = room.archived;
+ subscription.joinCodeRequired = room.joinCodeRequired;
}
if (subscription.roles) {
subscription.roles = subscription.roles.map(role => ({ value: role }));
@@ -823,6 +855,17 @@ const RocketChat = {
getRoomMembers(rid, allUsers) {
return call('getUsersOfRoom', rid, allUsers);
},
+ getUserRoles() {
+ return call('getUserRoles');
+ },
+ async getRoomMember(rid, currentUserId) {
+ try {
+ const membersResult = await RocketChat.getRoomMembers(rid, true);
+ return Promise.resolve(membersResult.records.find(m => m.id !== currentUserId));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ },
toggleBlockUser(rid, blocked, block) {
if (block) {
return call('blockUser', { rid, blocked });
@@ -831,6 +874,40 @@ const RocketChat = {
},
leaveRoom(rid) {
return call('leaveRoom', rid);
+ },
+ eraseRoom(rid) {
+ return call('eraseRoom', rid);
+ },
+ toggleArchiveRoom(rid, archive) {
+ if (archive) {
+ return call('archiveRoom', rid);
+ }
+ return call('unarchiveRoom', rid);
+ },
+ saveRoomSettings(rid, params) {
+ return call('saveRoomSettings', rid, params);
+ },
+ hasPermission(permissions, rid) {
+ // get the room from realm
+ const room = database.objects('subscriptions').filtered('rid = $0', rid)[0];
+ // 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);
+ return result;
+ }, {});
}
};
diff --git a/app/reducers/index.js b/app/reducers/index.js
index e7603c54..3a4c0dac 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -12,6 +12,7 @@ import app from './app';
import permissions from './permissions';
import customEmojis from './customEmojis';
import activeUsers from './activeUsers';
+import roles from './roles';
import starredMessages from './starredMessages';
import pinnedMessages from './pinnedMessages';
import mentionedMessages from './mentionedMessages';
@@ -32,6 +33,7 @@ export default combineReducers({
permissions,
customEmojis,
activeUsers,
+ roles,
starredMessages,
pinnedMessages,
mentionedMessages,
diff --git a/app/reducers/roles.js b/app/reducers/roles.js
new file mode 100644
index 00000000..f0e964e6
--- /dev/null
+++ b/app/reducers/roles.js
@@ -0,0 +1,15 @@
+import * as types from '../actions/actionsTypes';
+
+const initialState = {};
+
+export default (state = initialState, action) => {
+ switch (action.type) {
+ case types.ROLES.SET:
+ return {
+ ...state,
+ ...action.data
+ };
+ default:
+ return state;
+ }
+};
diff --git a/app/sagas/activeUsers.js b/app/sagas/activeUsers.js
deleted file mode 100644
index 70ca903e..00000000
--- a/app/sagas/activeUsers.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { put, takeLatest } from 'redux-saga/effects';
-import * as types from '../actions/actionsTypes';
-
-import { setActiveUser } from '../actions/activeUsers';
-
-const watchActiveUsers = function* handleInput({ users }) {
- yield put(setActiveUser(users));
-};
-
-const root = function* root() {
- yield takeLatest(types.ACTIVE_USERS.REQUEST, watchActiveUsers);
-};
-export default root;
diff --git a/app/sagas/index.js b/app/sagas/index.js
index 39aa6836..ab82fdef 100644
--- a/app/sagas/index.js
+++ b/app/sagas/index.js
@@ -8,7 +8,6 @@ import selectServer from './selectServer';
import createChannel from './createChannel';
import init from './init';
import state from './state';
-import activeUsers from './activeUsers';
import starredMessages from './starredMessages';
import pinnedMessages from './pinnedMessages';
import mentionedMessages from './mentionedMessages';
@@ -26,7 +25,6 @@ const root = function* root() {
messages(),
selectServer(),
state(),
- activeUsers(),
starredMessages(),
pinnedMessages(),
mentionedMessages(),
diff --git a/app/sagas/init.js b/app/sagas/init.js
index 15299112..423e91a9 100644
--- a/app/sagas/init.js
+++ b/app/sagas/init.js
@@ -4,6 +4,7 @@ import * as actions from '../actions';
import { setServer } from '../actions/server';
import { restoreToken } from '../actions/login';
import { APP } from '../actions/actionsTypes';
+import { setRoles } from '../actions/roles';
import database from '../lib/realm';
import RocketChat from '../lib/rocketchat';
@@ -23,6 +24,11 @@ const restore = function* restore() {
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');
+ yield put(setRoles(roles.reduce((result, role) => {
+ result[role._id] = role.description;
+ return result;
+ }, {})));
}
yield put(actions.appReady({}));
} catch (e) {
diff --git a/app/sagas/rooms.js b/app/sagas/rooms.js
index a3017655..ded0d4e6 100644
--- a/app/sagas/rooms.js
+++ b/app/sagas/rooms.js
@@ -11,6 +11,7 @@ import database from '../lib/realm';
import * as NavigationService from '../containers/routes/NavigationService';
const leaveRoom = rid => RocketChat.leaveRoom(rid);
+const eraseRoom = rid => RocketChat.eraseRoom(rid);
const getRooms = function* getRooms() {
return yield RocketChat.getRooms();
@@ -121,26 +122,39 @@ const updateLastOpen = function* updateLastOpen() {
yield put(setLastOpen());
};
+const goRoomsListAndDelete = function* goRoomsListAndDelete(rid) {
+ NavigationService.goRoomsList();
+ yield delay(1000);
+ database.write(() => {
+ const messages = database.objects('messages').filtered('rid = $0', rid);
+ database.delete(messages);
+ const subscription = database.objects('subscriptions').filtered('rid = $0', rid);
+ database.delete(subscription);
+ });
+};
+
const handleLeaveRoom = function* handleLeaveRoom({ rid }) {
try {
yield call(leaveRoom, rid);
- NavigationService.goRoomsList();
- yield delay(1000);
- database.write(() => {
- const messages = database.objects('messages').filtered('rid = $0', rid);
- database.delete(messages);
- const subscription = database.objects('subscriptions').filtered('rid = $0', rid);
- database.delete(subscription);
- });
+ yield goRoomsListAndDelete(rid);
} catch (e) {
if (e.error === 'error-you-are-last-owner') {
Alert.alert('You are the last owner. Please set new owner before leaving the room.');
} else {
- Alert.alert(e);
+ Alert.alert('Something happened when leaving room!');
}
}
};
+const handleEraseRoom = function* handleEraseRoom({ rid }) {
+ try {
+ yield call(eraseRoom, rid);
+ yield goRoomsListAndDelete(rid);
+ } catch (e) {
+ Alert.alert('Something happened when erasing room!');
+ }
+};
+
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeLatest(types.LOGIN.SUCCESS, watchRoomsRequest);
@@ -150,5 +164,6 @@ const root = function* root() {
yield takeLatest(FOREGROUND, watchRoomsRequest);
yield takeLatest(BACKGROUND, updateLastOpen);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
+ yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
};
export default root;
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 7eb6ea58..0b2632e4 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -6,7 +6,9 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import { connect } from 'react-redux';
import styles from './styles';
+import sharedStyles from '../Styles';
import Avatar from '../../containers/Avatar';
+import Status from '../../containers/status';
import Touch from '../../utils/touch';
import database from '../../lib/realm';
import RocketChat from '../../lib/rocketchat';
@@ -33,13 +35,15 @@ export default class RoomActionsView extends React.PureComponent {
this.state = {
sections: [],
room: {},
- members: []
+ members: [],
+ member: {}
};
}
async componentDidMount() {
await this.updateRoom();
this.updateRoomMembers();
+ this.updateRoomMember();
this.rooms.addListener(this.updateRoom);
}
@@ -59,7 +63,7 @@ export default class RoomActionsView extends React.PureComponent {
getRoomTitle = room => (room.t === 'd' ? room.fname : room.name);
updateRoomMembers = async() => {
- let members;
+ let members = [];
try {
const membersResult = await RocketChat.getRoomMembers(this.state.room.rid, false);
members = membersResult.records;
@@ -70,6 +74,17 @@ export default class RoomActionsView extends React.PureComponent {
this.updateSections();
}
+ updateRoomMember = async() => {
+ if (this.state.room.t === 'd') {
+ try {
+ const member = await RocketChat.getRoomMember(this.state.room.rid, this.props.user.id);
+ this.setState({ member });
+ } catch (error) {
+ console.warn(error);
+ }
+ }
+ }
+
updateRoom = async() => {
const [room] = this.rooms;
await this.setState({ room });
@@ -80,12 +95,17 @@ export default class RoomActionsView extends React.PureComponent {
const { rid, t, blocked } = this.state.room;
const { members } = this.state;
const sections = [{
- data: [{ icon: 'ios-star', name: 'USER' }],
+ data: [{
+ icon: 'ios-star',
+ name: 'USER',
+ route: 'RoomInfo',
+ params: { rid }
+ }],
renderItem: this.renderRoomInfo
}, {
data: [
- { icon: 'ios-call-outline', name: 'Voice call' },
- { icon: 'ios-videocam-outline', name: 'Video call' }
+ { icon: 'ios-call-outline', name: 'Voice call', disabled: true },
+ { icon: 'ios-videocam-outline', name: 'Video call', disabled: true }
],
renderItem: this.renderItem
}, {
@@ -108,8 +128,8 @@ export default class RoomActionsView extends React.PureComponent {
route: 'StarredMessages',
params: { rid }
},
- { icon: 'ios-search', name: 'Search' },
- { icon: 'ios-share-outline', name: 'Share' },
+ { icon: 'ios-search', name: 'Search', disabled: true },
+ { icon: 'ios-share-outline', name: 'Share', disabled: true },
{
icon: 'ios-pin',
name: 'Pinned',
@@ -122,14 +142,14 @@ export default class RoomActionsView extends React.PureComponent {
route: 'SnippetedMessages',
params: { rid }
},
- { icon: 'ios-notifications-outline', name: 'Notifications preferences' }
+ { icon: 'ios-notifications-outline', name: 'Notifications preferences', disabled: true }
],
renderItem: this.renderItem
}];
if (t === 'd') {
sections.push({
data: [
- { icon: 'ios-volume-off', name: 'Mute user' },
+ { icon: 'ios-volume-off', name: 'Mute user', disabled: true },
{
icon: 'block',
name: `${ blocked ? 'Unblock' : 'Block' } user`,
@@ -151,7 +171,7 @@ export default class RoomActionsView extends React.PureComponent {
}
sections.push({
data: [
- { icon: 'ios-volume-off', name: 'Mute channel' },
+ { icon: 'ios-volume-off', name: 'Mute channel', disabled: true },
{
icon: 'block',
name: 'Leave channel',
@@ -167,8 +187,7 @@ export default class RoomActionsView extends React.PureComponent {
toggleBlockUser = () => {
const { rid, blocked } = this.state.room;
- const { members } = this.state;
- const member = members.find(m => m.id !== this.props.user.id);
+ const { member } = this.state;
RocketChat.toggleBlockUser(rid, member._id, !blocked);
}
@@ -185,16 +204,14 @@ export default class RoomActionsView extends React.PureComponent {
{
text: 'Yes, leave it!',
style: 'destructive',
- onPress: async() => {
- this.props.leaveRoom(room.rid);
- }
+ onPress: () => this.props.leaveRoom(room.rid)
}
]
);
}
renderRoomInfo = ({ item }) => {
- const { room } = this.state;
+ const { room, member } = this.state;
const { name, t, topic } = room;
return (
this.renderTouchableItem([
@@ -205,12 +222,14 @@ export default class RoomActionsView extends React.PureComponent {
style={styles.avatar}
baseUrl={this.props.baseUrl}
type={t}
- />,
+ >
+ {t === 'd' ? : null }
+ ,
{ this.getRoomTitle(room) }
{t === 'd' ? `@${ name }` : topic}
,
-
+
], item)
);
}
@@ -223,7 +242,7 @@ export default class RoomActionsView extends React.PureComponent {
accessibilityLabel={item.name}
accessibilityTraits='button'
>
-
+
{subview}
@@ -241,11 +260,13 @@ export default class RoomActionsView extends React.PureComponent {
,
{ item.name },
item.description && { item.description },
-
+
];
return this.renderTouchableItem(subview, item);
}
+ renderSeparator = () => ;
+
renderSectionSeparator = (data) => {
if (!data.trailingItem) {
if (!data.trailingSection) {
@@ -265,6 +286,7 @@ export default class RoomActionsView extends React.PureComponent {
stickySectionHeadersEnabled={false}
sections={this.state.sections}
SectionSeparatorComponent={this.renderSectionSeparator}
+ ItemSeparatorComponent={this.renderSeparator}
keyExtractor={(item, index) => index}
/>
);
diff --git a/app/views/RoomActionsView/styles.js b/app/views/RoomActionsView/styles.js
index dba0c3b9..175c04bd 100644
--- a/app/views/RoomActionsView/styles.js
+++ b/app/views/RoomActionsView/styles.js
@@ -13,10 +13,13 @@ export default StyleSheet.create({
},
sectionItem: {
backgroundColor: '#ffffff',
- paddingVertical: 10,
+ paddingVertical: 16,
flexDirection: 'row',
alignItems: 'center'
},
+ sectionItemDisabled: {
+ opacity: 0.3
+ },
sectionItemIcon: {
width: 45,
textAlign: 'center'
@@ -25,7 +28,11 @@ export default StyleSheet.create({
flex: 1
},
sectionItemDescription: {
- color: '#cbced1'
+ color: '#ccc'
+ },
+ separator: {
+ height: StyleSheet.hairlineWidth,
+ backgroundColor: '#ddd'
},
sectionSeparator: {
height: 10,
@@ -49,6 +56,6 @@ export default StyleSheet.create({
},
roomDescription: {
fontSize: 12,
- color: '#cbced1'
+ color: '#ccc'
}
});
diff --git a/app/views/RoomInfoEditView/SwitchContainer.js b/app/views/RoomInfoEditView/SwitchContainer.js
new file mode 100644
index 00000000..7cb85b8a
--- /dev/null
+++ b/app/views/RoomInfoEditView/SwitchContainer.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { View, Text, Switch } from 'react-native';
+import PropTypes from 'prop-types';
+
+import styles from './styles';
+import sharedStyles from '../../views/Styles';
+
+export default class SwitchContainer extends React.PureComponent {
+ static propTypes = {
+ value: PropTypes.bool,
+ disabled: PropTypes.bool,
+ leftLabelPrimary: PropTypes.string,
+ leftLabelSecondary: PropTypes.string,
+ rightLabelPrimary: PropTypes.string,
+ rightLabelSecondary: PropTypes.string,
+ onValueChange: PropTypes.func
+ }
+
+ render() {
+ const {
+ value, disabled, onValueChange, leftLabelPrimary, leftLabelSecondary, rightLabelPrimary, rightLabelSecondary
+ } = this.props;
+ return (
+ [
+
+
+ {leftLabelPrimary}
+ {leftLabelSecondary}
+
+
+
+ {rightLabelPrimary}
+ {rightLabelSecondary}
+
+ ,
+
+ ]
+ );
+ }
+}
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
new file mode 100644
index 00000000..3b8834f0
--- /dev/null
+++ b/app/views/RoomInfoEditView/index.js
@@ -0,0 +1,381 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Text, View, ScrollView, TouchableOpacity, SafeAreaView, Keyboard, Alert } from 'react-native';
+import Spinner from 'react-native-loading-spinner-overlay';
+import { connect } from 'react-redux';
+
+import KeyboardView from '../../presentation/KeyboardView';
+import sharedStyles from '../Styles';
+import styles from './styles';
+import scrollPersistTaps from '../../utils/scrollPersistTaps';
+import { showErrorAlert, showToast } from '../../utils/info';
+import database from '../../lib/realm';
+import RocketChat from '../../lib/rocketchat';
+import { eraseRoom } from '../../actions/room';
+import RCTextInput from '../../containers/TextInput';
+import SwitchContainer from './SwitchContainer';
+import random from '../../utils/random';
+
+const PERMISSION_SET_READONLY = 'set-readonly';
+const PERMISSION_SET_REACT_WHEN_READONLY = 'set-react-when-readonly';
+const PERMISSION_ARCHIVE = 'archive-room';
+const PERMISSION_UNARCHIVE = 'unarchive-room';
+const PERMISSION_DELETE_C = 'delete-c';
+const PERMISSION_DELETE_P = 'delete-p';
+const PERMISSIONS_ARRAY = [
+ PERMISSION_SET_READONLY,
+ PERMISSION_SET_REACT_WHEN_READONLY,
+ PERMISSION_ARCHIVE,
+ PERMISSION_UNARCHIVE,
+ PERMISSION_DELETE_C,
+ PERMISSION_DELETE_P
+];
+
+@connect(null, dispatch => ({
+ eraseRoom: rid => dispatch(eraseRoom(rid))
+}))
+export default class RoomInfoEditView extends React.Component {
+ static propTypes = {
+ navigation: PropTypes.object,
+ eraseRoom: PropTypes.func
+ };
+
+ constructor(props) {
+ super(props);
+ const { rid } = props.navigation.state.params;
+ this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
+ this.permissions = {};
+ this.state = {
+ room: {},
+ name: '',
+ description: '',
+ topic: '',
+ announcement: '',
+ joinCode: '',
+ nameError: {},
+ saving: false,
+ t: false,
+ ro: false,
+ reactWhenReadOnly: false
+ };
+ }
+
+ async componentDidMount() {
+ await this.updateRoom();
+ this.init();
+ this.rooms.addListener(this.updateRoom);
+ this.permissions = RocketChat.hasPermission(PERMISSIONS_ARRAY, this.state.room.rid);
+ }
+
+ componentWillUnmount() {
+ this.rooms.removeAllListeners();
+ }
+
+ updateRoom = async() => {
+ const [room] = this.rooms;
+ this.setState({ room });
+ }
+
+ init = () => {
+ const {
+ name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCodeRequired
+ } = this.state.room;
+ // fake password just to user knows about it
+ this.randomValue = random(15);
+ this.setState({
+ name,
+ description,
+ topic,
+ announcement,
+ t: t === 'p',
+ ro,
+ reactWhenReadOnly,
+ joinCode: joinCodeRequired ? this.randomValue : ''
+ });
+ }
+
+ clearErrors = () => {
+ this.setState({
+ nameError: {}
+ });
+ }
+
+ reset = () => {
+ this.clearErrors();
+ this.init();
+ }
+
+ formIsChanged = () => {
+ const {
+ room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
+ } = this.state;
+ return !(room.name === name &&
+ room.description === description &&
+ room.topic === topic &&
+ room.announcement === announcement &&
+ this.randomValue === joinCode &&
+ room.t === 'p' === t &&
+ room.ro === ro &&
+ room.reactWhenReadOnly === reactWhenReadOnly
+ );
+ }
+
+ submit = async() => {
+ Keyboard.dismiss();
+ const {
+ room, name, description, topic, announcement, t, ro, reactWhenReadOnly, joinCode
+ } = this.state;
+
+ this.setState({ saving: true });
+ let error = false;
+
+ if (!this.formIsChanged()) {
+ showErrorAlert('Nothing to save!');
+ return;
+ }
+
+ // Clear error objects
+ await this.clearErrors();
+
+ const params = {};
+
+ // Name
+ if (room.name !== name) {
+ params.roomName = name;
+ }
+ // Description
+ if (room.description !== description) {
+ params.roomDescription = description;
+ }
+ // Topic
+ if (room.topic !== topic) {
+ params.roomTopic = topic;
+ }
+ // Announcement
+ if (room.announcement !== announcement) {
+ params.roomAnnouncement = announcement;
+ }
+ // Room Type
+ if (room.t !== t) {
+ params.roomType = t ? 'p' : 'c';
+ }
+ // Read Only
+ if (room.ro !== ro) {
+ params.readOnly = ro;
+ }
+ // React When Read Only
+ if (room.reactWhenReadOnly !== reactWhenReadOnly) {
+ params.reactWhenReadOnly = reactWhenReadOnly;
+ }
+
+ // Join Code
+ if (this.randomValue !== joinCode) {
+ params.joinCode = joinCode;
+ }
+
+ try {
+ await RocketChat.saveRoomSettings(room.rid, params);
+ } catch (e) {
+ if (e.error === 'error-invalid-room-name') {
+ this.setState({ nameError: e });
+ }
+ error = true;
+ }
+
+ await this.setState({ saving: false });
+ setTimeout(() => {
+ if (error) {
+ showErrorAlert('There was an error while saving settings!');
+ } else {
+ showToast('Settings succesfully changed!');
+ }
+ }, 100);
+ }
+
+ delete = () => {
+ Alert.alert(
+ 'Are you sure?',
+ 'Deleting a room will delete all messages posted within the room. This cannot be undone.',
+ [
+ {
+ text: 'Cancel',
+ style: 'cancel'
+ },
+ {
+ text: 'Yes, delete it!',
+ style: 'destructive',
+ onPress: () => this.props.eraseRoom(this.state.room.rid)
+ }
+ ],
+ { cancelable: false }
+ );
+ }
+
+ toggleArchive = () => {
+ const { archived } = this.state.room;
+ const action = `${ archived ? 'un' : '' }archive`;
+ Alert.alert(
+ 'Are you sure?',
+ `Do you really want to ${ action } this room?`,
+ [
+ {
+ text: 'Cancel',
+ style: 'cancel'
+ },
+ {
+ text: `Yes, ${ action } it!`,
+ style: 'destructive',
+ onPress: () => {
+ try {
+ RocketChat.toggleArchiveRoom(this.state.room.rid, !archived);
+ } catch (error) {
+ alert(error);
+ }
+ }
+ }
+ ],
+ { cancelable: false }
+ );
+ }
+
+ hasDeletePermission = () => (
+ this.state.room.t === 'p' ? this.permissions[PERMISSION_DELETE_P] : this.permissions[PERMISSION_DELETE_C]
+ );
+
+ hasArchivePermission = () => (
+ this.permissions[PERMISSION_ARCHIVE] || this.permissions[PERMISSION_UNARCHIVE]
+ );
+
+ render() {
+ const {
+ name, nameError, description, topic, announcement, t, ro, reactWhenReadOnly, room, joinCode
+ } = this.state;
+ return (
+
+
+
+
+ { this.name = e; }}
+ label='Name'
+ value={name}
+ onChangeText={value => this.setState({ name: value })}
+ onSubmitEditing={() => { this.description.focus(); }}
+ error={nameError}
+ />
+ { this.description = e; }}
+ label='Description'
+ value={description}
+ onChangeText={value => this.setState({ description: value })}
+ onSubmitEditing={() => { this.topic.focus(); }}
+ inputProps={{ multiline: true }}
+ />
+ { this.topic = e; }}
+ label='Topic'
+ value={topic}
+ onChangeText={value => this.setState({ topic: value })}
+ onSubmitEditing={() => { this.announcement.focus(); }}
+ inputProps={{ multiline: true }}
+ />
+ { this.announcement = e; }}
+ label='Announcement'
+ value={announcement}
+ onChangeText={value => this.setState({ announcement: value })}
+ onSubmitEditing={() => { this.joinCode.focus(); }}
+ inputProps={{ multiline: true }}
+ />
+ { this.joinCode = e; }}
+ label='Password'
+ value={joinCode}
+ onChangeText={value => this.setState({ joinCode: value })}
+ onSubmitEditing={this.submit}
+ inputProps={{ secureTextEntry: true }}
+ />
+ this.setState({ t: value })}
+ />
+ this.setState({ ro: value })}
+ disabled={!this.permissions[PERMISSION_SET_READONLY]}
+ />
+ {ro &&
+ this.setState({ reactWhenReadOnly: value })}
+ disabled={!this.permissions[PERMISSION_SET_REACT_WHEN_READONLY]}
+ />
+ }
+
+ SAVE
+
+
+
+ RESET
+
+
+
+ { room.archived ? 'UNARCHIVE' : 'ARCHIVE' }
+
+
+
+
+
+ DELETE
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/views/RoomInfoEditView/styles.js b/app/views/RoomInfoEditView/styles.js
new file mode 100644
index 00000000..e98f6786
--- /dev/null
+++ b/app/views/RoomInfoEditView/styles.js
@@ -0,0 +1,46 @@
+import { StyleSheet } from 'react-native';
+
+import { COLOR_DANGER } from '../../constants/colors';
+
+export default StyleSheet.create({
+ buttonInverted: {
+ borderColor: 'rgba(0,0,0,.15)',
+ borderWidth: 2,
+ borderRadius: 2
+ },
+ buttonContainerDisabled: {
+ backgroundColor: 'rgba(65, 72, 82, 0.7)'
+ },
+ buttonDanger: {
+ borderColor: COLOR_DANGER,
+ borderWidth: 2,
+ borderRadius: 2
+ },
+ colorDanger: {
+ color: COLOR_DANGER
+ },
+ switchContainer: {
+ flexDirection: 'row',
+ alignItems: 'flex-start'
+ },
+ switchLabelContainer: {
+ flex: 1,
+ paddingHorizontal: 10
+ },
+ switchLabelPrimary: {
+ fontSize: 16,
+ paddingBottom: 6
+ },
+ switchLabelSecondary: {
+ fontSize: 12
+ },
+ switch: {
+ alignSelf: 'center'
+ },
+ divider: {
+ height: StyleSheet.hairlineWidth,
+ borderColor: '#ddd',
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ marginVertical: 20
+ }
+});
diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js
new file mode 100644
index 00000000..bbcf14b5
--- /dev/null
+++ b/app/views/RoomInfoView/index.js
@@ -0,0 +1,192 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { View, Text, ScrollView } from 'react-native';
+import { connect } from 'react-redux';
+import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
+import moment from 'moment';
+
+import Status from '../../containers/status';
+import Avatar from '../../containers/Avatar';
+import styles from './styles';
+import sharedStyles from '../Styles';
+import database from '../../lib/realm';
+import RocketChat from '../../lib/rocketchat';
+import Touch from '../../utils/touch';
+
+const PERMISSION_EDIT_ROOM = 'edit-room';
+
+const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
+
+@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
+}))
+export default class RoomInfoView extends React.Component {
+ static propTypes = {
+ baseUrl: PropTypes.string,
+ user: PropTypes.object,
+ navigation: PropTypes.object,
+ activeUsers: PropTypes.object,
+ Message_TimeFormat: PropTypes.string,
+ roles: PropTypes.object
+ }
+
+ static navigationOptions = ({ navigation }) => {
+ const params = navigation.state.params || {};
+ if (!params.hasEditPermission) {
+ return;
+ }
+ return {
+ headerRight: (
+ navigation.navigate('RoomInfoEdit', { rid: navigation.state.params.rid })}
+ underlayColor='#ffffff'
+ activeOpacity={0.5}
+ accessibilityLabel='edit'
+ accessibilityTraits='button'
+ >
+
+
+
+
+ )
+ };
+ };
+
+ constructor(props) {
+ super(props);
+ const { rid } = props.navigation.state.params;
+ this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
+ this.sub = {
+ unsubscribe: () => {}
+ };
+ this.state = {
+ room: {},
+ roomUser: {},
+ roles: []
+ };
+ }
+
+ async componentDidMount() {
+ await this.updateRoom();
+ this.rooms.addListener(this.updateRoom);
+
+ // get user of room
+ if (this.state.room.t === 'd') {
+ try {
+ const roomUser = await RocketChat.getRoomMember(this.state.room.rid, this.props.user.id);
+ this.setState({ roomUser });
+ const username = this.state.room.name;
+
+ const activeUser = this.props.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 (error) {
+ alert(error);
+ }
+ } else {
+ const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], this.state.room.rid);
+ this.props.navigation.setParams({ hasEditPermission: permissions[PERMISSION_EDIT_ROOM] });
+ }
+ }
+
+ componentWillUnmount() {
+ this.rooms.removeAllListeners();
+ this.sub.unsubscribe();
+ }
+
+ getFullUserData = async(username) => {
+ const result = await RocketChat.subscribe('fullUserData', username);
+ this.sub = result;
+ }
+
+ getRoomTitle = room => (room.t === 'd' ? room.fname : room.name);
+
+ isDirect = () => this.state.room.t === 'd';
+
+ updateRoom = async() => {
+ const [room] = this.rooms;
+ this.setState({ room });
+ }
+ // TODO: translate
+ renderItem = (key, room) => (
+
+ {camelize(key)}
+ { room[key] ? room[key] : `No ${ key } provided.` }
+
+ );
+
+ renderRoles = () => (
+ this.state.roles.length > 0 &&
+
+ Roles
+
+ {this.state.roles.map(role => (
+
+ { this.props.roles[role] }
+
+ ))}
+
+
+ )
+
+ renderTimezone = (userId) => {
+ if (this.props.activeUsers[userId]) {
+ const { utcOffset } = this.props.activeUsers[userId];
+
+ if (!utcOffset) {
+ return null;
+ }
+ // TODO: translate
+ return (
+
+ Timezone
+ {moment().utcOffset(utcOffset).format(this.props.Message_TimeFormat)} (UTC { utcOffset })
+
+ );
+ }
+ return null;
+ }
+
+ render() {
+ const { room, roomUser } = this.state;
+ const { name, t } = room;
+ return (
+
+
+
+ {t === 'd' ? : null}
+
+ { this.getRoomTitle(room) }
+
+
+ {!this.isDirect() && this.renderItem('description', room)}
+ {!this.isDirect() && this.renderItem('topic', room)}
+ {!this.isDirect() && this.renderItem('announcement', room)}
+ {this.isDirect() && this.renderRoles()}
+ {this.isDirect() && this.renderTimezone(roomUser._id)}
+
+ );
+ }
+}
diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.js
new file mode 100644
index 00000000..a0fc8183
--- /dev/null
+++ b/app/views/RoomInfoView/styles.js
@@ -0,0 +1,70 @@
+import { StyleSheet } from 'react-native';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ flexDirection: 'column',
+ backgroundColor: '#ffffff',
+ padding: 10
+ },
+ headerButton: {
+ backgroundColor: 'transparent',
+ height: 44,
+ width: 44,
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ item: {
+ padding: 10,
+ // borderColor: '#EBEDF1',
+ // borderTopWidth: StyleSheet.hairlineWidth,
+ justifyContent: 'center'
+ },
+ avatarContainer: {
+ height: 250,
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ avatar: {
+ marginHorizontal: 10
+ },
+ roomTitle: {
+ fontSize: 18,
+ paddingTop: 20
+ },
+ roomDescription: {
+ fontSize: 14,
+ color: '#ccc',
+ paddingTop: 10
+ },
+ status: {
+ borderRadius: 24,
+ width: 24,
+ height: 24,
+ borderWidth: 4,
+ bottom: -4,
+ right: -4
+ },
+ itemLabel: {
+ fontWeight: '600',
+ marginBottom: 10
+ },
+ itemContent: {
+ color: '#ccc'
+ },
+ itemContent__empty: {
+ fontStyle: 'italic'
+ },
+ rolesContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap'
+ },
+ roleBadge: {
+ padding: 8,
+ backgroundColor: '#ddd',
+ borderRadius: 2,
+ marginRight: 5,
+ marginBottom: 5
+ }
+});
diff --git a/app/views/RoomMembersView/index.js b/app/views/RoomMembersView/index.js
index 321864f6..89beee3c 100644
--- a/app/views/RoomMembersView/index.js
+++ b/app/views/RoomMembersView/index.js
@@ -4,6 +4,7 @@ import { FlatList, Text, View, TextInput } from 'react-native';
import { connect } from 'react-redux';
import styles from './styles';
+import sharedStyles from '../Styles';
import Avatar from '../../containers/Avatar';
import Status from '../../containers/status';
import Touch from '../../utils/touch';
@@ -115,7 +116,7 @@ export default class MentionedMessagesView extends React.PureComponent {
accessibilityTraits='button'
>
- {}
+ {}
{item.username}
diff --git a/app/views/RoomMembersView/styles.js b/app/views/RoomMembersView/styles.js
index 68d6cc5e..e624966e 100644
--- a/app/views/RoomMembersView/styles.js
+++ b/app/views/RoomMembersView/styles.js
@@ -7,7 +7,7 @@ export default StyleSheet.create({
},
item: {
flexDirection: 'row',
- paddingVertical: 8,
+ paddingVertical: 10,
paddingHorizontal: 16,
alignItems: 'center'
},
@@ -15,14 +15,12 @@ export default StyleSheet.create({
marginRight: 16
},
status: {
- position: 'absolute',
- bottom: -3,
- right: -3,
+ bottom: -2,
+ right: -2,
borderWidth: 2,
- borderColor: '#fff',
- borderRadius: 12,
- width: 12,
- height: 12
+ borderRadius: 10,
+ width: 10,
+ height: 10
},
separator: {
height: StyleSheet.hairlineWidth,
diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js
index 582fd72c..811c6b91 100644
--- a/app/views/RoomView/Header/index.js
+++ b/app/views/RoomView/Header/index.js
@@ -50,7 +50,8 @@ export default class RoomHeaderView extends React.PureComponent {
getUserStatus() {
const userId = this.rid.replace(this.props.user.id, '').trim();
- return this.props.activeUsers[userId] || 'offline';
+ const userInfo = this.props.activeUsers[userId];
+ return (userInfo && userInfo.status) || 'offline';
}
getUserStatusLabel() {
@@ -86,7 +87,12 @@ export default class RoomHeaderView extends React.PureComponent {
}
return (
-
+ this.props.navigation.navigate('RoomInfo', { rid: this.rid })}
+ >
{this.isDirect() ?
: null
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index ebe8c221..ba1f4b9f 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -157,6 +157,7 @@ export default class RoomView extends React.Component {
user={this.props.user}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
+ archived={this.state.room.archived}
/>
);
@@ -171,7 +172,7 @@ export default class RoomView extends React.Component {
);
}
- if (this.state.room.ro) {
+ if (this.state.room.ro || this.state.room.archived) {
return (
This room is read only
diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js
index 1ad2a9e3..a38d7168 100644
--- a/app/views/RoomsListView/index.js
+++ b/app/views/RoomsListView/index.js
@@ -29,7 +29,6 @@ const ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
login: () => dispatch(actions.login()),
connect: () => dispatch(server.connectRequest())
}))
-
export default class RoomsListView extends React.Component {
static propTypes = {
navigation: PropTypes.object.isRequired,
@@ -51,7 +50,7 @@ export default class RoomsListView extends React.Component {
searchText: ''
};
this._keyExtractor = this._keyExtractor.bind(this);
- this.data = database.objects('subscriptions').sorted('roomUpdatedAt', true);
+ this.data = database.objects('subscriptions').filtered('archived != true').sorted('roomUpdatedAt', true);
}
componentDidMount() {
@@ -67,7 +66,7 @@ export default class RoomsListView extends React.Component {
componentWillReceiveProps(props) {
if (this.props.server !== props.server) {
this.data.removeListener(this.updateState);
- this.data = database.objects('subscriptions').sorted('roomUpdatedAt', true);
+ this.data = database.objects('subscriptions').filtered('archived != true').sorted('roomUpdatedAt', true);
this.data.addListener(this.updateState);
} else if (this.props.searchText !== props.searchText) {
this.search(props.searchText);
@@ -97,7 +96,7 @@ export default class RoomsListView extends React.Component {
});
}
- let data = this.data.filtered('name CONTAINS[c] $0', searchText).slice(0, 7);
+ let data = database.objects('subscriptions').filtered('name CONTAINS[c] $0', searchText).slice(0, 7);
const usernames = data.map(sub => sub.map);
try {
diff --git a/app/views/Styles.js b/app/views/Styles.js
index 113dd875..63c17bac 100644
--- a/app/views/Styles.js
+++ b/app/views/Styles.js
@@ -1,5 +1,7 @@
import { StyleSheet, Dimensions, Platform } from 'react-native';
+import { COLOR_DANGER } from '../constants/colors';
+
export default StyleSheet.create({
container: {
backgroundColor: 'white',
@@ -56,7 +58,7 @@ export default StyleSheet.create({
color: '#2f343d'
},
label_error: {
- color: 'red',
+ color: COLOR_DANGER,
flexGrow: 1,
paddingHorizontal: 0,
marginBottom: 20
@@ -83,10 +85,14 @@ export default StyleSheet.create({
borderColor: 'rgba(0,0,0,.15)',
color: 'black'
},
+ buttonContainerLastChild: {
+ marginBottom: 40
+ },
buttonContainer: {
paddingVertical: 15,
backgroundColor: '#414852',
- marginBottom: 20
+ marginBottom: 20,
+ borderRadius: 2
},
buttonContainer_white: {
paddingVertical: 15,
@@ -117,7 +123,7 @@ export default StyleSheet.create({
},
error: {
textAlign: 'center',
- color: 'red',
+ color: COLOR_DANGER,
paddingTop: 5
},
loading: {
@@ -166,7 +172,7 @@ export default StyleSheet.create({
color: 'green'
},
invalidText: {
- color: 'red'
+ color: COLOR_DANGER
},
validatingText: {
color: '#aaa'
@@ -177,7 +183,7 @@ export default StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
margin: 4,
- borderRadius: 4
+ borderRadius: 2
},
facebookButton: {
backgroundColor: '#3b5998'
@@ -208,5 +214,24 @@ export default StyleSheet.create({
},
oAuthModal: {
margin: 0
+ },
+ status: {
+ position: 'absolute',
+ bottom: -3,
+ right: -3,
+ borderWidth: 3,
+ borderColor: '#fff',
+ borderRadius: 16,
+ width: 16,
+ height: 16
+ },
+ alignItemsFlexEnd: {
+ alignItems: 'flex-end'
+ },
+ textAlignRight: {
+ textAlign: 'right'
+ },
+ opacity5: {
+ opacity: 0.5
}
});
diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj
index 3b7d702e..8e899c11 100644
--- a/ios/RocketChatRN.xcodeproj/project.pbxproj
+++ b/ios/RocketChatRN.xcodeproj/project.pbxproj
@@ -5,6 +5,7 @@
};
objectVersion = 46;
objects = {
+
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@@ -49,6 +50,7 @@
74815BBCB91147C08C8F7B3D /* libRNAudio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1142E3442BA94B19BCF52814 /* libRNAudio.a */; };
77C35F50C01C43668188886C /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A0EEFAF8AB14F5B9E796CDD /* libRNVectorIcons.a */; };
7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */; };
+ 7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AFB804C205AE63100D004E7 /* libRCTToast.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
8A159EDB97C44E52AF62D69C /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DA50CE47374C4C35BE6D9D58 /* libRNSVG.a */; };
8ECBD927DDAC4987B98E102E /* libRCTVideo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20CE3E407E0D4D9E8C9885F2 /* libRCTVideo.a */; };
@@ -327,6 +329,13 @@
remoteGlobalIDString = 3D7682761D8E76B80014119E;
remoteInfo = SplashScreen;
};
+ 7AFB804B205AE63100D004E7 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */;
+ proxyType = 2;
+ remoteGlobalIDString = 327633421BFAAD7E004DA88E;
+ remoteInfo = RCTToast;
+ };
832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
@@ -485,6 +494,7 @@
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; };
7A30DA4B2D474348824CD05B /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = ""; };
7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCustomInputController.xcodeproj; path = "../node_modules/react-native-keyboard-input/lib/ios/RCTCustomInputController.xcodeproj"; sourceTree = ""; };
+ 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTToast.xcodeproj; path = "../node_modules/@remobile/react-native-toast/ios/RCTToast.xcodeproj"; sourceTree = ""; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; };
8A2DD67ADD954AD9873F45FC /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; };
9A1E1766CCB84C91A62BD5A6 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = ""; };
@@ -518,6 +528,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */,
B8971BB2202A093B0000D245 /* libKeyboardTrackingView.a in Frameworks */,
7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
@@ -756,9 +767,18 @@
name = Products;
sourceTree = "";
};
+ 7AFB8036205AE63000D004E7 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 7AFB804C205AE63100D004E7 /* libRCTToast.a */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
+ 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */,
B8971BAC202A091D0000D245 /* KeyboardTrackingView.xcodeproj */,
7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */,
B88F58361FBF55E200B352B8 /* RCTPushNotification.xcodeproj */,
@@ -1091,6 +1111,10 @@
ProductGroup = 832341B11AAA6A8300B99B32 /* Products */;
ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */;
},
+ {
+ ProductGroup = 7AFB8036205AE63000D004E7 /* Products */;
+ ProjectRef = 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */;
+ },
{
ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */;
ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */;
@@ -1400,6 +1424,13 @@
remoteRef = 7ADCFEBF1FEA8A7A00763ED8 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
+ 7AFB804C205AE63100D004E7 /* libRCTToast.a */ = {
+ isa = PBXReferenceProxy;
+ fileType = archive.ar;
+ path = libRCTToast.a;
+ remoteRef = 7AFB804B205AE63100D004E7 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
832341B51AAA6A8300B99B32 /* libRCTText.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1771,6 +1802,7 @@
"$(SRCROOT)/../node_modules/react-native-splash-screen/ios",
"$(SRCROOT)/../node_modules/react-native-safari-view",
"$(SRCROOT)/../node_modules/react-native-audio/ios",
+ "$(SRCROOT)/../../../react-native/React/**",
);
INFOPLIST_FILE = RocketChatRN/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1811,6 +1843,7 @@
"$(SRCROOT)/../node_modules/react-native-splash-screen/ios",
"$(SRCROOT)/../node_modules/react-native-safari-view",
"$(SRCROOT)/../node_modules/react-native-audio/ios",
+ "$(SRCROOT)/../../../react-native/React/**",
);
INFOPLIST_FILE = RocketChatRN/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";