[NEW] Create Team (#3082)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Gerzon Z 2021-05-12 15:01:29 -04:00 committed by GitHub
parent 8f571fd029
commit 1f0ff830a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 265 additions and 44 deletions

5
app/definition/ITeam.js Normal file
View File

@ -0,0 +1,5 @@
// https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts
export const TEAM_TYPE = {
PUBLIC: 0,
PRIVATE: 1
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
>
<StatusBar />
<SafeAreaView testID='create-channel-view'>
<SafeAreaView testID={isTeam ? 'create-team-view' : 'create-channel-view'}>
<ScrollView {...scrollPersistTaps}>
<View style={[sharedStyles.separatorVertical, { borderColor: themes[theme].separatorColor }]}>
<TextInput
autoFocus
style={[styles.input, { backgroundColor: themes[theme].backgroundColor }]}
label={I18n.t('Channel_Name')}
label={isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')}
value={channelName}
onChangeText={this.onChangeText}
placeholder={I18n.t('Channel_Name')}
placeholder={isTeam ? I18n.t('Team_Name') : I18n.t('Channel_Name')}
returnKeyType='done'
testID='create-channel-name'
testID={isTeam ? 'create-team-name' : 'create-channel-name'}
autoCorrect={false}
autoCapitalize='none'
theme={theme}

View File

@ -116,6 +116,12 @@ class NewMessageView extends React.Component {
navigation.navigate('SelectedUsersViewCreateChannel', { nextAction: () => 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));

View File

@ -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 (
<HeaderButton.Container>
{teamId ? (
{isTeamRoom({ teamId, joined }) ? (
<HeaderButton.Item
iconName='channel-public'
onPress={this.goTeamChannels}

View File

@ -38,7 +38,9 @@ import { themes } from '../../constants/colors';
import debounce from '../../utils/debounce';
import ReactionsModal from '../../containers/ReactionsModal';
import { LISTENER } from '../../containers/Toast';
import { getBadgeColor, isBlocked, makeThreadName } from '../../utils/room';
import {
getBadgeColor, isBlocked, makeThreadName, isTeamRoom
} from '../../utils/room';
import { isReadOnly } from '../../utils/isReadOnly';
import { isIOS, isTablet } from '../../utils/deviceInfo';
import { showErrorAlert } from '../../utils/info';
@ -301,7 +303,7 @@ class RoomView extends React.Component {
setHeader = () => {
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}

View File

@ -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 (
<FlatList
data={users}
ref={this.setFlatListRef}
onContentSizeChange={this.onContentSizeChange}
getItemLayout={getItemLayout}
keyExtractor={item => item._id}
style={[sharedStyles.separatorTop, { borderColor: themes[theme].separatorColor }]}
contentContainerStyle={{ marginVertical: 5 }}

View File

@ -42,6 +42,11 @@ const data = {
name: `detox-private-${ value }`
}
},
teams: {
private: {
name: `detox-team-${ value }`
}
},
registeringUser: {
username: `newuser${ value }`,
password: `password${ value }`,

View File

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

View File

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