Messages permissions (#100)

This commit is contained in:
Diego Mello 2017-11-24 18:44:52 -02:00 committed by Guilherme Gazzo
parent 1cda98f415
commit f65a284953
17 changed files with 566 additions and 221 deletions

View File

@ -31,23 +31,28 @@ export const ROOM = createRequestTypes('ROOM', ['ADD_USER_TYPING', 'REMOVE_USER_
export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const APP = createRequestTypes('APP', ['READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [ export const MESSAGES = createRequestTypes('MESSAGES', [
...defaultTypes, ...defaultTypes,
'ACTIONS_SHOW',
'ACTIONS_HIDE',
'DELETE_REQUEST', 'DELETE_REQUEST',
'DELETE_SUCCESS', 'DELETE_SUCCESS',
'DELETE_FAILURE', 'DELETE_FAILURE',
'EDIT_INIT', 'EDIT_INIT',
'EDIT_CANCEL',
'EDIT_REQUEST', 'EDIT_REQUEST',
'EDIT_SUCCESS', 'EDIT_SUCCESS',
'EDIT_FAILURE', 'EDIT_FAILURE',
'STAR_REQUEST', 'TOGGLE_STAR_REQUEST',
'STAR_SUCCESS', 'TOGGLE_STAR_SUCCESS',
'STAR_FAILURE', 'TOGGLE_STAR_FAILURE',
'PERMALINK_REQUEST', 'PERMALINK_REQUEST',
'PERMALINK_SUCCESS', 'PERMALINK_SUCCESS',
'PERMALINK_FAILURE', 'PERMALINK_FAILURE',
'PERMALINK_CLEAR',
'TOGGLE_PIN_REQUEST', 'TOGGLE_PIN_REQUEST',
'TOGGLE_PIN_SUCCESS', 'TOGGLE_PIN_SUCCESS',
'TOGGLE_PIN_FAILURE', 'TOGGLE_PIN_FAILURE',
'SET_INPUT' 'SET_INPUT',
'CLEAR_INPUT'
]); ]);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [ export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [
...defaultTypes, ...defaultTypes,

View File

@ -25,6 +25,14 @@ export function setAllSettings(settings) {
payload: settings payload: settings
}; };
} }
export function setAllPermissions(permissions) {
return {
type: types.SET_ALL_PERMISSIONS,
payload: permissions
};
}
export function login() { export function login() {
return { return {
type: 'LOGIN' type: 'LOGIN'

View File

@ -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) { export function deleteRequest(message) {
return { return {
type: types.MESSAGES.DELETE_REQUEST, 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) { export function editRequest(message) {
return { return {
type: types.MESSAGES.EDIT_REQUEST, type: types.MESSAGES.EDIT_REQUEST,
@ -66,22 +85,22 @@ export function editFailure() {
}; };
} }
export function starRequest(message) { export function toggleStarRequest(message) {
return { return {
type: types.MESSAGES.STAR_REQUEST, type: types.MESSAGES.TOGGLE_STAR_REQUEST,
message message
}; };
} }
export function starSuccess() { export function toggleStarSuccess() {
return { return {
type: types.MESSAGES.STAR_SUCCESS type: types.MESSAGES.TOGGLE_STAR_SUCCESS
}; };
} }
export function starFailure() { export function toggleStarFailure() {
return { 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) { export function togglePinRequest(message) {
return { return {
type: types.MESSAGES.TOGGLE_PIN_REQUEST, type: types.MESSAGES.TOGGLE_PIN_REQUEST,
@ -132,3 +157,9 @@ export function setInput(message) {
message message
}; };
} }
export function clearInput() {
return {
type: types.MESSAGES.CLEAR_INPUT
};
}

View File

@ -1,2 +1,3 @@
export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER'; export const SET_CURRENT_SERVER = 'SET_CURRENT_SERVER';
export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS'; export const SET_ALL_SETTINGS = 'SET_ALL_SETTINGS';
export const SET_ALL_PERMISSIONS = 'SET_ALL_PERMISSIONS';

View File

@ -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 (
<ActionSheet
ref={o => this.ActionSheet = o}
title='Messages actions'
options={this.options}
cancelButtonIndex={this.CANCEL_INDEX}
destructiveButtonIndex={this.DELETE_INDEX}
onPress={this.handleActionPress}
/>
);
}
}

View File

@ -6,7 +6,7 @@ import ImagePicker from 'react-native-image-picker';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { userTyping } from '../actions/room'; import { userTyping } from '../actions/room';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { editRequest } from '../actions/messages'; import { editRequest, editCancel, clearInput } from '../actions/messages';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
textBox: { textBox: {
@ -25,7 +25,7 @@ const styles = StyleSheet.create({
alignSelf: 'stretch', alignSelf: 'stretch',
flexGrow: 1 flexGrow: 1
}, },
fileButton: { actionButtons: {
color: '#aaa', color: '#aaa',
paddingTop: 10, paddingTop: 10,
paddingBottom: 10, paddingBottom: 10,
@ -40,23 +40,29 @@ const styles = StyleSheet.create({
message: state.messages.message, message: state.messages.message,
editing: state.messages.editing editing: state.messages.editing
}), dispatch => ({ }), dispatch => ({
editCancel: () => dispatch(editCancel()),
editRequest: message => dispatch(editRequest(message)), 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 { export default class MessageBox extends React.Component {
static propTypes = { static propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,
editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired, editRequest: PropTypes.func.isRequired,
message: PropTypes.object, message: PropTypes.object,
editing: PropTypes.bool, editing: PropTypes.bool,
typing: PropTypes.func typing: PropTypes.func,
clearInput: PropTypes.func
} }
componentWillReceiveProps(nextProps) { 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.setNativeProps({ text: nextProps.message.msg });
this.component.focus(); 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 // if is submiting a new message
this.props.onSubmit(message); this.props.onSubmit(message);
} }
this.props.clearInput();
} }
addFile = () => { 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 <Icon style={styles.actionButtons} name='close' onPress={() => this.editCancel()} />;
}
return <Icon style={styles.actionButtons} name='add-circle-outline' onPress={this.addFile} />;
}
render() { render() {
return ( return (
<View style={[styles.textBox, (this.props.editing ? styles.editing : null)]}> <View style={[styles.textBox, (this.props.editing ? styles.editing : null)]}>
<SafeAreaView style={styles.safeAreaView}> <SafeAreaView style={styles.safeAreaView}>
{this.renderLeftButton()}
<TextInput <TextInput
ref={component => this.component = component} ref={component => this.component = component}
style={styles.textBoxInput} style={styles.textBoxInput}
@ -119,7 +140,6 @@ export default class MessageBox extends React.Component {
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
defaultValue='' defaultValue=''
/> />
<Icon style={styles.fileButton} name='add-circle-outline' onPress={this.addFile} />
</SafeAreaView> </SafeAreaView>
</View> </View>
); );

View File

@ -31,7 +31,7 @@ const styles = StyleSheet.create({
} }
}); });
export default class Message extends React.PureComponent { export default class User extends React.PureComponent {
static propTypes = { static propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,

View File

@ -1,28 +1,14 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { emojify } from 'react-emojione';
import Markdown from 'react-native-easy-markdown'; // eslint-disable-line import Markdown from 'react-native-easy-markdown'; // eslint-disable-line
import ActionSheet from 'react-native-actionsheet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { actionsShow } from '../../actions/messages';
import Card from './Card'; import Card from './Card';
import User from './User'; import User from './User';
import Avatar from '../Avatar'; 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({ const styles = StyleSheet.create({
content: { content: {
@ -47,70 +33,23 @@ const styles = StyleSheet.create({
@connect(state => ({ @connect(state => ({
message: state.messages.message, message: state.messages.message,
permalink: state.messages.permalink, editing: state.messages.editing
user: state.login.user
}), dispatch => ({ }), dispatch => ({
deleteRequest: message => dispatch(deleteRequest(message)), actionsShow: actionMessage => dispatch(actionsShow(actionMessage))
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))
})) }))
export default class Message extends React.Component { export default class Message extends React.Component {
static propTypes = { static propTypes = {
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
Message_TimeFormat: PropTypes.string.isRequired, Message_TimeFormat: PropTypes.string.isRequired,
deleteRequest: PropTypes.func.isRequired, message: PropTypes.object.isRequired,
editInit: PropTypes.func.isRequired, editing: PropTypes.bool,
starRequest: PropTypes.func.isRequired, actionsShow: PropTypes.func
permalinkRequest: PropTypes.func.isRequired,
togglePinRequest: PropTypes.func.isRequired,
setInput: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
message: PropTypes.object,
permalink: PropTypes.string
} }
constructor(props) { onLongPress() {
super(props); const { item } = this.props;
this.state = { this.props.actionsShow(JSON.parse(JSON.stringify(item)));
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 });
}
}
} }
isDeleted() { isDeleted() {
@ -125,90 +64,6 @@ export default class Message extends React.Component {
) : null; ) : 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() { renderMessageContent() {
if (this.isDeleted()) { if (this.isDeleted()) {
return <Text style={styles.textInfo}>Message removed</Text>; return <Text style={styles.textInfo}>Message removed</Text>;
@ -223,7 +78,9 @@ export default class Message extends React.Component {
} }
render() { render() {
const { item } = this.props; const {
item, message, editing
} = this.props;
const extraStyle = {}; const extraStyle = {};
if (item.temp) { if (item.temp) {
@ -231,15 +88,14 @@ export default class Message extends React.Component {
} }
const username = item.alias || item.u.username; const username = item.alias || item.u.username;
const isEditing = this.props.message._id === item._id; const isEditing = message._id === item._id && editing;
return ( return (
<TouchableOpacity <TouchableOpacity
onLongPress={() => this.showActions()} onLongPress={() => this.onLongPress()}
disabled={this.isDeleted()} disabled={this.isDeleted()}
style={isEditing ? styles.editing : null} style={[styles.message, extraStyle, isEditing ? styles.editing : null]}
> >
<View style={[styles.message, extraStyle]}>
<Avatar <Avatar
style={{ marginRight: 10 }} style={{ marginRight: 10 }}
text={item.avatar ? '' : username} text={item.avatar ? '' : username}
@ -257,15 +113,6 @@ export default class Message extends React.Component {
{this.attachments()} {this.attachments()}
{this.renderMessageContent(item)} {this.renderMessageContent(item)}
</View> </View>
<ActionSheet
ref={o => this.ActionSheet = o}
title={title}
options={options}
cancelButtonIndex={CANCEL_INDEX}
destructiveButtonIndex={DESTRUCTIVE_INDEX}
onPress={this.handleActionPress}
/>
</View>
</TouchableOpacity> </TouchableOpacity>
); );
} }

View File

@ -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 = { const roomsSchema = {
name: 'rooms', name: 'rooms',
primaryKey: '_id', primaryKey: '_id',
@ -35,6 +53,13 @@ const roomsSchema = {
} }
}; };
const subscriptionRolesSchema = {
name: 'subscriptionRolesSchema',
properties: {
value: 'string'
}
};
const subscriptionSchema = { const subscriptionSchema = {
name: 'subscriptions', name: 'subscriptions',
primaryKey: '_id', primaryKey: '_id',
@ -50,7 +75,7 @@ const subscriptionSchema = {
rid: 'string', rid: 'string',
open: { type: 'bool', optional: true }, open: { type: 'bool', optional: true },
alert: { type: 'bool', optional: true }, alert: { type: 'bool', optional: true },
// roles: [ 'owner' ], roles: { type: 'list', objectType: 'subscriptionRolesSchema' },
unread: { type: 'int', optional: true }, unread: { type: 'int', optional: true },
userMentions: { type: 'int', optional: true }, userMentions: { type: 'int', optional: true },
// userMentions: 0, // userMentions: 0,
@ -128,11 +153,14 @@ const realm = new Realm({
settingsSchema, settingsSchema,
serversSchema, serversSchema,
subscriptionSchema, subscriptionSchema,
subscriptionRolesSchema,
messagesSchema, messagesSchema,
usersSchema, usersSchema,
roomsSchema, roomsSchema,
attachment, attachment,
messagesEditedBySchema messagesEditedBySchema,
permissionsSchema,
permissionsRolesSchema
], ],
deleteRealmIfMigrationNeeded: true deleteRealmIfMigrationNeeded: true
}); });

View File

@ -70,7 +70,7 @@ const RocketChat = {
message.temp = false; message.temp = false;
message._server = server; message._server = server;
message.attachments = message.attachments || []; message.attachments = message.attachments || [];
message.starred = !!message.starred; message.starred = message.starred && message.starred.length > 0;
realm.create('messages', message, true); realm.create('messages', message, true);
}); });
} }
@ -85,6 +85,9 @@ const RocketChat = {
const [type, data] = ddpMessage.fields.args; const [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/'); const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) { if (/subscriptions/.test(ev)) {
if (data.roles) {
data.roles = data.roles.map(role => ({ value: role }));
}
realm.write(() => { realm.write(() => {
realm.create('subscriptions', data, true); realm.create('subscriptions', data, true);
}); });
@ -98,6 +101,7 @@ const RocketChat = {
} }
}); });
RocketChat.getSettings(); RocketChat.getSettings();
RocketChat.getPermissions();
}); });
}) })
.catch(e => console.error(e)); .catch(e => console.error(e));
@ -136,6 +140,17 @@ const RocketChat = {
}).then(response => response.json()); }).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 }) { register({ credentials }) {
return call('registerUser', credentials); return call('registerUser', credentials);
}, },
@ -385,6 +400,9 @@ const RocketChat = {
if (room) { if (room) {
subscription.roomUpdatedAt = room._updatedAt; subscription.roomUpdatedAt = room._updatedAt;
} }
if (subscription.roles) {
subscription.roles = subscription.roles.map(role => ({ value: role }));
}
subscription._server = { id: server.server }; subscription._server = { id: server.server };
return subscription; return subscription;
}); });
@ -413,7 +431,8 @@ const RocketChat = {
reduxStore.dispatch(actions.setAllSettings(RocketChat.parseSettings(filteredSettings))); reduxStore.dispatch(actions.setAllSettings(RocketChat.parseSettings(filteredSettings)));
}, },
parseSettings: settings => settings.reduce((ret, item) => { 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; return ret;
}, {}), }, {}),
_prepareSettings(settings) { _prepareSettings(settings) {
@ -423,6 +442,26 @@ const RocketChat = {
}); });
}, },
_filterSettings: settings => settings.filter(setting => settingsType[setting.type] && setting.value), _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) { deleteMessage(message) {
return call('deleteMessage', { _id: message._id }); return call('deleteMessage', { _id: message._id });
}, },
@ -430,7 +469,7 @@ const RocketChat = {
const { _id, msg, rid } = message; const { _id, msg, rid } = message;
return call('updateMessage', { _id, msg, rid }); return call('updateMessage', { _id, msg, rid });
}, },
starMessage(message) { toggleStarMessage(message) {
return call('starMessage', { _id: message._id, rid: message.rid, starred: !message.starred }); return call('starMessage', { _id: message._id, rid: message.rid, starred: !message.starred });
}, },
togglePinMessage(message) { togglePinMessage(message) {

View File

@ -8,8 +8,8 @@ import server from './server';
import navigator from './navigator'; import navigator from './navigator';
import createChannel from './createChannel'; import createChannel from './createChannel';
import app from './app'; import app from './app';
import permissions from './permissions';
export default combineReducers({ export default combineReducers({
settings, login, meteor, messages, server, navigator, createChannel, app, room settings, login, meteor, messages, server, navigator, createChannel, app, room, permissions
}); });

View File

@ -4,8 +4,10 @@ const initialState = {
isFetching: false, isFetching: false,
failure: false, failure: false,
message: {}, message: {},
actionMessage: {},
editing: false, editing: false,
permalink: '' permalink: '',
showActions: false
}; };
export default function messages(state = initialState, action) { export default function messages(state = initialState, action) {
@ -27,12 +29,29 @@ export default function messages(state = initialState, action) {
failure: true, failure: true,
errorMessage: action.err 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: case types.MESSAGES.EDIT_INIT:
return { return {
...state, ...state,
message: action.message, message: action.message,
editing: true editing: true
}; };
case types.MESSAGES.EDIT_CANCEL:
return {
...state,
message: {},
editing: false
};
case types.MESSAGES.EDIT_SUCCESS: case types.MESSAGES.EDIT_SUCCESS:
return { return {
...state, ...state,
@ -50,13 +69,21 @@ export default function messages(state = initialState, action) {
...state, ...state,
permalink: action.permalink permalink: action.permalink
}; };
case types.MESSAGES.PERMALINK_CLEAR:
return {
...state,
permalink: ''
};
case types.MESSAGES.SET_INPUT: case types.MESSAGES.SET_INPUT:
return { return {
...state, ...state,
message: action.message message: action.message
}; };
// case types.LOGOUT: case types.MESSAGES.CLEAR_INPUT:
// return initialState; return {
...state,
message: {}
};
default: default:
return state; return state;
} }

View File

@ -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;
}

View File

@ -18,8 +18,10 @@ const restore = function* restore() {
const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer');
if (currentServer) { if (currentServer) {
yield put(setServer(currentServer)); yield put(setServer(currentServer));
const tmp = realm.objects('settings'); const settings = realm.objects('settings');
yield put(actions.setAllSettings(RocketChat.parseSettings(tmp.slice(0, tmp.length)))); 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({})); yield put(actions.appReady({}));
} catch (e) { } catch (e) {

View File

@ -26,6 +26,7 @@ const setUsernameCall = args => RocketChat.setUsername(args);
const logoutCall = args => RocketChat.logout(args); const logoutCall = args => RocketChat.logout(args);
const meCall = args => RocketChat.me(args); const meCall = args => RocketChat.me(args);
const forgotPasswordCall = args => RocketChat.forgotPassword(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args);
const userInfoCall = args => RocketChat.userInfo(args);
const getToken = function* getToken() { const getToken = function* getToken() {
const currentServer = yield select(getServer); const currentServer = yield select(getServer);
@ -76,6 +77,10 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) {
// if user has username // if user has username
if (me.username) { if (me.username) {
user.username = 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 { } else {
yield put(registerIncomplete()); yield put(registerIncomplete());
} }

View File

@ -7,8 +7,8 @@ import {
deleteFailure, deleteFailure,
editSuccess, editSuccess,
editFailure, editFailure,
starSuccess, toggleStarSuccess,
starFailure, toggleStarFailure,
permalinkSuccess, permalinkSuccess,
permalinkFailure, permalinkFailure,
togglePinSuccess, togglePinSuccess,
@ -18,7 +18,7 @@ import RocketChat from '../lib/rocketchat';
const deleteMessage = message => RocketChat.deleteMessage(message); const deleteMessage = message => RocketChat.deleteMessage(message);
const editMessage = message => RocketChat.editMessage(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 getPermalink = message => RocketChat.getPermalink(message);
const togglePinMessage = message => RocketChat.togglePinMessage(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 { try {
yield call(starMessage, message); yield call(toggleStarMessage, message);
yield put(starSuccess()); yield put(toggleStarSuccess());
} catch (error) { } catch (error) {
yield put(starFailure()); yield put(toggleStarFailure());
} }
}; };
@ -85,7 +85,7 @@ const root = function* root() {
yield takeLatest(MESSAGES.REQUEST, get); yield takeLatest(MESSAGES.REQUEST, get);
yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest); yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest); 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.PERMALINK_REQUEST, handlePermalinkRequest);
yield takeLatest(MESSAGES.TOGGLE_PIN_REQUEST, handleTogglePinRequest); yield takeLatest(MESSAGES.TOGGLE_PIN_REQUEST, handleTogglePinRequest);
}; };

View File

@ -7,9 +7,11 @@ import { bindActionCreators } from 'redux';
import * as actions from '../actions'; import * as actions from '../actions';
import { openRoom } from '../actions/room'; import { openRoom } from '../actions/room';
import { editCancel } from '../actions/messages';
import realm from '../lib/realm'; import realm from '../lib/realm';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import Message from '../containers/message'; import Message from '../containers/message';
import MessageActions from '../containers/MessageActions';
import MessageBox from '../containers/MessageBox'; import MessageBox from '../containers/MessageBox';
import Typing from '../containers/Typing'; import Typing from '../containers/Typing';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
@ -57,13 +59,15 @@ const typing = () => <Typing />;
}), }),
dispatch => ({ dispatch => ({
actions: bindActionCreators(actions, 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 { export default class RoomView extends React.Component {
static propTypes = { static propTypes = {
navigation: PropTypes.object.isRequired, navigation: PropTypes.object.isRequired,
openRoom: PropTypes.func.isRequired, openRoom: PropTypes.func.isRequired,
editCancel: PropTypes.func,
rid: PropTypes.string, rid: PropTypes.string,
server: PropTypes.string, server: PropTypes.string,
sid: PropTypes.string, sid: PropTypes.string,
@ -86,6 +90,7 @@ export default class RoomView extends React.Component {
.objects('messages') .objects('messages')
.filtered('_server.id = $0 AND rid = $1', this.props.server, this.rid) .filtered('_server.id = $0 AND rid = $1', this.props.server, this.rid)
.sorted('ts', true); .sorted('ts', true);
this.room = realm.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
slow: false, slow: false,
dataSource: ds.cloneWithRows([]), dataSource: ds.cloneWithRows([]),
@ -114,6 +119,7 @@ export default class RoomView extends React.Component {
componentWillUnmount() { componentWillUnmount() {
clearTimeout(this.timer); clearTimeout(this.timer);
this.data.removeAllListeners(); this.data.removeAllListeners();
this.props.editCancel();
} }
onEndReached = () => { onEndReached = () => {
@ -210,6 +216,7 @@ export default class RoomView extends React.Component {
/> />
</SafeAreaView> </SafeAreaView>
{this.renderFooter()} {this.renderFooter()}
<MessageActions room={this.room} />
</KeyboardView> </KeyboardView>
); );
} }