From 5d8ad1df82d3d200abd53bfb68c3e8008656e05a Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 29 Mar 2018 14:55:37 -0300 Subject: [PATCH] [NEW] Room info and Room info edit (#254) * - Block user - Load room members async - fixed reactive change of room's read only flag * Snippet messages * - Room files - Dismiss Video component on back button press - Improvements on Image component * Improvement on Video component * Leave room * Missing message types * lint * - Room info (read only) - Missing message types * Room info scroll * - Tap on room header opens room info - Layout tweaks * - Room info edit - iOS Toast fixed * - Style not implemented actions as disabled * Edit room permission * - Save all room settings in a single call - Implemented roomType and readOnly * - Allow reacting when room is read only * Message type added: room_changed_privacy * Erase room * Created TextInput and SwitchContainer components for reuse and readability * - hasPermission method * - Archive/Unarchive room - Set Join Code * Twitter keyboard type on iOS * Archived room * reactWhenReadOnly permission on message * Active users refactored * User roles * - Subscribe to roles (in order to get role description info: e.g. 'core-team' to 'Rocket.Chat Team') - Save roles to realm (for offline access) - Save roles to redux (and get data from realm on app init) * Lint * code style --- app/actions/actionsTypes.js | 4 +- app/actions/activeUsers.js | 7 - app/actions/roles.js | 8 + app/actions/room.js | 7 + app/constants/colors.js | 3 +- app/containers/MessageActions.js | 23 +- app/containers/MessageBox/index.js | 1 + app/containers/TextInput.js | 76 ++++ app/containers/message/index.js | 34 +- app/containers/routes/AuthRoutes.js | 16 + app/containers/status.js | 4 +- app/lib/realm.js | 19 +- app/lib/rocketchat.js | 91 ++++- app/reducers/index.js | 2 + app/reducers/roles.js | 15 + app/sagas/activeUsers.js | 13 - app/sagas/index.js | 2 - app/sagas/init.js | 6 + app/sagas/rooms.js | 33 +- app/views/RoomActionsView/index.js | 62 ++- app/views/RoomActionsView/styles.js | 13 +- app/views/RoomInfoEditView/SwitchContainer.js | 45 +++ app/views/RoomInfoEditView/index.js | 381 ++++++++++++++++++ app/views/RoomInfoEditView/styles.js | 46 +++ app/views/RoomInfoView/index.js | 192 +++++++++ app/views/RoomInfoView/styles.js | 70 ++++ app/views/RoomMembersView/index.js | 3 +- app/views/RoomMembersView/styles.js | 14 +- app/views/RoomView/Header/index.js | 10 +- app/views/RoomView/index.js | 3 +- app/views/RoomsListView/index.js | 7 +- app/views/Styles.js | 35 +- ios/RocketChatRN.xcodeproj/project.pbxproj | 33 ++ 33 files changed, 1170 insertions(+), 108 deletions(-) create mode 100644 app/actions/roles.js create mode 100644 app/containers/TextInput.js create mode 100644 app/reducers/roles.js delete mode 100644 app/sagas/activeUsers.js create mode 100644 app/views/RoomInfoEditView/SwitchContainer.js create mode 100644 app/views/RoomInfoEditView/index.js create mode 100644 app/views/RoomInfoEditView/styles.js create mode 100644 app/views/RoomInfoView/index.js create mode 100644 app/views/RoomInfoView/styles.js diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 2f1f95a30..f4daf9a81 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 a273d7348..1e7c5ecb7 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 000000000..074111985 --- /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 533fb4ab3..ef6430c3b 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 276ceda23..5caf08c58 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 6a2de4b4a..0a82d50ff 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 313e7740e..1a320fe49 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 000000000..73f2ccabf --- /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 ab3e9a3fb..babf15cec 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 9dd5cab43..e0d8c0d2a 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 55a78fcc1..8a434d0cf 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 ec0fa3824..8ad8d5db7 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 1ea5c6d60..97b00d3b9 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 e7603c54c..3a4c0dac1 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 000000000..f0e964e6c --- /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 70ca903ef..000000000 --- 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 39aa6836e..ab82fdefc 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 152991124..423e91a96 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 a3017655c..ded0d4e63 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 7eb6ea585..0b2632e40 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 dba0c3b93..175c04bd3 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 000000000..7cb85b8af --- /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 000000000..3b8834f06 --- /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 000000000..e98f6786c --- /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 000000000..bbcf14b5e --- /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 000000000..a0fc81839 --- /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 321864f68..89beee3c1 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 68d6cc5ed..e624966e1 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 582fd72ca..811c6b913 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 ebe8c221a..ba1f4b9f5 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 1ad2a9e39..a38d7168b 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 113dd875c..63c17bac6 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 3b7d702e7..8e899c110 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";