diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js
index d85ad30d330..6feb764f136 100644
--- a/app/i18n/locales/en.js
+++ b/app/i18n/locales/en.js
@@ -281,6 +281,8 @@ export default {
Invite_Link: 'Invite Link',
Invite_users: 'Invite users',
Join: 'Join',
+ Join_Code: 'Join Code',
+ Insert_Join_Code: 'Insert Join Code',
Join_our_open_workspace: 'Join our open workspace',
Join_your_workspace: 'Join your workspace',
Just_invited_people_can_access_this_channel: 'Just invited people can access this channel',
diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js
index 743b64295a8..3625120787f 100644
--- a/app/i18n/locales/pt-BR.js
+++ b/app/i18n/locales/pt-BR.js
@@ -270,6 +270,8 @@ export default {
Invite_Link: 'Link de Convite',
Invite_users: 'Convidar usuários',
Join: 'Entrar',
+ Join_Code: 'Insira o Código da Sala',
+ Insert_Join_Code: 'Insira o código para entrar na sala',
Join_our_open_workspace: 'Entra na nossa workspace pública',
Join_your_workspace: 'Entre na sua workspace',
Just_invited_people_can_access_this_channel: 'Apenas as pessoas convidadas podem acessar este canal',
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 182fd856d1f..5020ee4add4 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -705,13 +705,13 @@ const RocketChat = {
});
},
- joinRoom(roomId, type) {
+ joinRoom(roomId, joinCode, type) {
// TODO: join code
// RC 0.48.0
if (type === 'p') {
return this.methodCallWrapper('joinRoom', roomId);
}
- return this.post('channels.join', { roomId });
+ return this.post('channels.join', { roomId, joinCode });
},
triggerBlockAction,
triggerSubmitView,
diff --git a/app/views/DirectoryView/index.js b/app/views/DirectoryView/index.js
index 01788bcc2f7..8be78712d03 100644
--- a/app/views/DirectoryView/index.js
+++ b/app/views/DirectoryView/index.js
@@ -149,8 +149,9 @@ class DirectoryView extends React.Component {
this.goRoom({ rid: result.room._id, name: item.username, t: 'd' });
}
} else {
+ const { room } = await RocketChat.getRoomInfo(item._id);
this.goRoom({
- rid: item._id, name: item.name, t: 'c', search: true
+ rid: item._id, name: item.name, joinCodeRequired: room.joinCodeRequired, t: 'c', search: true
});
}
}
diff --git a/app/views/RoomView/JoinCode.js b/app/views/RoomView/JoinCode.js
new file mode 100644
index 00000000000..e3639818ba6
--- /dev/null
+++ b/app/views/RoomView/JoinCode.js
@@ -0,0 +1,140 @@
+import React, {
+ useState,
+ forwardRef,
+ useImperativeHandle
+} from 'react';
+import PropTypes from 'prop-types';
+import {
+ View,
+ Text,
+ StyleSheet,
+ InteractionManager
+} from 'react-native';
+import Modal from 'react-native-modal';
+import { connect } from 'react-redux';
+
+import I18n from '../../i18n';
+import Button from '../../containers/Button';
+import TextInput from '../../containers/TextInput';
+import RocketChat from '../../lib/rocketchat';
+import sharedStyles from '../Styles';
+
+import { themes } from '../../constants/colors';
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center'
+ },
+ content: {
+ padding: 16,
+ width: '100%',
+ borderRadius: 4
+ },
+ title: {
+ fontSize: 16,
+ paddingBottom: 8,
+ ...sharedStyles.textBold,
+ ...sharedStyles.textAlignCenter
+ },
+ button: {
+ minWidth: 96,
+ marginBottom: 0
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between'
+ },
+ tablet: {
+ height: undefined
+ }
+});
+
+const JoinCode = React.memo(forwardRef(({
+ rid,
+ t,
+ onJoin,
+ isMasterDetail,
+ theme
+}, ref) => {
+ const [visible, setVisible] = useState(false);
+ const [error, setError] = useState(false);
+ const [code, setCode] = useState('');
+
+ const show = () => setVisible(true);
+
+ const hide = () => setVisible(false);
+
+ const joinRoom = async() => {
+ try {
+ await RocketChat.joinRoom(rid, code, t);
+ onJoin();
+ hide();
+ } catch (e) {
+ setError(true);
+ }
+ };
+
+ useImperativeHandle(ref, () => ({ show }));
+
+ return (
+
+
+
+ {I18n.t('Insert_Join_Code')}
+ InteractionManager.runAfterInteractions(() => e?.getNativeRef()?.focus())}
+ returnKeyType='send'
+ autoCapitalize='none'
+ onChangeText={setCode}
+ onSubmitEditing={joinRoom}
+ placeholder={I18n.t('Join_Code')}
+ secureTextEntry
+ error={error && { error: 'error-code-invalid', reason: I18n.t('Code_or_password_invalid') }}
+ testID='join-code-input'
+ />
+
+
+
+
+
+
+
+ );
+}));
+JoinCode.propTypes = {
+ rid: PropTypes.string,
+ t: PropTypes.string,
+ onJoin: PropTypes.func,
+ isMasterDetail: PropTypes.bool,
+ theme: PropTypes.string
+};
+
+const mapStateToProps = state => ({
+ isMasterDetail: state.app.isMasterDetail
+});
+export default connect(mapStateToProps, null, null, { forwardRef: true })(JoinCode);
diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js
index 9434e035712..ea8164f8a37 100644
--- a/app/views/RoomView/index.js
+++ b/app/views/RoomView/index.js
@@ -24,6 +24,7 @@ import MessageErrorActions from '../../containers/MessageErrorActions';
import MessageBox from '../../containers/MessageBox';
import ReactionPicker from './ReactionPicker';
import UploadProgress from './UploadProgress';
+import JoinCode from './JoinCode';
import styles from './styles';
import log, { logEvent, events } from '../../utils/log';
import EventEmitter from '../../utils/events';
@@ -73,7 +74,7 @@ const stateAttrsUpdate = [
'readOnly',
'member'
];
-const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor'];
+const roomAttrsUpdate = ['f', 'ro', 'blocked', 'blocker', 'archived', 'tunread', 'muted', 'ignored', 'jitsiTimeout', 'announcement', 'sysMes', 'topic', 'name', 'fname', 'roles', 'bannerClosed', 'visitor', 'joinCodeRequired'];
class RoomView extends React.Component {
static propTypes = {
@@ -152,6 +153,7 @@ class RoomView extends React.Component {
this.messagebox = React.createRef();
this.list = React.createRef();
+ this.joinCode = React.createRef();
this.mounted = false;
if (this.rid) {
this.sub = new RoomClass(this.rid);
@@ -711,6 +713,12 @@ class RoomView extends React.Component {
setLastOpen = lastOpen => this.setState({ lastOpen });
+ onJoin = () => {
+ this.internalSetState({
+ joined: true
+ });
+ }
+
joinRoom = async() => {
logEvent(events.ROOM_JOIN);
try {
@@ -719,11 +727,14 @@ class RoomView extends React.Component {
if (this.isOmnichannel) {
await takeInquiry(room._id);
} else {
- await RocketChat.joinRoom(this.rid, this.t);
+ const { joinCodeRequired } = room;
+ if (joinCodeRequired) {
+ this.joinCode.current?.show();
+ } else {
+ await RocketChat.joinRoom(this.rid, null, this.t);
+ this.onJoin();
+ }
}
- this.internalSetState({
- joined: true
- });
} catch (e) {
log(e);
}
@@ -1086,6 +1097,13 @@ class RoomView extends React.Component {
onClose={this.onCloseReactionsModal}
getCustomEmoji={this.getCustomEmoji}
/>
+
);
}
diff --git a/e2e/data.js b/e2e/data.js
index d3f4a0df930..f1dbd1bf65b 100644
--- a/e2e/data.js
+++ b/e2e/data.js
@@ -31,6 +31,10 @@ const data = {
channels: {
detoxpublic: {
name: 'detox-public'
+ },
+ detoxpublicprotected: {
+ name: 'detox-public-protected',
+ joinCode: '123'
}
},
groups: {
diff --git a/e2e/data/data.cloud.js b/e2e/data/data.cloud.js
index d3f4a0df930..f1dbd1bf65b 100644
--- a/e2e/data/data.cloud.js
+++ b/e2e/data/data.cloud.js
@@ -31,6 +31,10 @@ const data = {
channels: {
detoxpublic: {
name: 'detox-public'
+ },
+ detoxpublicprotected: {
+ name: 'detox-public-protected',
+ joinCode: '123'
}
},
groups: {
diff --git a/e2e/data/data.docker.js b/e2e/data/data.docker.js
index a22423a638b..e0bb530e184 100644
--- a/e2e/data/data.docker.js
+++ b/e2e/data/data.docker.js
@@ -31,6 +31,10 @@ const data = {
channels: {
detoxpublic: {
name: 'detox-public'
+ },
+ detoxpublicprotected: {
+ name: 'detox-public-protected',
+ joinCode: '123'
}
},
groups: {
diff --git a/e2e/helpers/data_setup.js b/e2e/helpers/data_setup.js
index 11c273dd973..13f1e73d288 100644
--- a/e2e/helpers/data_setup.js
+++ b/e2e/helpers/data_setup.js
@@ -40,12 +40,14 @@ const createUser = async (username, password, name, email) => {
const createChannelIfNotExists = async (channelname) => {
console.log(`Creating public channel ${channelname}`)
try {
- await rocketchat.post('channels.create', {
+ const room = await rocketchat.post('channels.create', {
"name": channelname
})
+ return room
} catch (createError) {
try { //Maybe it exists already?
- await rocketchat.get(`channels.info?roomName=${channelname}`)
+ const room = rocketchat.get(`channels.info?roomName=${channelname}`)
+ return room
} catch (infoError) {
console.log(JSON.stringify(createError))
console.log(JSON.stringify(infoError))
@@ -71,6 +73,24 @@ const createGroupIfNotExists = async (groupname) => {
}
}
+const changeChannelJoinCode = async (roomId, joinCode) => {
+ console.log(`Changing channel Join Code ${roomId}`)
+ try {
+ await rocketchat.post('method.call/saveRoomSettings', {
+ message: JSON.stringify({
+ method: 'saveRoomSettings',
+ params: [
+ roomId,
+ { joinCode }
+ ]
+ })
+ })
+ } catch (createError) {
+ console.log(JSON.stringify(createError))
+ throw "Failed to create protected channel"
+ }
+}
+
const sendMessage = async (user, groupname, msg) => {
console.log(`Sending message to ${groupname}`)
try {
@@ -96,7 +116,11 @@ const setup = async () => {
for (var channelKey in data.channels) {
if (data.channels.hasOwnProperty(channelKey)) {
const channel = data.channels[channelKey]
- await createChannelIfNotExists(channel.name)
+ const { data: { channel: { _id } } } = await createChannelIfNotExists(channel.name)
+
+ if (channel.joinCode) {
+ await changeChannelJoinCode(_id, channel.joinCode);
+ }
}
}
diff --git a/e2e/tests/assorted/08-joinprotectedroom.spec.js b/e2e/tests/assorted/08-joinprotectedroom.spec.js
new file mode 100644
index 00000000000..d6e5605f7d5
--- /dev/null
+++ b/e2e/tests/assorted/08-joinprotectedroom.spec.js
@@ -0,0 +1,60 @@
+const {
+ device, expect, element, by, waitFor
+} = require('detox');
+const data = require('../../data');
+const { navigateToLogin, login, mockMessage, tapBack, sleep, searchRoom } = require('../../helpers/app');
+
+const testuser = data.users.regular
+const room = data.channels.detoxpublicprotected.name
+const joinCode = data.channels.detoxpublicprotected.joinCode
+
+async function navigateToRoom() {
+ await searchRoom(room);
+ await waitFor(element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0)).toBeVisible().withTimeout(60000);
+ await element(by.id(`rooms-list-view-item-${ room }`)).atIndex(0).tap();
+ await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
+}
+
+async function navigateToRoomActions() {
+ await element(by.id('room-view-header-actions')).tap();
+ await waitFor(element(by.id('room-actions-view'))).toBeVisible().withTimeout(5000);
+}
+
+async function openJoinCode() {
+ await element(by.id('room-view-join-button')).tap();
+ await waitFor(element(by.id('join-code'))).toBeVisible().withTimeout(5000);
+}
+
+describe('Join public room', () => {
+ before(async() => {
+ await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
+ await navigateToLogin();
+ await login(testuser.username, testuser.password);
+ await navigateToRoom();
+ });
+
+ describe('Usage', async() => {
+ it('should tap join and ask for join code', async() => {
+ await openJoinCode();
+ })
+
+ it('should cancel join room', async() => {
+ await element(by.id('join-code-cancel')).tap();
+ await waitFor(element(by.id('join-code'))).toBeNotVisible().withTimeout(5000);
+ });
+
+ it('should join room', async() => {
+ await openJoinCode();
+ await element(by.id('join-code-input')).replaceText(joinCode);
+ await element(by.id('join-code-submit')).tap();
+ await waitFor(element(by.id('join-code'))).toBeNotVisible().withTimeout(5000);
+ await waitFor(element(by.id('messagebox'))).toBeVisible().withTimeout(60000);
+ await expect(element(by.id('messagebox'))).toBeVisible();
+ await expect(element(by.id('room-view-join'))).toBeNotVisible();
+ });
+
+ it('should send message', async() => {
+ await mockMessage('message');
+ });
+ });
+});