[NEW] Broadcast channels (#301)

* Broadcast channels

* e2e tests
This commit is contained in:
Diego Mello 2018-05-24 17:17:45 -03:00 committed by Guilherme Gazzo
parent 8f90565e55
commit 061c313e3f
23 changed files with 471 additions and 168 deletions

View File

@ -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']);

View File

@ -183,3 +183,10 @@ export function toggleReactionPicker(message) {
message
};
}
export function replyBroadcast(message) {
return {
type: types.MESSAGES.REPLY_BROADCAST,
message
};
}

View File

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

View File

@ -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 (
<TouchableOpacity
style={styles.broadcastButton}
onPress={() => this.props.replyBroadcast(this.parseMessage())}
>
<Text style={styles.broadcastButtonText}>Reply</Text>
</TouchableOpacity>
);
}
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()}
</View>
</View>
{this.state.reactionsModal &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<View style={[styles.view_white, styles.switchContainer]}>
renderSwitch = ({
id, value, label, description, onValueChange, disabled = false
}) => (
<View style={{ marginBottom: 15 }}>
<View style={styles.switchContainer}>
<Switch
style={[{ flexGrow: 0, flexShrink: 1 }]}
value={this.state.type}
onValueChange={type => 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}
/>
<Text style={[styles.label_white, styles.switchLabel]}>
{this.state.type ? 'Public' : 'Private'}
</Text>
<Text style={styles.switchLabel}>{label}</Text>
</View>
);
<Text style={styles.switchDescription}>{description}</Text>
</View>
);
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()}
<Text
style={[
styles.label_white,
{
color: '#9ea2a8',
flexGrow: 1,
paddingHorizontal: 0,
marginBottom: 20
}
]}
>
{this.state.type ? (
'Everyone can access this channel'
) : (
'Just invited people can access this channel'
)}
</Text>
<TouchableOpacity
onPress={() => this.submit()}
style={[
styles.buttonContainer_white,
this.state.channelName.length === 0 || this.props.createChannel.isFetching
? styles.disabledButton
: styles.enabledButton
]}
testID='create-channel-submit'
>
<Text style={styles.button_white}>CREATE</Text>
</TouchableOpacity>
{this.renderType()}
{this.renderReadOnly()}
{this.renderBroadcast()}
<View style={styles.alignItemsFlexStart}>
<Button
title='Create'
type='primary'
onPress={this.submit}
disabled={this.state.channelName.length === 0 || this.props.createChannel.isFetching}
testID='create-channel-submit'
/>
</View>
<Loading visible={this.props.createChannel.isFetching} />
</SafeAreaView>
</ScrollView>

View File

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

View File

@ -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 &&
<SwitchContainer
value={reactWhenReadOnly}
leftLabelPrimary='No Reactions'
@ -339,6 +339,12 @@ export default class RoomInfoEditView extends LoggedView {
testID='room-info-edit-view-react-when-ro'
/>
}
{room.broadcast &&
[
<Text style={styles.broadcast}>Broadcast channel</Text>,
<View style={styles.divider} />
]
}
<TouchableOpacity
style={[sharedStyles.buttonContainer, !this.formIsChanged() && styles.buttonContainerDisabled]}
onPress={this.submit}

View File

@ -42,5 +42,9 @@ export default StyleSheet.create({
borderColor: '#ddd',
borderBottomWidth: StyleSheet.hairlineWidth,
marginVertical: 20
},
broadcast: {
fontWeight: 'bold',
textAlign: 'center'
}
});

View File

@ -21,8 +21,11 @@ const PERMISSION_EDIT_ROOM = 'edit-room';
const camelize = str => str.replace(/^(.)/, (match, chr) => chr.toUpperCase());
const getRoomTitle = room => (room.t === 'd' ?
<Text testID='room-info-view-name'>{room.fname}</Text> :
[<RoomTypeIcon type={room.t} />, <Text testID='room-info-view-name'>{room.name}</Text>]
<Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text> :
[
<RoomTypeIcon type={room.t} key='room-info-type' />,
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
]
);
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@ -184,6 +187,17 @@ export default class RoomInfoView extends LoggedView {
</Avatar>
)
renderBroadcast = () => (
<View style={styles.item}>
<Text style={styles.itemLabel}>Broadcast Channel</Text>
<Text
style={styles.itemContent}
testID='room-info-view-broadcast'
>Only authorized users can write new messages, but the other users will be able to reply
</Text>
</View>
)
render() {
const { room, roomUser } = this.state;
if (!room) {
@ -193,13 +207,14 @@ export default class RoomInfoView extends LoggedView {
<ScrollView style={styles.container}>
<View style={styles.avatarContainer} testID='room-info-view'>
{this.renderAvatar(room, roomUser)}
<View style={styles.roomTitle}>{ getRoomTitle(room) }</View>
<View style={styles.roomTitleContainer}>{ getRoomTitle(room) }</View>
</View>
{!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()}
</ScrollView>
);
}

View File

@ -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',

View File

@ -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) => (
<Message
key={item._id}
@ -164,6 +180,7 @@ export default class RoomView extends LoggedView {
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
archived={this.state.room.archived}
broadcast={this.state.room.broadcast}
previousItem={previousItem}
/>
);
@ -179,13 +196,20 @@ export default class RoomView extends LoggedView {
</View>
);
}
if (this.state.room.ro || this.state.room.archived) {
if (this.state.room.archived || this.isReadOnly()) {
return (
<View style={styles.readOnly}>
<Text>This room is read only</Text>
</View>
);
}
if (this.isBlocked()) {
return (
<View style={styles.blockedOrBlocker}>
<Text>This room is blocked</Text>
</View>
);
}
return <MessageBox onSubmit={this.sendMessage} rid={this.rid} />;
};

View File

@ -34,6 +34,9 @@ export default StyleSheet.create({
readOnly: {
padding: 10
},
blockedOrBlocker: {
padding: 10
},
reactionPickerContainer: {
// width: width - 20,
// height: width - 20,

View File

@ -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'
},

View File

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

View File

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

101
e2e/11-broadcast.spec.js Normal file
View File

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

View File

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