diff --git a/app/definition/ITeam.js b/app/definition/ITeam.js
new file mode 100644
index 00000000..10919715
--- /dev/null
+++ b/app/definition/ITeam.js
@@ -0,0 +1,5 @@
+// https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts
+export const TEAM_TYPE = {
+ PUBLIC: 0,
+ PRIVATE: 1
+};
diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json
index ec289d27..58438f31 100644
--- a/app/i18n/locales/en.json
+++ b/app/i18n/locales/en.json
@@ -709,5 +709,12 @@
"This_room_encryption_has_been_disabled_by__username_": "This room's encryption has been disabled by {{username}}",
"Teams": "Teams",
"No_team_channels_found": "No channels found",
- "Team_not_found": "Team not found"
-}
\ No newline at end of file
+ "Team_not_found": "Team not found",
+ "Create_Team": "Create Team",
+ "Team_Name": "Team Name",
+ "Private_Team": "Private Team",
+ "Read_Only_Team": "Read Only Team",
+ "Broadcast_Team": "Broadcast Team",
+ "creating_team" : "creating team",
+ "team-name-already-exists": "A team with that name already exists"
+}
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 84f85a6f..adc7f80f 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -60,6 +60,7 @@ import UserPreferences from './userPreferences';
import { Encryption } from './encryption';
import EventEmitter from '../utils/events';
import { sanitizeLikeString } from './database/utils';
+import { TEAM_TYPE } from '../definition/ITeam';
const TOKEN_KEY = 'reactnativemeteor_usertoken';
const CURRENT_SERVER = 'currentServer';
@@ -732,7 +733,24 @@ const RocketChat = {
prid, pmid, t_name, reply, users, encrypted
});
},
-
+ createTeam({
+ name, users, type, readOnly, broadcast, encrypted
+ }) {
+ const params = {
+ name,
+ users,
+ type: type ? TEAM_TYPE.PRIVATE : TEAM_TYPE.PUBLIC,
+ room: {
+ readOnly,
+ extraData: {
+ broadcast,
+ encrypted
+ }
+ }
+ };
+ // RC 3.13.0
+ return this.post('teams.create', params);
+ },
joinRoom(roomId, joinCode, type) {
// TODO: join code
// RC 0.48.0
diff --git a/app/sagas/createChannel.js b/app/sagas/createChannel.js
index 613aedec..f2ecfe76 100644
--- a/app/sagas/createChannel.js
+++ b/app/sagas/createChannel.js
@@ -21,6 +21,10 @@ const createGroupChat = function createGroupChat() {
return RocketChat.createGroupChat();
};
+const createTeam = function createTeam(data) {
+ return RocketChat.createTeam(data);
+};
+
const handleRequest = function* handleRequest({ data }) {
try {
const auth = yield select(state => state.login.isAuthenticated);
@@ -29,7 +33,21 @@ const handleRequest = function* handleRequest({ data }) {
}
let sub;
- if (data.group) {
+ if (data.isTeam) {
+ const {
+ type,
+ readOnly,
+ broadcast,
+ encrypted
+ } = data;
+ logEvent(events.CR_CREATE, {
+ type,
+ readOnly,
+ broadcast,
+ encrypted
+ });
+ sub = yield call(createTeam, data);
+ } else if (data.group) {
logEvent(events.SELECTED_USERS_CREATE_GROUP);
const result = yield call(createGroupChat);
if (result.success) {
@@ -56,7 +74,7 @@ const handleRequest = function* handleRequest({ data }) {
const subCollection = db.get('subscriptions');
yield db.action(async() => {
await subCollection.create((s) => {
- s._raw = sanitizedRaw({ id: sub.rid }, subCollection.schema);
+ s._raw = sanitizedRaw({ id: sub.team ? sub.team.roomId : sub.rid }, subCollection.schema);
Object.assign(s, sub);
});
});
@@ -64,7 +82,17 @@ const handleRequest = function* handleRequest({ data }) {
// do nothing
}
- yield put(createChannelSuccess(sub));
+ let successParams = {};
+ if (data.isTeam) {
+ successParams = {
+ ...sub.team,
+ rid: sub.team.roomId,
+ t: sub.team.type ? 'p' : 'c'
+ };
+ } else {
+ successParams = data;
+ }
+ yield put(createChannelSuccess(successParams));
} catch (err) {
logEvent(events[data.group ? 'SELECTED_USERS_CREATE_GROUP_F' : 'CR_CREATE_F']);
yield put(createChannelFailure(err));
@@ -81,7 +109,7 @@ const handleSuccess = function* handleSuccess({ data }) {
const handleFailure = function handleFailure({ err }) {
setTimeout(() => {
- const msg = err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
+ const msg = err.data ? I18n.t(err.data.error) : err.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_channel') });
showErrorAlert(msg);
}, 300);
};
diff --git a/app/utils/log/events.js b/app/utils/log/events.js
index c9c9579f..490f0dfd 100644
--- a/app/utils/log/events.js
+++ b/app/utils/log/events.js
@@ -88,6 +88,7 @@ export default {
// NEW MESSAGE VIEW
NEW_MSG_CREATE_CHANNEL: 'new_msg_create_channel',
+ NEW_MSG_CREATE_TEAM: 'new_msg_create_team',
NEW_MSG_CREATE_GROUP_CHAT: 'new_msg_create_group_chat',
NEW_MSG_CREATE_DISCUSSION: 'new_msg_create_discussion',
NEW_MSG_CHAT_WITH_USER: 'new_msg_chat_with_user',
diff --git a/app/utils/room.js b/app/utils/room.js
index 7077c73d..fef926d5 100644
--- a/app/utils/room.js
+++ b/app/utils/room.js
@@ -45,3 +45,5 @@ export const getBadgeColor = ({ subscription, messageId, theme }) => {
};
export const makeThreadName = messageRecord => messageRecord.msg || messageRecord?.attachments[0]?.title;
+
+export const isTeamRoom = ({ teamId, joined }) => teamId && joined;
diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js
index 9d1e450f..8090ef4f 100644
--- a/app/views/CreateChannelView.js
+++ b/app/views/CreateChannelView.js
@@ -68,12 +68,9 @@ const styles = StyleSheet.create({
});
class CreateChannelView extends React.Component {
- static navigationOptions = () => ({
- title: I18n.t('Create_Channel')
- });
-
static propTypes = {
navigation: PropTypes.object,
+ route: PropTypes.object,
baseUrl: PropTypes.string,
create: PropTypes.func.isRequired,
removeUser: PropTypes.func.isRequired,
@@ -89,12 +86,19 @@ class CreateChannelView extends React.Component {
theme: PropTypes.string
};
- state = {
- channelName: '',
- type: true,
- readOnly: false,
- encrypted: false,
- broadcast: false
+ constructor(props) {
+ super(props);
+ const { route } = this.props;
+ const isTeam = route?.params?.isTeam || false;
+ this.state = {
+ channelName: '',
+ type: true,
+ readOnly: false,
+ encrypted: false,
+ broadcast: false,
+ isTeam
+ };
+ this.setHeader();
}
shouldComponentUpdate(nextProps, nextState) {
@@ -134,6 +138,15 @@ class CreateChannelView extends React.Component {
return false;
}
+ setHeader = () => {
+ const { navigation } = this.props;
+ const { isTeam } = this.state;
+
+ navigation.setOptions({
+ title: isTeam ? I18n.t('Create_Team') : I18n.t('Create_Channel')
+ });
+ }
+
toggleRightButton = (channelName) => {
const { navigation } = this.props;
navigation.setOptions({
@@ -152,9 +165,11 @@ class CreateChannelView extends React.Component {
submit = () => {
const {
- channelName, type, readOnly, broadcast, encrypted
+ channelName, type, readOnly, broadcast, encrypted, isTeam
} = this.state;
- const { users: usersProps, isFetching, create } = this.props;
+ const {
+ users: usersProps, isFetching, create
+ } = this.props;
if (!channelName.trim() || isFetching) {
return;
@@ -163,9 +178,9 @@ class CreateChannelView extends React.Component {
// transform users object into array of usernames
const users = usersProps.map(user => user.name);
- // create channel
+ // create channel or team
create({
- name: channelName, users, type, readOnly, broadcast, encrypted
+ name: channelName, users, type, readOnly, broadcast, encrypted, isTeam
});
Review.pushPositiveEvent();
@@ -196,11 +211,12 @@ class CreateChannelView extends React.Component {
}
renderType() {
- const { type } = this.state;
+ const { type, isTeam } = this.state;
+
return this.renderSwitch({
id: 'type',
value: type,
- label: 'Private_Channel',
+ label: isTeam ? 'Private_Team' : 'Private_Channel',
onValueChange: (value) => {
logEvent(events.CR_TOGGLE_TYPE);
// If we set the channel as public, encrypted status should be false
@@ -210,11 +226,12 @@ class CreateChannelView extends React.Component {
}
renderReadOnly() {
- const { readOnly, broadcast } = this.state;
+ const { readOnly, broadcast, isTeam } = this.state;
+
return this.renderSwitch({
id: 'readonly',
value: readOnly,
- label: 'Read_Only_Channel',
+ label: isTeam ? 'Read_Only_Team' : 'Read_Only_Channel',
onValueChange: (value) => {
logEvent(events.CR_TOGGLE_READ_ONLY);
this.setState({ readOnly: value });
@@ -244,11 +261,12 @@ class CreateChannelView extends React.Component {
}
renderBroadcast() {
- const { broadcast, readOnly } = this.state;
+ const { broadcast, readOnly, isTeam } = this.state;
+
return this.renderSwitch({
id: 'broadcast',
value: broadcast,
- label: 'Broadcast_Channel',
+ label: isTeam ? 'Broadcast_Team' : 'Broadcast_Channel',
onValueChange: (value) => {
logEvent(events.CR_TOGGLE_BROADCAST);
this.setState({
@@ -301,8 +319,10 @@ class CreateChannelView extends React.Component {
}
render() {
- const { channelName } = this.state;
- const { users, isFetching, theme } = this.props;
+ const { channelName, isTeam } = this.state;
+ const {
+ users, isFetching, theme
+ } = this.props;
const userCount = users.length;
return (
@@ -312,18 +332,18 @@ class CreateChannelView extends React.Component {
keyboardVerticalOffset={128}
>
-
+
navigation.navigate('CreateChannelView') });
}
+ createTeam = () => {
+ logEvent(events.NEW_MSG_CREATE_TEAM);
+ const { navigation } = this.props;
+ navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => navigation.navigate('CreateChannelView', { isTeam: true }) });
+ }
+
createGroupChat = () => {
logEvent(events.NEW_MSG_CREATE_GROUP_CHAT);
const { createChannel, maxUsers, navigation } = this.props;
@@ -172,6 +178,12 @@ class NewMessageView extends React.Component {
testID: 'new-message-view-create-channel',
first: true
})}
+ {this.renderButton({
+ onPress: this.createTeam,
+ title: I18n.t('Create_Team'),
+ icon: 'teams',
+ testID: 'new-message-view-create-team'
+ })}
{maxUsers > 2 ? this.renderButton({
onPress: this.createGroupChat,
title: I18n.t('Create_Direct_Messages'),
@@ -253,7 +265,7 @@ const mapStateToProps = state => ({
});
const mapDispatchToProps = dispatch => ({
- createChannel: params => dispatch(createChannelRequest(params))
+ create: params => dispatch(createChannelRequest(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(NewMessageView));
diff --git a/app/views/RoomView/RightButtons.js b/app/views/RoomView/RightButtons.js
index debc3edb..81b8f153 100644
--- a/app/views/RoomView/RightButtons.js
+++ b/app/views/RoomView/RightButtons.js
@@ -7,6 +7,7 @@ import * as HeaderButton from '../../containers/HeaderButton';
import database from '../../lib/database';
import { getUserSelector } from '../../selectors/login';
import { logEvent, events } from '../../utils/log';
+import { isTeamRoom } from '../../utils/room';
class RightButtonsContainer extends Component {
static propTypes = {
@@ -15,10 +16,11 @@ class RightButtonsContainer extends Component {
rid: PropTypes.string,
t: PropTypes.string,
tmid: PropTypes.string,
- teamId: PropTypes.bool,
+ teamId: PropTypes.string,
navigation: PropTypes.object,
isMasterDetail: PropTypes.bool,
- toggleFollowThread: PropTypes.func
+ toggleFollowThread: PropTypes.func,
+ joined: PropTypes.bool
};
constructor(props) {
@@ -163,7 +165,7 @@ class RightButtonsContainer extends Component {
isFollowingThread, tunread, tunreadUser, tunreadGroup
} = this.state;
const {
- t, tmid, threadsEnabled, teamId
+ t, tmid, threadsEnabled, teamId, joined
} = this.props;
if (t === 'l') {
return null;
@@ -181,7 +183,7 @@ class RightButtonsContainer extends Component {
}
return (
- {teamId ? (
+ {isTeamRoom({ teamId, joined }) ? (
{
const {
- room, unreadsCount, roomUserId
+ room, unreadsCount, roomUserId, joined
} = this.state;
const {
navigation, isMasterDetail, theme, baseUrl, user, insets, route
@@ -331,7 +333,7 @@ class RoomView extends React.Component {
let numIconsRight = 2;
if (tmid) {
numIconsRight = 1;
- } else if (teamId) {
+ } else if (isTeamRoom({ teamId, joined })) {
numIconsRight = 3;
}
const headerTitlePosition = getHeaderTitlePosition({ insets, numIconsRight });
@@ -380,6 +382,8 @@ class RoomView extends React.Component {
rid={rid}
tmid={tmid}
teamId={teamId}
+ teamMain={teamMain}
+ joined={joined}
t={t}
navigation={navigation}
toggleFollowThread={this.toggleFollowThread}
diff --git a/app/views/SelectedUsersView.js b/app/views/SelectedUsersView.js
index f9d14e16..bd6740e1 100644
--- a/app/views/SelectedUsersView.js
+++ b/app/views/SelectedUsersView.js
@@ -17,7 +17,6 @@ import sharedStyles from './Styles';
import * as HeaderButton from '../containers/HeaderButton';
import StatusBar from '../containers/StatusBar';
import { themes } from '../constants/colors';
-import { animateNextTransition } from '../utils/layoutAnimation';
import { withTheme } from '../theme';
import { getUserSelector } from '../selectors/login';
import {
@@ -28,6 +27,9 @@ import {
import { showErrorAlert } from '../utils/info';
import SafeAreaView from '../containers/SafeAreaView';
+const ITEM_WIDTH = 250;
+const getItemLayout = (_, index) => ({ length: ITEM_WIDTH, offset: ITEM_WIDTH * index, index });
+
class SelectedUsersView extends React.Component {
static propTypes = {
baseUrl: PropTypes.string,
@@ -50,7 +52,7 @@ class SelectedUsersView extends React.Component {
constructor(props) {
super(props);
this.init();
-
+ this.flatlist = React.createRef();
const maxUsers = props.route.params?.maxUsers;
this.state = {
maxUsers,
@@ -151,7 +153,6 @@ class SelectedUsersView extends React.Component {
return;
}
- animateNextTransition();
if (!this.isChecked(user.name)) {
if (this.isGroupChat() && users.length === maxUsers) {
return showErrorAlert(I18n.t('Max_number_of_users_allowed_is_number', { maxUsers }), I18n.t('Oops'));
@@ -184,15 +185,23 @@ class SelectedUsersView extends React.Component {
);
}
+ setFlatListRef = ref => this.flatlist = ref;
+
+ onContentSizeChange = () => this.flatlist.scrollToEnd({ animated: true });
+
renderSelected = () => {
const { users, theme } = this.props;
if (users.length === 0) {
return null;
}
+
return (
item._id}
style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]}
contentContainerStyle={{ marginVertical: 5 }}
diff --git a/e2e/data.js b/e2e/data.js
index 79e7842e..c69b7251 100644
--- a/e2e/data.js
+++ b/e2e/data.js
@@ -42,6 +42,11 @@ const data = {
name: `detox-private-${ value }`
}
},
+ teams: {
+ private: {
+ name: `detox-team-${ value }`
+ }
+ },
registeringUser: {
username: `newuser${ value }`,
password: `password${ value }`,
diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js
index 1f8f8fb6..ce1d5083 100644
--- a/e2e/helpers/data_setup.js
+++ b/e2e/helpers/data_setup.js
@@ -1,5 +1,6 @@
const axios = require('axios').default;
const data = require('../data');
+const { TEAM_TYPE } = require('../../app/definition/ITeam');
let server = data.server
@@ -57,6 +58,24 @@ const createChannelIfNotExists = async (channelname) => {
}
}
+const createTeamIfNotExists = async (teamname) => {
+ console.log(`Creating private team ${teamname}`)
+ try {
+ await rocketchat.post('teams.create', {
+ "name": teamname,
+ "type": TEAM_TYPE.PRIVATE
+ })
+ } catch (createError) {
+ try { //Maybe it exists already?
+ await rocketchat.get(`teams.info?teamName=${teamname}`)
+ } catch (infoError) {
+ console.log(JSON.stringify(createError))
+ console.log(JSON.stringify(infoError))
+ throw "Failed to find or create private team"
+ }
+ }
+}
+
const createGroupIfNotExists = async (groupname) => {
console.log(`Creating private group ${groupname}`)
try {
@@ -133,6 +152,13 @@ const setup = async () => {
}
}
+ for (var teamKey in data.teams) {
+ if (data.teams.hasOwnProperty(teamKey)) {
+ const team = data.teams[teamKey]
+ await createTeamIfNotExists(team.name)
+ }
+ }
+
return
}
diff --git a/e2e/tests/team/01-createteam.spec.js b/e2e/tests/team/01-createteam.spec.js
new file mode 100644
index 00000000..4dfe17ca
--- /dev/null
+++ b/e2e/tests/team/01-createteam.spec.js
@@ -0,0 +1,82 @@
+const {
+ device, expect, element, by, waitFor
+} = require('detox');
+const data = require('../../data');
+const { tapBack, sleep, navigateToLogin, login, tryTapping } = require('../../helpers/app');
+
+
+
+describe('Create team screen', () => {
+ before(async() => {
+ await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
+ await navigateToLogin();
+ await login(data.users.regular.username, data.users.regular.password);
+ });
+
+ describe('New Message', async() => {
+ before(async() => {
+ await element(by.id('rooms-list-view-create-channel')).tap();
+ });
+
+ describe('Render', async() => {
+ it('should have team button', async() => {
+ await waitFor(element(by.id('new-message-view-create-channel'))).toBeVisible().withTimeout(2000);
+ });
+ })
+
+ describe('Usage', async() => {
+ it('should navigate to select users', async() => {
+ await element(by.id('new-message-view-create-channel')).tap();
+ await waitFor(element(by.id('select-users-view'))).toExist().withTimeout(5000);
+ });
+ })
+ });
+
+ describe('Select Users', async() => {
+ it('should search users', async() => {
+ await element(by.id('select-users-view-search')).replaceText('rocket.cat');
+ await waitFor(element(by.id(`select-users-view-item-rocket.cat`))).toBeVisible().withTimeout(10000);
+ });
+
+ it('should select/unselect user', async() => {
+ // Spotlight issues
+ await element(by.id('select-users-view-item-rocket.cat')).tap();
+ await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000);
+ await element(by.id('selected-user-rocket.cat')).tap();
+ await waitFor(element(by.id('selected-user-rocket.cat'))).toBeNotVisible().withTimeout(10000);
+ // Spotlight issues
+ await element(by.id('select-users-view-item-rocket.cat')).tap();
+ await waitFor(element(by.id('selected-user-rocket.cat'))).toBeVisible().withTimeout(10000);
+ });
+
+ it('should create team', async() => {
+ await element(by.id('selected-users-view-submit')).tap();
+ await waitFor(element(by.id('create-channel-view'))).toExist().withTimeout(10000);
+ });
+ })
+
+ describe('Create Team', async() => {
+ describe('Usage', async() => {
+ it('should get invalid team name', async() => {
+ await element(by.id('create-channel-name')).typeText(`${data.teams.private.name}`);
+ await element(by.id('create-channel-submit')).tap();
+ await element(by.text('OK')).tap();
+ });
+
+ it('should create private team', async() => {
+ const room = `private${ data.random }`;
+ await element(by.id('create-channel-name')).replaceText('');
+ await element(by.id('create-channel-name')).typeText(room);
+ await element(by.id('create-channel-submit')).tap();
+ await waitFor(element(by.id('room-view'))).toExist().withTimeout(20000);
+ await expect(element(by.id('room-view'))).toExist();
+ await waitFor(element(by.id(`room-view-title-${ room }`))).toExist().withTimeout(6000);
+ await expect(element(by.id(`room-view-title-${ room }`))).toExist();
+ await tapBack();
+ await waitFor(element(by.id('rooms-list-view'))).toExist().withTimeout(10000);
+ await waitFor(element(by.id(`rooms-list-view-item-${ room }`))).toExist().withTimeout(6000);
+ await expect(element(by.id(`rooms-list-view-item-${ room }`))).toExist();
+ });
+ })
+ });
+});