diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index effed1822..2593bd09a 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -31,23 +31,28 @@ export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_ export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const MESSAGES = createRequestTypes('MESSAGES', [ ...defaultTypes, + 'ACTIONS_SHOW', + 'ACTIONS_HIDE', 'DELETE_REQUEST', 'DELETE_SUCCESS', 'DELETE_FAILURE', 'EDIT_INIT', + 'EDIT_CANCEL', 'EDIT_REQUEST', 'EDIT_SUCCESS', 'EDIT_FAILURE', - 'STAR_REQUEST', - 'STAR_SUCCESS', - 'STAR_FAILURE', + 'TOGGLE_STAR_REQUEST', + 'TOGGLE_STAR_SUCCESS', + 'TOGGLE_STAR_FAILURE', 'PERMALINK_REQUEST', 'PERMALINK_SUCCESS', 'PERMALINK_FAILURE', + 'PERMALINK_CLEAR', 'TOGGLE_PIN_REQUEST', 'TOGGLE_PIN_SUCCESS', 'TOGGLE_PIN_FAILURE', - 'SET_INPUT' + 'SET_INPUT', + 'CLEAR_INPUT' ]); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [ ...defaultTypes, diff --git a/app/actions/index.js b/app/actions/index.js index dba8b7e18..023d9eeb0 100644 --- a/app/actions/index.js +++ b/app/actions/index.js @@ -25,6 +25,14 @@ export function setAllSettings(settings) { payload: settings }; } + +export function setAllPermissions(permissions) { + return { + type: types.SET_ALL_PERMISSIONS, + payload: permissions + }; +} + export function login() { return { type: 'LOGIN' diff --git a/app/actions/messages.js b/app/actions/messages.js index 5f01815ef..5d45b2886 100644 --- a/app/actions/messages.js +++ b/app/actions/messages.js @@ -20,6 +20,19 @@ export function messagesFailure(err) { }; } +export function actionsShow(actionMessage) { + return { + type: types.MESSAGES.ACTIONS_SHOW, + actionMessage + }; +} + +export function actionsHide() { + return { + type: types.MESSAGES.ACTIONS_HIDE + }; +} + export function deleteRequest(message) { return { type: types.MESSAGES.DELETE_REQUEST, @@ -47,6 +60,12 @@ export function editInit(message) { }; } +export function editCancel() { + return { + type: types.MESSAGES.EDIT_CANCEL + }; +} + export function editRequest(message) { return { type: types.MESSAGES.EDIT_REQUEST, @@ -66,22 +85,22 @@ export function editFailure() { }; } -export function starRequest(message) { +export function toggleStarRequest(message) { return { - type: types.MESSAGES.STAR_REQUEST, + type: types.MESSAGES.TOGGLE_STAR_REQUEST, message }; } -export function starSuccess() { +export function toggleStarSuccess() { return { - type: types.MESSAGES.STAR_SUCCESS + type: types.MESSAGES.TOGGLE_STAR_SUCCESS }; } -export function starFailure() { +export function toggleStarFailure() { return { - type: types.MESSAGES.STAR_FAILURE + type: types.MESSAGES.TOGGLE_STAR_FAILURE }; } @@ -106,6 +125,12 @@ export function permalinkFailure(err) { }; } +export function permalinkClear() { + return { + type: types.MESSAGES.PERMALINK_CLEAR + }; +} + export function togglePinRequest(message) { return { type: types.MESSAGES.TOGGLE_PIN_REQUEST, @@ -132,3 +157,9 @@ export function setInput(message) { message }; } + +export function clearInput() { + return { + type: types.MESSAGES.CLEAR_INPUT + }; +} diff --git a/app/constants/types.js b/app/constants/types.js index f772c2b92..0ac33098d 100644 --- a/app/constants/types.js +++ b/app/constants/types.js @@ -1,2 +1,3 @@ export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER'; export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS'; +export const SET_ALL_PERMISSIONS = 'SET_ALL_PERMISSIONS'; diff --git a/app/containers/MessageActions.js b/app/containers/MessageActions.js new file mode 100644 index 000000000..7c645e231 --- /dev/null +++ b/app/containers/MessageActions.js @@ -0,0 +1,308 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Alert, Clipboard } from 'react-native'; +import { connect } from 'react-redux'; +import ActionSheet from 'react-native-actionsheet'; +import * as moment from 'moment'; + +import { + deleteRequest, + editInit, + toggleStarRequest, + permalinkRequest, + permalinkClear, + togglePinRequest, + setInput, + actionsHide +} from '../actions/messages'; + +@connect( + state => ({ + showActions: state.messages.showActions, + actionMessage: state.messages.actionMessage, + user: state.login.user, + permissions: state.permissions, + permalink: state.messages.permalink, + Message_AllowDeleting: state.settings.Message_AllowDeleting, + Message_AllowDeleting_BlockDeleteInMinutes: state.settings.Message_AllowDeleting_BlockDeleteInMinutes, + Message_AllowEditing: state.settings.Message_AllowEditing, + Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes, + Message_AllowPinning: state.settings.Message_AllowPinning, + Message_AllowStarring: state.settings.Message_AllowStarring + }), + dispatch => ({ + actionsHide: () => dispatch(actionsHide()), + deleteRequest: message => dispatch(deleteRequest(message)), + editInit: message => dispatch(editInit(message)), + toggleStarRequest: message => dispatch(toggleStarRequest(message)), + permalinkRequest: message => dispatch(permalinkRequest(message)), + permalinkClear: () => dispatch(permalinkClear()), + togglePinRequest: message => dispatch(togglePinRequest(message)), + setInput: message => dispatch(setInput(message)) + }) +) +export default class MessageActions extends React.Component { + static propTypes = { + actionsHide: PropTypes.func.isRequired, + showActions: PropTypes.bool.isRequired, + room: PropTypes.object, + actionMessage: PropTypes.object, + user: PropTypes.object, + permissions: PropTypes.object.isRequired, + deleteRequest: PropTypes.func.isRequired, + editInit: PropTypes.func.isRequired, + toggleStarRequest: PropTypes.func.isRequired, + permalinkRequest: PropTypes.func.isRequired, + permalinkClear: PropTypes.func.isRequired, + togglePinRequest: PropTypes.func.isRequired, + setInput: PropTypes.func.isRequired, + permalink: PropTypes.string, + Message_AllowDeleting: PropTypes.bool, + Message_AllowDeleting_BlockDeleteInMinutes: PropTypes.number, + Message_AllowEditing: PropTypes.bool, + Message_AllowEditing_BlockEditInMinutes: PropTypes.number, + Message_AllowPinning: PropTypes.bool, + Message_AllowStarring: PropTypes.bool + }; + + constructor(props) { + super(props); + this.state = { + copyPermalink: false, + reply: false, + quote: false + }; + this.handleActionPress = this.handleActionPress.bind(this); + this.options = ['']; + const { roles } = this.props.room[0]; + 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); + } + + async componentWillReceiveProps(nextProps) { + if (nextProps.showActions !== this.props.showActions && nextProps.showActions) { + const { actionMessage } = nextProps; + // Cancel + this.options = ['Cancel']; + this.CANCEL_INDEX = 0; + // Reply + this.options.push('Reply'); + this.REPLY_INDEX = this.options.length - 1; + // Edit + if (this.allowEdit(nextProps)) { + this.options.push('Edit'); + this.EDIT_INDEX = this.options.length - 1; + } + // Permalink + this.options.push('Copy Permalink'); + this.PERMALINK_INDEX = this.options.length - 1; + // Copy + this.options.push('Copy Message'); + this.COPY_INDEX = this.options.length - 1; + // Quote + this.options.push('Quote'); + this.QUOTE_INDEX = this.options.length - 1; + // Star + if (this.props.Message_AllowStarring) { + this.options.push(actionMessage.starred ? 'Unstar' : 'Star'); + this.STAR_INDEX = this.options.length - 1; + } + // Pin + if (this.props.Message_AllowPinning) { + this.options.push(actionMessage.pinned ? 'Unpin' : 'Pin'); + this.PIN_INDEX = this.options.length - 1; + } + // Delete + if (this.allowDelete(nextProps)) { + this.options.push('Delete'); + this.DELETE_INDEX = this.options.length - 1; + } + setTimeout(() => { + this.ActionSheet.show(); + }); + } else if (this.props.permalink !== nextProps.permalink && nextProps.permalink) { + // copy permalink + if (this.state.copyPermalink) { + this.setState({ copyPermalink: false }); + await Clipboard.setString(nextProps.permalink); + Alert.alert('Permalink copied to clipboard!'); + this.props.permalinkClear(); + // quote + } else if (this.state.quote) { + this.setState({ quote: false }); + const msg = `[ ](${ nextProps.permalink }) `; + this.props.setInput({ msg }); + + // reply + } else if (this.state.reply) { + this.setState({ reply: false }); + let msg = `[ ](${ nextProps.permalink }) `; + + // if original message wasn't sent by current user and neither from a direct room + if (this.props.user.username !== this.props.actionMessage.u.username && this.props.room[0].t !== 'd') { + msg += `@${ this.props.actionMessage.u.username } `; + } + this.props.setInput({ msg }); + } + } + } + + componentDidUpdate() { + this.setPermissions(this.props.permissions); + } + + setPermissions(permissions) { + this.hasEditPermission = permissions['edit-message'] + .some(item => this.mergedRoles.indexOf(item) !== -1); + this.hasDeletePermission = permissions['delete-message'] + .some(item => this.mergedRoles.indexOf(item) !== -1); + this.hasForceDeletePermission = permissions['force-delete-message'] + .some(item => this.mergedRoles.indexOf(item) !== -1); + } + + isOwn = props => props.actionMessage.u && props.actionMessage.u._id === props.user.id; + + allowEdit = (props) => { + const editOwn = this.isOwn(props); + const { Message_AllowEditing: isEditAllowed } = this.props; + if (!(this.hasEditPermission || (isEditAllowed && editOwn))) { + return false; + } + const blockEditInMinutes = this.props.Message_AllowEditing_BlockEditInMinutes; + if (blockEditInMinutes) { + let msgTs; + if (props.actionMessage.ts != null) { + msgTs = moment(props.actionMessage.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockEditInMinutes; + } + return true; + } + + allowDelete = (props) => { + const deleteOwn = this.isOwn(props); + const { Message_AllowDeleting: isDeleteAllowed } = this.props; + if (!(this.hasDeletePermission || (isDeleteAllowed && deleteOwn) || this.hasForceDeletePermission)) { + return false; + } + if (this.hasForceDeletePermission) { + return true; + } + const blockDeleteInMinutes = this.props.Message_AllowDeleting_BlockDeleteInMinutes; + if (blockDeleteInMinutes != null && blockDeleteInMinutes !== 0) { + let msgTs; + if (props.actionMessage.ts != null) { + msgTs = moment(props.actionMessage.ts); + } + let currentTsDiff; + if (msgTs != null) { + currentTsDiff = moment().diff(msgTs, 'minutes'); + } + return currentTsDiff < blockDeleteInMinutes; + } + return true; + } + + handleDelete() { + Alert.alert( + 'Are you sure?', + 'You will not be able to recover this message!', + [ + { + text: 'Cancel', + style: 'cancel' + }, + { + text: 'Yes, delete it!', + style: 'destructive', + onPress: () => this.props.deleteRequest(this.props.actionMessage) + } + ], + { cancelable: false } + ); + } + + handleEdit() { + const { _id, msg, rid } = this.props.actionMessage; + this.props.editInit({ _id, msg, rid }); + } + + handleCopy = async() => { + await Clipboard.setString(this.props.actionMessage.msg); + Alert.alert('Copied to clipboard!'); + } + + handleStar() { + this.props.toggleStarRequest(this.props.actionMessage); + } + + handlePermalink() { + this.setState({ copyPermalink: true }); + this.props.permalinkRequest(this.props.actionMessage); + } + + handlePin() { + this.props.togglePinRequest(this.props.actionMessage); + } + + handleReply() { + this.setState({ reply: true }); + this.props.permalinkRequest(this.props.actionMessage); + } + + handleQuote() { + this.setState({ quote: true }); + this.props.permalinkRequest(this.props.actionMessage); + } + + handleActionPress = (actionIndex) => { + switch (actionIndex) { + case this.REPLY_INDEX: + this.handleReply(); + break; + case this.EDIT_INDEX: + this.handleEdit(); + break; + case this.PERMALINK_INDEX: + this.handlePermalink(); + break; + case this.COPY_INDEX: + this.handleCopy(); + break; + case this.QUOTE_INDEX: + this.handleQuote(); + break; + case this.STAR_INDEX: + this.handleStar(); + break; + case this.PIN_INDEX: + this.handlePin(); + break; + case this.DELETE_INDEX: + this.handleDelete(); + break; + default: + break; + } + this.props.actionsHide(); + } + + render() { + return ( + this.ActionSheet = o} + title='Messages actions' + options={this.options} + cancelButtonIndex={this.CANCEL_INDEX} + destructiveButtonIndex={this.DELETE_INDEX} + onPress={this.handleActionPress} + /> + ); + } +} diff --git a/app/containers/MessageBox.js b/app/containers/MessageBox.js index 8a9d1c453..95de126bf 100644 --- a/app/containers/MessageBox.js +++ b/app/containers/MessageBox.js @@ -6,7 +6,7 @@ import ImagePicker from 'react-native-image-picker'; import { connect } from 'react-redux'; import { userTyping } from '../actions/room'; import RocketChat from '../lib/rocketchat'; -import { editRequest } from '../actions/messages'; +import { editRequest, editCancel, clearInput } from '../actions/messages'; const styles = StyleSheet.create({ textBox: { @@ -25,7 +25,7 @@ const styles = StyleSheet.create({ alignSelf: 'stretch', flexGrow: 1 }, - fileButton: { + actionButtons: { color: '#aaa', paddingTop: 10, paddingBottom: 10, @@ -40,23 +40,29 @@ const styles = StyleSheet.create({ message: state.messages.message, editing: state.messages.editing }), dispatch => ({ + editCancel: () => dispatch(editCancel()), editRequest: message => dispatch(editRequest(message)), - typing: status => dispatch(userTyping(status)) + typing: status => dispatch(userTyping(status)), + clearInput: () => dispatch(clearInput()) })) export default class MessageBox extends React.Component { static propTypes = { onSubmit: PropTypes.func.isRequired, rid: PropTypes.string.isRequired, + editCancel: PropTypes.func.isRequired, editRequest: PropTypes.func.isRequired, message: PropTypes.object, editing: PropTypes.bool, - typing: PropTypes.func + typing: PropTypes.func, + clearInput: PropTypes.func } componentWillReceiveProps(nextProps) { - if (this.props.message !== nextProps.message) { + if (this.props.message !== nextProps.message && nextProps.message) { this.component.setNativeProps({ text: nextProps.message.msg }); this.component.focus(); + } else if (!nextProps.message) { + this.component.setNativeProps({ text: '' }); } } @@ -75,6 +81,7 @@ export default class MessageBox extends React.Component { // if is submiting a new message this.props.onSubmit(message); } + this.props.clearInput(); } addFile = () => { @@ -104,10 +111,24 @@ export default class MessageBox extends React.Component { }); } + editCancel() { + this.props.editCancel(); + this.component.setNativeProps({ text: '' }); + } + + renderLeftButton() { + const { editing } = this.props; + if (editing) { + return this.editCancel()} />; + } + return ; + } + render() { return ( + {this.renderLeftButton()} this.component = component} style={styles.textBoxInput} @@ -119,7 +140,6 @@ export default class MessageBox extends React.Component { underlineColorAndroid='transparent' defaultValue='' /> - ); diff --git a/app/containers/message/User.js b/app/containers/message/User.js index 873e96888..4921b48e8 100644 --- a/app/containers/message/User.js +++ b/app/containers/message/User.js @@ -31,7 +31,7 @@ const styles = StyleSheet.create({ } }); -export default class Message extends React.PureComponent { +export default class User extends React.PureComponent { static propTypes = { item: PropTypes.object.isRequired, Message_TimeFormat: PropTypes.string.isRequired, diff --git a/app/containers/message/index.js b/app/containers/message/index.js index 2835a6d41..4cc67c123 100644 --- a/app/containers/message/index.js +++ b/app/containers/message/index.js @@ -1,28 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, StyleSheet, TouchableOpacity, Text, Alert, Clipboard } from 'react-native'; +import { View, StyleSheet, TouchableOpacity, Text } from 'react-native'; import { emojify } from 'react-emojione'; import Markdown from 'react-native-easy-markdown'; // eslint-disable-line -import ActionSheet from 'react-native-actionsheet'; import { connect } from 'react-redux'; +import { actionsShow } from '../../actions/messages'; import Card from './Card'; import User from './User'; import Avatar from '../Avatar'; -import { - deleteRequest, - editInit, - starRequest, - permalinkRequest, - togglePinRequest, - setInput -} from '../../actions/messages'; -import RocketChat from '../../lib/rocketchat'; - -const title = 'Message actions'; -const options = ['Cancel', 'Reply', 'Edit', 'Permalink', 'Copy', 'Quote', 'Star Message', 'Pin Message', 'Delete']; -const CANCEL_INDEX = 0; -const DESTRUCTIVE_INDEX = 8; const styles = StyleSheet.create({ content: { @@ -47,70 +33,23 @@ const styles = StyleSheet.create({ @connect(state => ({ message: state.messages.message, - permalink: state.messages.permalink, - user: state.login.user + editing: state.messages.editing }), dispatch => ({ - deleteRequest: message => dispatch(deleteRequest(message)), - editInit: message => dispatch(editInit(message)), - starRequest: message => dispatch(starRequest(message)), - permalinkRequest: message => dispatch(permalinkRequest(message)), - togglePinRequest: message => dispatch(togglePinRequest(message)), - setInput: message => dispatch(setInput(message)) + actionsShow: actionMessage => dispatch(actionsShow(actionMessage)) })) export default class Message extends React.Component { static propTypes = { item: PropTypes.object.isRequired, baseUrl: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired, - deleteRequest: PropTypes.func.isRequired, - editInit: PropTypes.func.isRequired, - starRequest: PropTypes.func.isRequired, - permalinkRequest: PropTypes.func.isRequired, - togglePinRequest: PropTypes.func.isRequired, - setInput: PropTypes.func.isRequired, - user: PropTypes.object.isRequired, - message: PropTypes.object, - permalink: PropTypes.string + message: PropTypes.object.isRequired, + editing: PropTypes.bool, + actionsShow: PropTypes.func } - constructor(props) { - super(props); - this.state = { - copyPermalink: false, - reply: false, - quote: false - }; - this.handleActionPress = this.handleActionPress.bind(this); - this.showActions = this.showActions.bind(this); - } - - async componentWillReceiveProps(nextProps) { - if (this.props.permalink !== nextProps.permalink) { - // copy permalink - if (this.state.copyPermalink) { - this.setState({ copyPermalink: false }); - await Clipboard.setString(nextProps.permalink); - Alert.alert('Permalink copied to clipboard!'); - - // quote - } else if (this.state.quote) { - this.setState({ quote: false }); - const msg = `[ ](${ nextProps.permalink }) `; - this.props.setInput({ msg }); - - // reply - } else if (this.state.reply) { - this.setState({ reply: false }); - let msg = `[ ](${ nextProps.permalink }) `; - const room = await RocketChat.getRoom(this.props.item.rid); - - // if original message wasn't sent by current user and neither from a direct room - if (this.props.user.username !== this.props.item.u.username && room.t !== 'd') { - msg += `@${ this.props.item.u.username } `; - } - this.props.setInput({ msg }); - } - } + onLongPress() { + const { item } = this.props; + this.props.actionsShow(JSON.parse(JSON.stringify(item))); } isDeleted() { @@ -125,90 +64,6 @@ export default class Message extends React.Component { ) : null; } - showActions = () => { - this.ActionSheet.show(); - } - - handleDelete() { - Alert.alert( - 'Are you sure?', - 'You will not be able to recover this message!', - [ - { - text: 'Cancel', - style: 'cancel' - }, - { - text: 'Yes, delete it!', - style: 'destructive', - onPress: () => this.props.deleteRequest(this.props.item) - } - ], - { cancelable: false } - ); - } - - handleEdit() { - const { _id, msg, rid } = this.props.item; - this.props.editInit({ _id, msg, rid }); - } - - handleCopy = async() => { - await Clipboard.setString(this.props.item.msg); - Alert.alert('Copied to clipboard!'); - } - - handleStar() { - this.props.starRequest(this.props.item); - } - - handlePermalink() { - this.setState({ copyPermalink: true }); - this.props.permalinkRequest(this.props.item); - } - - handleTogglePin() { - this.props.togglePinRequest(this.props.item); - } - - handleReply() { - this.setState({ reply: true }); - this.props.permalinkRequest(this.props.item); - } - - handleQuote() { - this.setState({ quote: true }); - this.props.permalinkRequest(this.props.item); - } - - handleActionPress = (actionIndex) => { - // reply - if (actionIndex === 1) { - this.handleReply(); - // edit - } else if (actionIndex === 2) { - this.handleEdit(); - // permalink - } else if (actionIndex === 3) { - this.handlePermalink(); - // copy - } else if (actionIndex === 4) { - this.handleCopy(); - // quote - } else if (actionIndex === 5) { - this.handleQuote(); - // star - } else if (actionIndex === 6) { - this.handleStar(); - // toggle pin - } else if (actionIndex === 7) { - this.handleTogglePin(); - // delete - } else if (actionIndex === 8) { - this.handleDelete(); - } - } - renderMessageContent() { if (this.isDeleted()) { return Message removed; @@ -223,7 +78,9 @@ export default class Message extends React.Component { } render() { - const { item } = this.props; + const { + item, message, editing + } = this.props; const extraStyle = {}; if (item.temp) { @@ -231,40 +88,30 @@ export default class Message extends React.Component { } const username = item.alias || item.u.username; - const isEditing = this.props.message._id === item._id; + const isEditing = message._id === item._id && editing; return ( this.showActions()} + onLongPress={() => this.onLongPress()} disabled={this.isDeleted()} - style={isEditing ? styles.editing : null} + style={[styles.message, extraStyle, isEditing ? styles.editing : null]} > - - + + - - - {this.attachments()} - {this.renderMessageContent(item)} - - this.ActionSheet = o} - title={title} - options={options} - cancelButtonIndex={CANCEL_INDEX} - destructiveButtonIndex={DESTRUCTIVE_INDEX} - onPress={this.handleActionPress} /> + {this.attachments()} + {this.renderMessageContent(item)} ); diff --git a/app/lib/realm.js b/app/lib/realm.js index 73243c0b8..bc36b6940 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -24,6 +24,24 @@ const settingsSchema = { } }; +const permissionsRolesSchema = { + name: 'permissionsRoles', + properties: { + value: 'string' + } +}; + +const permissionsSchema = { + name: 'permissions', + primaryKey: '_id', + properties: { + _id: 'string', + _server: 'servers', + roles: { type: 'list', objectType: 'permissionsRoles' }, + _updatedAt: { type: 'date', optional: true } + } +}; + const roomsSchema = { name: 'rooms', primaryKey: '_id', @@ -35,6 +53,13 @@ const roomsSchema = { } }; +const subscriptionRolesSchema = { + name: 'subscriptionRolesSchema', + properties: { + value: 'string' + } +}; + const subscriptionSchema = { name: 'subscriptions', primaryKey: '_id', @@ -50,7 +75,7 @@ const subscriptionSchema = { rid: 'string', open: { type: 'bool', optional: true }, alert: { type: 'bool', optional: true }, - // roles: [ 'owner' ], + roles: { type: 'list', objectType: 'subscriptionRolesSchema' }, unread: { type: 'int', optional: true }, userMentions: { type: 'int', optional: true }, // userMentions: 0, @@ -128,11 +153,14 @@ const realm = new Realm({ settingsSchema, serversSchema, subscriptionSchema, + subscriptionRolesSchema, messagesSchema, usersSchema, roomsSchema, attachment, - messagesEditedBySchema + messagesEditedBySchema, + permissionsSchema, + permissionsRolesSchema ], deleteRealmIfMigrationNeeded: true }); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 58fa11635..52c4e573a 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -70,7 +70,7 @@ const RocketChat = { message.temp = false; message._server = server; message.attachments = message.attachments || []; - message.starred = !!message.starred; + message.starred = message.starred && message.starred.length > 0; realm.create('messages', message, true); }); } @@ -85,6 +85,9 @@ const RocketChat = { const [type, data] = ddpMessage.fields.args; const [, ev] = ddpMessage.fields.eventName.split('/'); if (/subscriptions/.test(ev)) { + if (data.roles) { + data.roles = data.roles.map(role => ({ value: role })); + } realm.write(() => { realm.create('subscriptions', data, true); }); @@ -98,6 +101,7 @@ const RocketChat = { } }); RocketChat.getSettings(); + RocketChat.getPermissions(); }); }) .catch(e => console.error(e)); @@ -136,6 +140,17 @@ const RocketChat = { }).then(response => response.json()); }, + userInfo({ server, token, userId }) { + return fetch(`${ server }/api/v1/users.info?userId=${ userId }`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': userId + } + }).then(response => response.json()); + }, + register({ credentials }) { return call('registerUser', credentials); }, @@ -385,6 +400,9 @@ const RocketChat = { if (room) { subscription.roomUpdatedAt = room._updatedAt; } + if (subscription.roles) { + subscription.roles = subscription.roles.map(role => ({ value: role })); + } subscription._server = { id: server.server }; return subscription; }); @@ -413,7 +431,8 @@ const RocketChat = { reduxStore.dispatch(actions.setAllSettings(RocketChat.parseSettings(filteredSettings))); }, parseSettings: settings => settings.reduce((ret, item) => { - ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.value; + ret[item._id] = item[settingsType[item.type]] || item.valueAsString || item.valueAsNumber || + item.valueAsBoolean || item.value; return ret; }, {}), _prepareSettings(settings) { @@ -423,6 +442,26 @@ const RocketChat = { }); }, _filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value), + async getPermissions() { + const temp = realm.objects('permissions').sorted('_updatedAt', true)[0]; + const result = await (!temp ? call('permissions/get') : call('permissions/get', new Date(temp._updatedAt))); + let permissions = temp ? result.update : result; + permissions = RocketChat._preparePermissions(permissions); + realm.write(() => { + permissions.forEach(permission => realm.create('permissions', permission, true)); + }); + reduxStore.dispatch(actions.setAllPermissions(RocketChat.parsePermissions(permissions))); + }, + parsePermissions: permissions => permissions.reduce((ret, item) => { + ret[item._id] = item.roles.reduce((roleRet, role) => [...roleRet, role.value], []); + return ret; + }, {}), + _preparePermissions(permissions) { + permissions.forEach((permission) => { + permission.roles = permission.roles.map(role => ({ value: role })); + }); + return permissions; + }, deleteMessage(message) { return call('deleteMessage', { _id: message._id }); }, @@ -430,7 +469,7 @@ const RocketChat = { const { _id, msg, rid } = message; return call('updateMessage', { _id, msg, rid }); }, - starMessage(message) { + toggleStarMessage(message) { return call('starMessage', { _id: message._id, rid: message.rid, starred: !message.starred }); }, togglePinMessage(message) { diff --git a/app/reducers/index.js b/app/reducers/index.js index 15a063a61..860dbdc35 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -8,8 +8,8 @@ import server from './server'; import navigator from './navigator'; import createChannel from './createChannel'; import app from './app'; - +import permissions from './permissions'; export default combineReducers({ - settings, login, meteor, messages, server, navigator, createChannel, app, room + settings, login, meteor, messages, server, navigator, createChannel, app, room, permissions }); diff --git a/app/reducers/messages.js b/app/reducers/messages.js index 4c1a73e2d..9061d6f47 100644 --- a/app/reducers/messages.js +++ b/app/reducers/messages.js @@ -4,8 +4,10 @@ const initialState = { isFetching: false, failure: false, message: {}, + actionMessage: {}, editing: false, - permalink: '' + permalink: '', + showActions: false }; export default function messages(state = initialState, action) { @@ -27,12 +29,29 @@ export default function messages(state = initialState, action) { failure: true, errorMessage: action.err }; + case types.MESSAGES.ACTIONS_SHOW: + return { + ...state, + showActions: true, + actionMessage: action.actionMessage + }; + case types.MESSAGES.ACTIONS_HIDE: + return { + ...state, + showActions: false + }; case types.MESSAGES.EDIT_INIT: return { ...state, message: action.message, editing: true }; + case types.MESSAGES.EDIT_CANCEL: + return { + ...state, + message: {}, + editing: false + }; case types.MESSAGES.EDIT_SUCCESS: return { ...state, @@ -50,13 +69,21 @@ export default function messages(state = initialState, action) { ...state, permalink: action.permalink }; + case types.MESSAGES.PERMALINK_CLEAR: + return { + ...state, + permalink: '' + }; case types.MESSAGES.SET_INPUT: return { ...state, message: action.message }; - // case types.LOGOUT: - // return initialState; + case types.MESSAGES.CLEAR_INPUT: + return { + ...state, + message: {} + }; default: return state; } diff --git a/app/reducers/permissions.js b/app/reducers/permissions.js new file mode 100644 index 000000000..e90a871e6 --- /dev/null +++ b/app/reducers/permissions.js @@ -0,0 +1,17 @@ +import * as types from '../constants/types'; + +const initialState = { + permissions: {} +}; + + +export default function permissions(state = initialState.permissions, action) { + if (action.type === types.SET_ALL_PERMISSIONS) { + return { + ...state, + ...action.payload + }; + } + + return state; +} diff --git a/app/sagas/init.js b/app/sagas/init.js index 78938dd88..024c26b45 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -18,8 +18,10 @@ const restore = function* restore() { const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); if (currentServer) { yield put(setServer(currentServer)); - const tmp = realm.objects('settings'); - yield put(actions.setAllSettings(RocketChat.parseSettings(tmp.slice(0, tmp.length)))); + const settings = realm.objects('settings'); + yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length)))); + const permissions = realm.objects('permissions'); + yield put(actions.setAllPermissions(RocketChat.parsePermissions(permissions.slice(0, permissions.length)))); } yield put(actions.appReady({})); } catch (e) { diff --git a/app/sagas/login.js b/app/sagas/login.js index 0838aea02..eb767e87f 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -26,6 +26,7 @@ const setUsernameCall = args => RocketChat.setUsername(args); const logoutCall = args => RocketChat.logout(args); const meCall = args => RocketChat.me(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args); +const userInfoCall = args => RocketChat.userInfo(args); const getToken = function* getToken() { const currentServer = yield select(getServer); @@ -76,6 +77,10 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) { // if user has username if (me.username) { user.username = me.username; + const userInfo = yield call(userInfoCall, { server, token: user.token, userId: user.id }); + if (userInfo.user.roles) { + user.roles = userInfo.user.roles; + } } else { yield put(registerIncomplete()); } diff --git a/app/sagas/messages.js b/app/sagas/messages.js index b3d54db3e..4f806aaec 100644 --- a/app/sagas/messages.js +++ b/app/sagas/messages.js @@ -7,8 +7,8 @@ import { deleteFailure, editSuccess, editFailure, - starSuccess, - starFailure, + toggleStarSuccess, + toggleStarFailure, permalinkSuccess, permalinkFailure, togglePinSuccess, @@ -18,7 +18,7 @@ import RocketChat from '../lib/rocketchat'; const deleteMessage = message => RocketChat.deleteMessage(message); const editMessage = message => RocketChat.editMessage(message); -const starMessage = message => RocketChat.starMessage(message); +const toggleStarMessage = message => RocketChat.toggleStarMessage(message); const getPermalink = message => RocketChat.getPermalink(message); const togglePinMessage = message => RocketChat.togglePinMessage(message); @@ -54,12 +54,12 @@ const handleEditRequest = function* handleEditRequest({ message }) { } }; -const handleStarRequest = function* handleStarRequest({ message }) { +const handleToggleStarRequest = function* handleToggleStarRequest({ message }) { try { - yield call(starMessage, message); - yield put(starSuccess()); + yield call(toggleStarMessage, message); + yield put(toggleStarSuccess()); } catch (error) { - yield put(starFailure()); + yield put(toggleStarFailure()); } }; @@ -85,7 +85,7 @@ const root = function* root() { yield takeLatest(MESSAGES.REQUEST, get); yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest); yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest); - yield takeLatest(MESSAGES.STAR_REQUEST, handleStarRequest); + yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest); yield takeLatest(MESSAGES.PERMALINK_REQUEST, handlePermalinkRequest); yield takeLatest(MESSAGES.TOGGLE_PIN_REQUEST, handleTogglePinRequest); }; diff --git a/app/views/RoomView.js b/app/views/RoomView.js index a8d9a5823..e4c4ad80a 100644 --- a/app/views/RoomView.js +++ b/app/views/RoomView.js @@ -7,9 +7,11 @@ import { bindActionCreators } from 'redux'; import * as actions from '../actions'; import { openRoom } from '../actions/room'; +import { editCancel } from '../actions/messages'; import realm from '../lib/realm'; import RocketChat from '../lib/rocketchat'; import Message from '../containers/message'; +import MessageActions from '../containers/MessageActions'; import MessageBox from '../containers/MessageBox'; import Typing from '../containers/Typing'; import KeyboardView from '../presentation/KeyboardView'; @@ -57,13 +59,15 @@ const typing = () => ; }), dispatch => ({ actions: bindActionCreators(actions, dispatch), - openRoom: room => dispatch(openRoom(room)) + openRoom: room => dispatch(openRoom(room)), + editCancel: () => dispatch(editCancel()) }) ) export default class RoomView extends React.Component { static propTypes = { navigation: PropTypes.object.isRequired, openRoom: PropTypes.func.isRequired, + editCancel: PropTypes.func, rid: PropTypes.string, server: PropTypes.string, sid: PropTypes.string, @@ -86,6 +90,7 @@ export default class RoomView extends React.Component { .objects('messages') .filtered('_server.id = $0 AND rid = $1', this.props.server, this.rid) .sorted('ts', true); + this.room = realm.objects('subscriptions').filtered('rid = $0', this.rid); this.state = { slow: false, dataSource: ds.cloneWithRows([]), @@ -114,6 +119,7 @@ export default class RoomView extends React.Component { componentWillUnmount() { clearTimeout(this.timer); this.data.removeAllListeners(); + this.props.editCancel(); } onEndReached = () => { @@ -210,6 +216,7 @@ export default class RoomView extends React.Component { /> {this.renderFooter()} + ); }