diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 985fde88b..f2d87dd23 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -71,7 +71,8 @@ export const MESSAGES = createRequestTypes('MESSAGES', [
'TOGGLE_PIN_FAILURE',
'SET_INPUT',
'CLEAR_INPUT',
- 'TOGGLE_REACTION_PICKER'
+ 'TOGGLE_REACTION_PICKER',
+ 'REPLY_BROADCAST'
]);
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
export const SELECTED_USERS = createRequestTypes('SELECTED_USERS', ['ADD_USER', 'REMOVE_USER', 'RESET', 'SET_LOADING']);
diff --git a/app/actions/messages.js b/app/actions/messages.js
index cfedf3004..edaaefb17 100644
--- a/app/actions/messages.js
+++ b/app/actions/messages.js
@@ -183,3 +183,10 @@ export function toggleReactionPicker(message) {
message
};
}
+
+export function replyBroadcast(message) {
+ return {
+ type: types.MESSAGES.REPLY_BROADCAST,
+ message
+ };
+}
diff --git a/app/containers/RoomTypeIcon.js b/app/containers/RoomTypeIcon.js
index 4450594cc..269d42940 100644
--- a/app/containers/RoomTypeIcon.js
+++ b/app/containers/RoomTypeIcon.js
@@ -11,6 +11,10 @@ const styles = StyleSheet.create({
});
const RoomTypeIcon = ({ type, size }) => {
+ if (!type) {
+ return null;
+ }
+
const icon = {
c: 'pound',
p: 'lock',
@@ -21,7 +25,7 @@ const RoomTypeIcon = ({ type, size }) => {
};
RoomTypeIcon.propTypes = {
- type: PropTypes.string.isRequired,
+ type: PropTypes.string,
size: PropTypes.number
};
diff --git a/app/containers/message/index.js b/app/containers/message/index.js
index 4254a4cfd..3aac53d5f 100644
--- a/app/containers/message/index.js
+++ b/app/containers/message/index.js
@@ -18,10 +18,28 @@ import Reply from './Reply';
import ReactionsModal from './ReactionsModal';
import Emoji from './Emoji';
import styles from './styles';
-import { actionsShow, errorActionsShow, toggleReactionPicker } from '../../actions/messages';
+import { actionsShow, errorActionsShow, toggleReactionPicker, replyBroadcast } from '../../actions/messages';
import messagesStatus from '../../constants/messagesStatus';
import Touch from '../../utils/touch';
+const SYSTEM_MESSAGES = [
+ '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'
+];
+
const getInfoMessage = ({
t, role, msg, u
}) => {
@@ -68,7 +86,8 @@ const getInfoMessage = ({
}), dispatch => ({
actionsShow: actionMessage => dispatch(actionsShow(actionMessage)),
errorActionsShow: actionMessage => dispatch(errorActionsShow(actionMessage)),
- toggleReactionPicker: message => dispatch(toggleReactionPicker(message))
+ toggleReactionPicker: message => dispatch(toggleReactionPicker(message)),
+ replyBroadcast: message => dispatch(replyBroadcast(message))
}))
export default class Message extends React.Component {
static propTypes = {
@@ -83,17 +102,20 @@ export default class Message extends React.Component {
editing: PropTypes.bool,
errorActionsShow: PropTypes.func,
toggleReactionPicker: PropTypes.func,
+ replyBroadcast: PropTypes.func,
onReactionPress: PropTypes.func,
style: ViewPropTypes.style,
onLongPress: PropTypes.func,
_updatedAt: PropTypes.instanceOf(Date),
- archived: PropTypes.bool
+ archived: PropTypes.bool,
+ broadcast: PropTypes.bool
}
static defaultProps = {
onLongPress: () => {},
_updatedAt: new Date(),
- archived: false
+ archived: false,
+ broadcast: false
}
constructor(props) {
@@ -116,6 +138,9 @@ export default class Message extends React.Component {
if (!equal(this.props.reactions, nextProps.reactions)) {
return true;
}
+ if (this.props.broadcast !== nextProps.broadcast) {
+ return true;
+ }
return this.props._updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
}
@@ -150,25 +175,11 @@ export default class Message extends React.Component {
parseMessage = () => JSON.parse(JSON.stringify(this.props.item));
isInfoMessage() {
- return [
- '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);
+ return SYSTEM_MESSAGES.includes(this.props.item.t);
}
+ isOwn = () => this.props.item.u && this.props.item.u._id === this.props.user.id;
+
isDeleted() {
return this.props.item.t === 'rm';
}
@@ -187,7 +198,7 @@ export default class Message extends React.Component {
if (previousItem && (
(previousItem.ts.toDateString() === item.ts.toDateString()) &&
(previousItem.u.username === item.u.username) &&
- !(previousItem.groupable === false || item.groupable === false) &&
+ !(previousItem.groupable === false || item.groupable === false || this.props.broadcast === true) &&
(previousItem.status === item.status) &&
(item.ts - previousItem.ts < this.props.Message_GroupingPeriod * 1000)
)) {
@@ -303,6 +314,20 @@ export default class Message extends React.Component {
);
}
+ renderBroadcastReply() {
+ if (!this.props.broadcast || this.isOwn()) {
+ return null;
+ }
+ return (
+ this.props.replyBroadcast(this.parseMessage())}
+ >
+ Reply
+
+ );
+ }
+
render() {
const {
item, message, editing, style, archived
@@ -329,6 +354,7 @@ export default class Message extends React.Component {
{this.renderAttachment()}
{this.renderUrl()}
{this.renderReactions()}
+ {this.renderBroadcastReply()}
{this.state.reactionsModal &&
diff --git a/app/containers/message/styles.js b/app/containers/message/styles.js
index 45a7a8f45..8421b09e6 100644
--- a/app/containers/message/styles.js
+++ b/app/containers/message/styles.js
@@ -84,5 +84,18 @@ export default StyleSheet.create({
padding: 10,
paddingRight: 12,
paddingLeft: 0
+ },
+ broadcastButton: {
+ borderColor: '#1d74f5',
+ borderWidth: 2,
+ borderRadius: 2,
+ paddingVertical: 10,
+ width: 100,
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 6
+ },
+ broadcastButtonText: {
+ color: '#1d74f5'
}
});
diff --git a/app/lib/methods/helpers/mergeSubscriptionsRooms.js b/app/lib/methods/helpers/mergeSubscriptionsRooms.js
index 4289fe5de..e5d4340d4 100644
--- a/app/lib/methods/helpers/mergeSubscriptionsRooms.js
+++ b/app/lib/methods/helpers/mergeSubscriptionsRooms.js
@@ -4,6 +4,9 @@ import normalizeMessage from './normalizeMessage';
export const merge = (subscription, room) => {
subscription.muted = [];
if (room) {
+ if (room.rid) {
+ subscription.rid = room.rid;
+ }
subscription.roomUpdatedAt = room._updatedAt;
subscription.lastMessage = normalizeMessage(room.lastMessage);
subscription.ro = room.ro;
@@ -13,6 +16,7 @@ export const merge = (subscription, room) => {
subscription.reactWhenReadOnly = room.reactWhenReadOnly;
subscription.archived = room.archived;
subscription.joinCodeRequired = room.joinCodeRequired;
+ subscription.broadcast = room.broadcast;
if (room.muted && room.muted.length) {
subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
@@ -28,7 +32,8 @@ export const merge = (subscription, room) => {
subscription.notifications = false;
}
- subscription.blocked = !!subscription.blocker;
+ subscription.blocker = !!subscription.blocker;
+ subscription.blocked = !!subscription.blocked;
return subscription;
};
diff --git a/app/lib/methods/subscriptions/rooms.js b/app/lib/methods/subscriptions/rooms.js
index 279eb1c4f..cf7270dd0 100644
--- a/app/lib/methods/subscriptions/rooms.js
+++ b/app/lib/methods/subscriptions/rooms.js
@@ -51,16 +51,24 @@ export default async function subscribeRooms(id) {
const [type, data] = ddpMessage.fields.args;
const [, ev] = ddpMessage.fields.eventName.split('/');
if (/subscriptions/.test(ev)) {
- const tpm = merge(data);
+ const rooms = database.objects('rooms').filtered('_id == $0', data.rid);
+ const tpm = merge(data, rooms[0]);
database.write(() => {
database.create('subscriptions', tpm, true);
+ database.delete(rooms);
});
}
- if (/rooms/.test(ev) && type === 'updated') {
- const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id);
- database.write(() => {
- merge(sub, data);
- });
+ if (/rooms/.test(ev)) {
+ if (type === 'updated') {
+ const [sub] = database.objects('subscriptions').filtered('rid == $0', data._id);
+ database.write(() => {
+ merge(sub, data);
+ });
+ } else if (type === 'inserted') {
+ database.write(() => {
+ database.create('rooms', data, true);
+ });
+ }
}
if (/message/.test(ev)) {
const [args] = ddpMessage.fields.args;
diff --git a/app/lib/realm.js b/app/lib/realm.js
index bfe84ed59..1d8aedde8 100644
--- a/app/lib/realm.js
+++ b/app/lib/realm.js
@@ -49,10 +49,7 @@ const roomsSchema = {
primaryKey: '_id',
properties: {
_id: 'string',
- t: 'string',
- lastMessage: 'messages',
- description: { type: 'string', optional: true },
- _updatedAt: { type: 'date', optional: true }
+ broadcast: { type: 'bool', optional: true }
}
};
@@ -97,11 +94,13 @@ const subscriptionSchema = {
announcement: { type: 'string', optional: true },
topic: { type: 'string', optional: true },
blocked: { type: 'bool', optional: true },
+ blocker: { type: 'bool', optional: true },
reactWhenReadOnly: { type: 'bool', optional: true },
archived: { type: 'bool', optional: true },
joinCodeRequired: { type: 'bool', optional: true },
notifications: { type: 'bool', optional: true },
- muted: { type: 'list', objectType: 'usersMuted' }
+ muted: { type: 'list', objectType: 'usersMuted' },
+ broadcast: { type: 'bool', optional: true }
}
};
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 8605c747c..e27dc77a9 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -49,8 +49,10 @@ const RocketChat = {
subscribeRooms,
subscribeRoom,
canOpenRoom,
- createChannel({ name, users, type }) {
- return call(type ? 'createChannel' : 'createPrivateGroup', name, users, type);
+ createChannel({
+ name, users, type, readOnly, broadcast
+ }) {
+ return call(type ? 'createPrivateGroup' : 'createChannel', name, users, readOnly, {}, { broadcast });
},
async createDirectMessageAndWait(username) {
const room = await RocketChat.createDirectMessage(username);
diff --git a/app/sagas/messages.js b/app/sagas/messages.js
index 7db85f934..0585e833b 100644
--- a/app/sagas/messages.js
+++ b/app/sagas/messages.js
@@ -1,4 +1,5 @@
-import { takeLatest, put, call } from 'redux-saga/effects';
+import { delay } from 'redux-saga';
+import { takeLatest, put, call, select } from 'redux-saga/effects';
import { MESSAGES } from '../actions/actionsTypes';
import {
messagesSuccess,
@@ -12,9 +13,13 @@ import {
permalinkSuccess,
permalinkFailure,
togglePinSuccess,
- togglePinFailure
+ togglePinFailure,
+ setInput
} from '../actions/messages';
import RocketChat from '../lib/rocketchat';
+import database from '../lib/realm';
+import { goRoom } from '../containers/routes/NavigationService';
+import log from '../utils/log';
const deleteMessage = message => RocketChat.deleteMessage(message);
const editMessage = message => RocketChat.editMessage(message);
@@ -81,6 +86,25 @@ const handleTogglePinRequest = function* handleTogglePinRequest({ message }) {
}
};
+const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
+ try {
+ const { username } = message.u;
+ const subscriptions = database.objects('subscriptions').filtered('name = $0', username);
+ if (subscriptions.length) {
+ goRoom({ rid: subscriptions[0].rid, name: subscriptions[0].name });
+ } else {
+ const room = yield RocketChat.createDirectMessage(username);
+ goRoom({ rid: room.rid, name: username });
+ }
+ yield delay(100);
+ const server = yield select(state => state.server.server);
+ const msg = `[ ](${ server }/direct/${ username }?msg=${ message._id })`;
+ yield put(setInput({ msg }));
+ } catch (e) {
+ log('handleReplyBroadcast', e);
+ }
+};
+
const root = function* root() {
yield takeLatest(MESSAGES.REQUEST, get);
yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
@@ -88,5 +112,6 @@ const root = function* root() {
yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest);
yield takeLatest(MESSAGES.PERMALINK_REQUEST, handlePermalinkRequest);
yield takeLatest(MESSAGES.TOGGLE_PIN_REQUEST, handleTogglePinRequest);
+ yield takeLatest(MESSAGES.REPLY_BROADCAST, handleReplyBroadcast);
};
export default root;
diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js
index 35253d467..ed221fe3e 100644
--- a/app/views/CreateChannelView.js
+++ b/app/views/CreateChannelView.js
@@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import { View, Text, Switch, TouchableOpacity, SafeAreaView, ScrollView } from 'react-native';
+import { View, Text, Switch, SafeAreaView, ScrollView, Platform } from 'react-native';
import RCTextInput from '../containers/TextInput';
import Loading from '../containers/Loading';
@@ -10,6 +10,7 @@ import { createChannelRequest } from '../actions/createChannel';
import styles from './Styles';
import KeyboardView from '../presentation/KeyboardView';
import scrollPersistTaps from '../utils/scrollPersistTaps';
+import Button from '../containers/Button';
@connect(
state => ({
@@ -35,22 +36,28 @@ export default class CreateChannelView extends LoggedView {
super('CreateChannelView', props);
this.state = {
channelName: '',
- type: true
+ type: true,
+ readOnly: false,
+ broadcast: false
};
}
- submit() {
+ submit = () => {
if (!this.state.channelName.trim() || this.props.createChannel.isFetching) {
return;
}
- const { channelName, type = true } = this.state;
+ const {
+ channelName, type, readOnly, broadcast
+ } = this.state;
let { users } = this.props;
// transform users object into array of usernames
users = users.map(user => user.name);
// create channel
- this.props.create({ name: channelName, users, type });
+ this.props.create({
+ name: channelName, users, type, readOnly, broadcast
+ });
}
renderChannelNameError() {
@@ -68,20 +75,62 @@ export default class CreateChannelView extends LoggedView {
);
}
- renderTypeSwitch() {
- return (
-
+ renderSwitch = ({
+ id, value, label, description, onValueChange, disabled = false
+ }) => (
+
+
this.setState({ type })}
- testID='create-channel-type'
+ value={value}
+ onValueChange={onValueChange}
+ testID={`create-channel-${ id }`}
+ onTintColor='#2de0a5'
+ tintColor={Platform.OS === 'android' ? '#f5455c' : null}
+ disabled={disabled}
/>
-
- {this.state.type ? 'Public' : 'Private'}
-
+ {label}
- );
+ {description}
+
+ );
+
+ renderType() {
+ const { type } = this.state;
+ return this.renderSwitch({
+ id: 'type',
+ value: type,
+ label: type ? 'Private Channel' : 'Public Channel',
+ description: type ? 'Just invited people can access this channel' : 'Everyone can access this channel',
+ onValueChange: value => this.setState({ type: value })
+ });
+ }
+
+ renderReadOnly() {
+ const { readOnly, broadcast } = this.state;
+ return this.renderSwitch({
+ id: 'readonly',
+ value: readOnly,
+ label: 'Read Only Channel',
+ description: readOnly ? 'Only authorized users can write new messages' : 'All users in the channel can write new messages',
+ onValueChange: value => this.setState({ readOnly: value }),
+ disabled: broadcast
+ });
+ }
+
+ renderBroadcast() {
+ const { broadcast, readOnly } = this.state;
+ return this.renderSwitch({
+ id: 'broadcast',
+ value: broadcast,
+ label: 'Broadcast Channel',
+ description: 'Only authorized users can write new messages, but the other users will be able to reply',
+ onValueChange: (value) => {
+ this.setState({
+ broadcast: value,
+ readOnly: value ? true : readOnly
+ });
+ }
+ });
}
render() {
@@ -102,36 +151,18 @@ export default class CreateChannelView extends LoggedView {
testID='create-channel-name'
/>
{this.renderChannelNameError()}
- {this.renderTypeSwitch()}
-
- {this.state.type ? (
- 'Everyone can access this channel'
- ) : (
- 'Just invited people can access this channel'
- )}
-
- this.submit()}
- style={[
- styles.buttonContainer_white,
- this.state.channelName.length === 0 || this.props.createChannel.isFetching
- ? styles.disabledButton
- : styles.enabledButton
- ]}
- testID='create-channel-submit'
- >
- CREATE
-
+ {this.renderType()}
+ {this.renderReadOnly()}
+ {this.renderBroadcast()}
+
+
+
diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js
index 093154be8..02b31881d 100644
--- a/app/views/RoomActionsView/index.js
+++ b/app/views/RoomActionsView/index.js
@@ -71,6 +71,11 @@ export default class RoomActionsView extends LoggedView {
updateRoomMembers = async() => {
const { t } = this.state.room;
+
+ if (!this.canViewMembers) {
+ return {};
+ }
+
if (t === 'c' || t === 'p') {
let onlineMembers = [];
let allMembers = [];
@@ -123,9 +128,20 @@ export default class RoomActionsView extends LoggedView {
}
return false;
}
+ get canViewMembers() {
+ const { rid, t, broadcast } = this.state.room;
+ if (broadcast) {
+ const viewBroadcastMemberListPermission = 'view-broadcast-member-list';
+ const permissions = RocketChat.hasPermission([viewBroadcastMemberListPermission], rid);
+ if (!permissions[viewBroadcastMemberListPermission]) {
+ return false;
+ }
+ }
+ return (t === 'c' || t === 'p');
+ }
get sections() {
const {
- rid, t, blocked, notifications
+ rid, t, blocker, notifications
} = this.room;
const { onlineMembers } = this.state;
@@ -219,7 +235,7 @@ export default class RoomActionsView extends LoggedView {
data: [
{
icon: 'block',
- name: `${ blocked ? 'Unblock' : 'Block' } user`,
+ name: `${ blocker ? 'Unblock' : 'Block' } user`,
type: 'danger',
event: () => this.toggleBlockUser(),
testID: 'room-actions-block-user'
@@ -228,14 +244,18 @@ export default class RoomActionsView extends LoggedView {
renderItem: this.renderItem
});
} else if (t === 'c' || t === 'p') {
- const actions = [{
- icon: 'ios-people',
- name: 'Members',
- description: (onlineMembers.length === 1 ? `${ onlineMembers.length } member` : `${ onlineMembers.length } members`),
- route: 'RoomMembers',
- params: { rid, members: onlineMembers },
- testID: 'room-actions-members'
- }];
+ const actions = [];
+
+ if (this.canViewMembers) {
+ actions.push({
+ icon: 'ios-people',
+ name: 'Members',
+ description: (onlineMembers.length === 1 ? `${ onlineMembers.length } member` : `${ onlineMembers.length } members`),
+ route: 'RoomMembers',
+ params: { rid, members: onlineMembers },
+ testID: 'room-actions-members'
+ });
+ }
if (this.canAddUser) {
actions.push({
@@ -276,10 +296,10 @@ export default class RoomActionsView extends LoggedView {
}
toggleBlockUser = async() => {
- const { rid, blocked } = this.state.room;
+ const { rid, blocker } = this.state.room;
const { member } = this.state;
try {
- RocketChat.toggleBlockUser(rid, member._id, !blocked);
+ RocketChat.toggleBlockUser(rid, member._id, !blocker);
} catch (e) {
log('toggleBlockUser', e);
}
diff --git a/app/views/RoomInfoEditView/index.js b/app/views/RoomInfoEditView/index.js
index 712c500e1..cd98cae9a 100644
--- a/app/views/RoomInfoEditView/index.js
+++ b/app/views/RoomInfoEditView/index.js
@@ -324,10 +324,10 @@ export default class RoomInfoEditView extends LoggedView {
rightLabelPrimary='Read Only'
rightLabelSecondary='Only authorized users can write new messages'
onValueChange={value => this.setState({ ro: value })}
- disabled={!this.permissions[PERMISSION_SET_READONLY]}
+ disabled={!this.permissions[PERMISSION_SET_READONLY] || room.broadcast}
testID='room-info-edit-view-ro'
/>
- {ro &&
+ {ro && !room.broadcast &&
}
+ {room.broadcast &&
+ [
+ Broadcast channel,
+
+ ]
+ }
str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
const getRoomTitle = room => (room.t === 'd' ?
- {room.fname} :
- [, {room.name}]
+ {room.fname} :
+ [
+ ,
+ {room.name}
+ ]
);
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@@ -184,6 +187,17 @@ export default class RoomInfoView extends LoggedView {
)
+ renderBroadcast = () => (
+
+ Broadcast Channel
+ Only authorized users can write new messages, but the other users will be able to reply
+
+
+ )
+
render() {
const { room, roomUser } = this.state;
if (!room) {
@@ -193,13 +207,14 @@ export default class RoomInfoView extends LoggedView {
{this.renderAvatar(room, roomUser)}
- { getRoomTitle(room) }
+ { 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)}
+ {room.broadcast && this.renderBroadcast()}
);
}
diff --git a/app/views/RoomInfoView/styles.js b/app/views/RoomInfoView/styles.js
index c333c4d92..fe9c0af71 100644
--- a/app/views/RoomInfoView/styles.js
+++ b/app/views/RoomInfoView/styles.js
@@ -29,11 +29,13 @@ export default StyleSheet.create({
avatar: {
marginHorizontal: 10
},
- roomTitle: {
- fontSize: 18,
+ roomTitleContainer: {
paddingTop: 20,
flexDirection: 'row'
},
+ roomTitle: {
+ fontSize: 18
+ },
roomDescription: {
fontSize: 14,
color: '#ccc',
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 28bfbde16..5a7d3c741 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -153,6 +153,22 @@ export default class RoomView extends LoggedView {
}
};
+ isOwner = () => this.state.room && this.state.room.roles && Array.from(Object.keys(this.state.room.roles), i => this.state.room.roles[i].value).includes('owner');
+
+ isMuted = () => this.state.room && this.state.room.muted && Array.from(Object.keys(this.state.room.muted), i => this.state.room.muted[i].value).includes(this.props.user.username);
+
+ isReadOnly = () => this.state.room.ro && this.isMuted() && !this.isOwner();
+
+ isBlocked = () => {
+ if (this.state.room) {
+ const { t, blocked, blocker } = this.state.room;
+ if (t === 'd' && (blocked || blocker)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
renderItem = (item, previousItem) => (
);
@@ -179,13 +196,20 @@ export default class RoomView extends LoggedView {
);
}
- if (this.state.room.ro || this.state.room.archived) {
+ if (this.state.room.archived || this.isReadOnly()) {
return (
This room is read only
);
}
+ if (this.isBlocked()) {
+ return (
+
+ This room is blocked
+
+ );
+ }
return ;
};
diff --git a/app/views/RoomView/styles.js b/app/views/RoomView/styles.js
index 936cc48e0..bd9e68d71 100644
--- a/app/views/RoomView/styles.js
+++ b/app/views/RoomView/styles.js
@@ -34,6 +34,9 @@ export default StyleSheet.create({
readOnly: {
padding: 10
},
+ blockedOrBlocker: {
+ padding: 10
+ },
reactionPickerContainer: {
// width: width - 20,
// height: width - 20,
diff --git a/app/views/Styles.js b/app/views/Styles.js
index 0e429e9aa..2a0d4d040 100644
--- a/app/views/Styles.js
+++ b/app/views/Styles.js
@@ -80,12 +80,18 @@ export default StyleSheet.create({
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
- paddingHorizontal: 0
+ paddingHorizontal: 0,
+ paddingBottom: 5
},
switchLabel: {
- flexGrow: 1,
+ fontSize: 16,
+ color: '#2f343d',
paddingHorizontal: 10
},
+ switchDescription: {
+ fontSize: 16,
+ color: '#9ea2a8'
+ },
disabledButton: {
backgroundColor: '#e1e5e8'
},
diff --git a/e2e/06-createroom.spec.js b/e2e/06-createroom.spec.js
index 6dc9f37a5..53a82eb2f 100644
--- a/e2e/06-createroom.spec.js
+++ b/e2e/06-createroom.spec.js
@@ -59,7 +59,9 @@ describe('Create room screen', () => {
await expect(element(by.id('create-channel-view'))).toBeVisible();
await expect(element(by.id('create-channel-name'))).toBeVisible();
await expect(element(by.id('create-channel-type'))).toBeVisible();
- await expect(element(by.id('create-channel-submit'))).toBeVisible();
+ await expect(element(by.id('create-channel-readonly'))).toBeVisible();
+ await expect(element(by.id('create-channel-broadcast'))).toExist();
+ await expect(element(by.id('create-channel-submit'))).toExist();
});
it('should get invalid room', async() => {
@@ -69,28 +71,9 @@ describe('Create room screen', () => {
await expect(element(by.id('create-channel-error'))).toBeVisible();
});
- it('should create private room', async() => {
- await element(by.id('create-channel-name')).replaceText(`private${ data.random }`);
- await element(by.id('create-channel-type')).tap();
- await element(by.id('create-channel-submit')).tap();
- await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
- await expect(element(by.id('room-view'))).toBeVisible();
- await waitFor(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`).withTimeout(60000);
- await expect(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`);
- await element(by.id('header-back')).tap();
- await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
- await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000);
- await expect(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible();
- });
-
it('should create public room', async() => {
- await element(by.id('rooms-list-view-create-channel')).tap();
- await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
- await element(by.id('select-users-view-item-rocket.cat')).tap();
- await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000);
- await element(by.id('selected-users-view-submit')).tap();
- await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000);
await element(by.id('create-channel-name')).replaceText(`public${ data.random }`);
+ await element(by.id('create-channel-type')).tap();
await element(by.id('create-channel-submit')).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view'))).toBeVisible();
@@ -102,6 +85,25 @@ describe('Create room screen', () => {
await expect(element(by.id(`rooms-list-view-item-public${ data.random }`))).toBeVisible();
});
+ it('should create private room', async() => {
+ await element(by.id('rooms-list-view-create-channel')).tap();
+ await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('select-users-view-item-rocket.cat')).tap();
+ await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(5000);
+ await element(by.id('selected-users-view-submit')).tap();
+ await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000);
+ await element(by.id('create-channel-name')).replaceText(`private${ data.random }`);
+ await element(by.id('create-channel-submit')).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
+ await expect(element(by.id('room-view'))).toBeVisible();
+ await waitFor(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`).withTimeout(60000);
+ await expect(element(by.id('room-view-title'))).toHaveText(`private${ data.random }`);
+ await element(by.id('header-back')).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.id(`rooms-list-view-item-private${ data.random }`))).toBeVisible();
+ });
+
afterEach(async() => {
takeScreenshot();
});
diff --git a/e2e/07-room.spec.js b/e2e/07-room.spec.js
index 4c5696bec..e3cb3874d 100644
--- a/e2e/07-room.spec.js
+++ b/e2e/07-room.spec.js
@@ -176,12 +176,6 @@ describe('Room screen', () => {
});
describe('Message', async() => {
- before(async() => {
- await mockMessage('reply');
- await mockMessage('edit');
- await mockMessage('quote');
- });
-
it('should show message actions', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
@@ -190,27 +184,6 @@ describe('Room screen', () => {
await waitFor(element(by.text('Cancel'))).toBeNotVisible().withTimeout(2000);
});
- it('should reply message', async() => {
- await element(by.text(`${ data.random }reply`)).longPress();
- await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
- await expect(element(by.text('Messages actions'))).toBeVisible();
- await element(by.text('Reply')).tap();
- await element(by.id('messagebox-input')).typeText('replied');
- await element(by.id('messagebox-send-message')).tap();
- // TODO: test if reply was sent
- });
-
- it('should edit message', async() => {
- await element(by.text(`${ data.random }edit`)).longPress();
- await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
- await expect(element(by.text('Messages actions'))).toBeVisible();
- await element(by.text('Edit')).tap();
- await element(by.id('messagebox-input')).typeText('ed');
- await element(by.id('messagebox-send-message')).tap();
- await waitFor(element(by.text(`${ data.random }edited`))).toBeVisible().withTimeout(60000);
- await expect(element(by.text(`${ data.random }edited`))).toBeVisible();
- });
-
it('should copy permalink', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
@@ -232,16 +205,6 @@ describe('Room screen', () => {
// TODO: test clipboard
});
- it('should quote message', async() => {
- await element(by.text(`${ data.random }quote`)).longPress();
- await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
- await expect(element(by.text('Messages actions'))).toBeVisible();
- await element(by.text('Quote')).tap();
- await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
- await element(by.id('messagebox-send-message')).tap();
- // TODO: test if quote was sent
- });
-
it('should star message', async() => {
await element(by.text(`${ data.random }message`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
@@ -288,6 +251,40 @@ describe('Room screen', () => {
await expect(element(by.id('message-reaction-:grinning:'))).toBeNotVisible();
});
+ it('should reply message', async() => {
+ await mockMessage('reply');
+ await element(by.text(`${ data.random }reply`)).longPress();
+ await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
+ await expect(element(by.text('Messages actions'))).toBeVisible();
+ await element(by.text('Reply')).tap();
+ await element(by.id('messagebox-input')).typeText('replied');
+ await element(by.id('messagebox-send-message')).tap();
+ // TODO: test if reply was sent
+ });
+
+ it('should edit message', async() => {
+ await mockMessage('edit');
+ await element(by.text(`${ data.random }edit`)).longPress();
+ await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
+ await expect(element(by.text('Messages actions'))).toBeVisible();
+ await element(by.text('Edit')).tap();
+ await element(by.id('messagebox-input')).typeText('ed');
+ await element(by.id('messagebox-send-message')).tap();
+ await waitFor(element(by.text(`${ data.random }edited`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text(`${ data.random }edited`))).toBeVisible();
+ });
+
+ it('should quote message', async() => {
+ await mockMessage('quote');
+ await element(by.text(`${ data.random }quote`)).longPress();
+ await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
+ await expect(element(by.text('Messages actions'))).toBeVisible();
+ await element(by.text('Quote')).tap();
+ await element(by.id('messagebox-input')).typeText(`${ data.random }quoted`);
+ await element(by.id('messagebox-send-message')).tap();
+ // TODO: test if quote was sent
+ });
+
it('should pin message', async() => {
await element(by.text(`${ data.random }edited`)).longPress();
await waitFor(element(by.text('Messages actions'))).toBeVisible().withTimeout(5000);
diff --git a/e2e/11-broadcast.spec.js b/e2e/11-broadcast.spec.js
new file mode 100644
index 000000000..5ae9344e7
--- /dev/null
+++ b/e2e/11-broadcast.spec.js
@@ -0,0 +1,101 @@
+const {
+ device, expect, element, by, waitFor
+} = require('detox');
+const { takeScreenshot } = require('./helpers/screenshot');
+const { logout, navigateToLogin } = require('./helpers/app');
+const data = require('./data');
+
+describe('Broadcast room', () => {
+ before(async() => {
+ await device.reloadReactNative();
+ });
+
+ it('should create broadcast room', async() => {
+ await element(by.id('rooms-list-view-create-channel')).tap();
+ await waitFor(element(by.id('select-users-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id(`select-users-view-item-${ data.alternateUser }`)).tap();
+ await waitFor(element(by.id(`selected-user-${ data.alternateUser }`))).toBeVisible().withTimeout(5000);
+ await element(by.id('selected-users-view-submit')).tap();
+ await waitFor(element(by.id('create-channel-view'))).toBeVisible().withTimeout(5000);
+ await element(by.id('create-channel-name')).replaceText(`broadcast${ data.random }`);
+ await element(by.id('create-channel-broadcast')).tap();
+ await element(by.id('create-channel-submit')).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(60000);
+ await expect(element(by.id('room-view'))).toBeVisible();
+ await waitFor(element(by.id('room-view-title'))).toHaveText(`broadcast${ data.random }`).withTimeout(60000);
+ await expect(element(by.id('room-view-title'))).toHaveText(`broadcast${ data.random }`);
+ await element(by.id('room-view-title')).tap();
+ await waitFor(element(by.id('room-info-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id('room-info-view-broadcast'))).toBeVisible().withTimeout(2000);
+ await expect(element(by.id('room-info-view-broadcast'))).toBeVisible();
+ await element(by.id('header-back')).atIndex(0).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(2000);
+ await element(by.id('header-back')).atIndex(0).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible();
+ });
+
+ it('should send message', async() => {
+ await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
+ await element(by.id('messagebox-input')).tap();
+ await element(by.id('messagebox-input')).typeText(`${ data.random }message`);
+ await element(by.id('messagebox-send-message')).tap();
+ await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text(`${ data.random }message`))).toBeVisible();
+ });
+
+ it('should login as user without write message authorization and enter room', async() => {
+ await element(by.id('header-back')).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(2000);
+ await expect(element(by.id('rooms-list-view'))).toBeVisible();
+ await logout();
+ await navigateToLogin();
+ await element(by.id('login-view-email')).replaceText(data.alternateUser);
+ await element(by.id('login-view-password')).replaceText(data.alternateUserPassword);
+ await element(by.id('login-view-submit')).tap();
+ await waitFor(element(by.id('rooms-list-view'))).toBeVisible().withTimeout(10000);
+ await waitFor(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.id(`rooms-list-view-item-broadcast${ data.random }`))).toBeVisible();
+ await element(by.id(`rooms-list-view-item-broadcast${ data.random }`)).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
+ await expect(element(by.id('room-view-title'))).toHaveText(`broadcast${ data.random }`);
+ });
+
+ it('should not have messagebox', async() => {
+ await expect(element(by.id('messagebox'))).toBeNotVisible();
+ });
+
+ it('should be read only', async() => {
+ await expect(element(by.text('This room is read only'))).toBeVisible();
+ });
+
+ it('should have the message created earlier', async() => {
+ await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text(`${ data.random }message`))).toBeVisible();
+ });
+
+ it('should have reply button', async() => {
+ await expect(element(by.text('Reply'))).toBeVisible();
+ });
+
+ it('should tap on reply button and navigate to direct room', async() => {
+ await element(by.text('Reply')).tap();
+ await waitFor(element(by.id('room-view-title'))).toHaveText(data.user).withTimeout(60000);
+ await expect(element(by.id('room-view-title'))).toHaveText(data.user);
+ });
+
+ it('should reply broadcasted message', async() => {
+ await element(by.id('messagebox-input')).tap();
+ await element(by.id('messagebox-input')).typeText(`${ data.random }broadcastreply`);
+ await element(by.id('messagebox-send-message')).tap();
+ await waitFor(element(by.text(`${ data.random }message`))).toBeVisible().withTimeout(60000);
+ await expect(element(by.text(`${ data.random }message`))).toBeVisible(); // broadcasted message
+ await expect(element(by.text(` ${ data.random }broadcastreply`))).toBeVisible(); // reply
+ });
+
+ afterEach(async() => {
+ takeScreenshot();
+ });
+});
diff --git a/e2e/data.js b/e2e/data.js
index d0f5769f9..ee3e31964 100644
--- a/e2e/data.js
+++ b/e2e/data.js
@@ -1,10 +1,12 @@
const random = require('./helpers/random');
const value = random(20);
const data = {
- server: 'https://unstable.rocket.chat',
- alternateServer: 'https://stable.rocket.chat',
+ server: 'https://stable.rocket.chat',
+ alternateServer: 'https://unstable.rocket.chat',
user: `user${ value }`,
password: `password${ value }`,
+ alternateUser: 'detoxrn',
+ alternateUserPassword: '123',
email: `detoxrn+${ value }@rocket.chat`,
random: value
}