ref: merge prejoin with lobby

This commit is contained in:
Bettenbuk Zoltan 2020-05-20 10:25:31 +02:00 committed by Zoltan Bettenbuk
parent 475a2ae596
commit 29dc63fbcb
68 changed files with 1415 additions and 1211 deletions

View File

@ -1,115 +1,48 @@
#lobby-screen {
align-items: center;
color: $overflowMenuItemColor;
display: flex;
flex-direction: column;
font-size: 1.2em;
margin: 48px 36px;
.content {
span {
padding: 8px 0;
}
.container {
align-items: center;
display: flex;
flex-direction: column;
.title {
color: $defaultColor;
font-size: 2em;
}
.roomName {
font-size: 1em;
}
.participantInfo {
align-items: center;
align-self: stretch;
border: 1px solid #B8C7E0;
border-radius: 4px;
display: flex;
flex-direction: column;
margin: 24px 0;
padding: 34px 0;
&:hover {
padding-top: 0px;
.editButton {
display: flex;
.spinner {
margin: 30px;
}
.joining-message {
margin: 10px;
}
}
.editButton {
align-self: stretch;
display: none;
justify-content: flex-end;
padding: 5px;
position: relative;
button {
background-color: transparent;
border-width: 0;
margin: 0;
padding: 0;
}
.form {
align-items: stretch;
display: flex;
flex-direction: column;
min-width: 400px;
}
.displayName {
color: $defaultColor;
font-size: 1.3em;
}
}
.form {
align-self: stretch;
display: flex;
flex-direction: column;
margin: 32px 0;
input {
margin: 5px 0 15px 0;
}
span {
color: white;
font-size: 1.3em;
text-align: center;
}
}
.joiningContainer {
align-items: center;
display: flex;
flex-direction: column;
margin: 36px 0;
span {
margin-top: 36px;
text-align: center;
.participant-info {
align-items: center;
display: flex;
flex-direction: column;
}
}
}
#lobby-dialog {
align-self: stretch;
#lobby-section {
display: flex;
flex-direction: column;
margin: 32px 0;
.description {
margin-bottom: 18px;
}
.field {
.control-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
:first-child {
align-items: center;
display: flex;
padding-right: 15px;
}
:last-child {
flex: 1;
label {
font-size: 14px;
font-weight: bold;
}
}
}
@ -162,11 +95,7 @@
}
}
}
}
// Common styles
#lobby-dialog, #lobby-screen, #knocking-participant-list {
input {
align-self: stretch;
background-color: transparent;
@ -208,4 +137,4 @@
border-width: 0;
}
}
}
}

View File

@ -1,18 +1,4 @@
.prejoin {
&-full-page {
background: #1C2025;
position: absolute;
width: 100%;
height: 100%;
z-index: $toolbarZ + 1;
}
&-input-area-container {
position: absolute;
bottom: 48px;
width: 100%;
z-index: 2;
}
&-input-area {
margin: 0 auto;
@ -27,65 +13,6 @@
margin-bottom: 16px;
}
&-btn {
border-radius: 3px;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 15px;
line-height: 24px;
padding: 7px 16px;
position: relative;
text-align: center;
width: 286px;
&--primary {
background: #0376DA;
border: 1px solid #0376DA;
}
&--secondary {
background: #2A3A4B;
border: 1px solid #5E6D7A;
}
&--text {
width: auto;
font-size: 13px;
margin: 0;
padding: 0;
}
&--disabled {
background: #5E6D7A;
border: 1px solid #5E6D7A;
color: #AFB6BC;
cursor: initial;
.prejoin-btn-icon {
& > svg {
fill: #AFB6BC;
}
}
.prejoin-btn-options {
border-left: 1px solid #AFB6BC;
}
}
}
&-btn-options {
align-items: center;
border-left: 1px solid #fff;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
right: 0;
top: 0;
width: 40px;
}
&-text-btns {
display: flex;
justify-content: space-between;
@ -179,25 +106,6 @@
margin: 200px auto 0 auto;
}
&-btn-container {
display: flex;
justify-content: center;
margin-top: 32px;
width: 100%;
&> div {
margin: 0 12px;
}
.settings-button-small-icon {
right: -8px;
&--hovered {
right: -10px;
}
}
}
&-overlay {
height: 100%;
position: absolute;
@ -217,22 +125,20 @@
&-status {
align-items: center;
bottom: 0;
align-self: stretch;
color: #fff;
display: flex;
font-size: 13px;
min-height: 24px;
justify-content: center;
position: absolute;
text-align: center;
width: 100%;
z-index: 1;
&--warning {
background: rgba(241, 173, 51, 0.5)
background: rgba(241, 173, 51, 0.7)
}
&--ok {
background: rgba(49, 183, 106, 0.5);
background: rgba(49, 183, 106, 0.7);
}
}
@ -291,63 +197,3 @@
}
}
.prejoin-copy {
&-meeting {
cursor: pointer;
color: #fff;
font-size: 15px;
font-weight: 300;
line-height: 24px;
position: relative;
}
&-url {
max-width: 278px;
padding: 8px 10px;
overflow: hidden;
text-overflow: ellipsis;
}
&-badge {
border-radius: 4px;
height: 100%;
line-height: 38px;
position: absolute;
padding-left: 10px;
text-align: left;
top: 0;
width: 100%;
&--hover {
background: #1C2025;
}
&--done {
background: #31B76A;
}
}
&-icon {
position: absolute;
right: 8px;
top: 8px;
&--white {
&> svg > path {
fill: #fff
}
}
&--light {
&> svg > path {
fill: #D1DBE8;
}
}
}
&-textarea {
position: absolute;
left: -9999px;
}
}

View File

@ -0,0 +1,194 @@
/**
* Shared style for full screen local track based dialogs/modals.
*/
.premeeting-screen {
align-items: stretch;
background: #1C2025;
bottom: 0;
display: flex;
flex-direction: column;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
.content {
align-items: center;
background-image: linear-gradient(transparent, black);
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
z-index: $toolbarZ + 2;
.title {
color: #fff;
font-size: 24px;
line-height: 32px;
margin-bottom: 16px;
}
.copy-meeting {
align-items: center;
cursor: pointer;
color: #fff;
display: flex;
flex-direction: row;
font-size: 15px;
font-weight: 300;
justify-content: center;
line-height: 24px;
.url {
display: flex;
padding: 8px 10px;
&:hover {
background: #1C2025;
border-radius: 4px;
}
&.done {
background: #31B76A;
}
.jitsi-icon {
margin-left: 10px;
}
}
&:hover {
align-self: stretch;
}
textarea {
border-width: 0;
height: 0;
opacity: 0;
padding: 0;
width: 0;
}
}
input.field {
background-color: transparent;
border: 1px solid transparent;
color: white;
outline-width: 0;
padding: 20px;
text-align: center;
&.focused {
border-bottom: 1px solid white;
}
&.error::placeholder {
color: $defaultWarningColor;
}
}
.action-btn {
border-radius: 3px;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 15px;
line-height: 24px;
margin: 10px;
padding: 7px 16px;
position: relative;
text-align: center;
width: 286px;
&.primary {
background: #0376DA;
border: 1px solid #0376DA;
}
&.secondary {
background: transparent;
border: 1px solid #5E6D7A;
}
&.text {
width: auto;
font-size: 13px;
margin: 0;
padding: 0;
}
&.disabled {
background: #5E6D7A;
border: 1px solid #5E6D7A;
color: #AFB6BC;
cursor: initial;
.icon {
& > svg {
fill: #AFB6BC;
}
}
.options {
border-left: 1px solid #AFB6BC;
}
}
.options {
align-items: center;
border-left: 1px solid #fff;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
right: 0;
top: 0;
width: 40px;
}
}
}
.media-btn-container {
display: flex;
justify-content: center;
margin: 32px 0;
width: 100%;
&> div {
margin: 0 12px;
}
.settings-button-small-icon {
right: -8px;
&--hovered {
right: -10px;
}
}
}
}
#preview {
height: 100%;
position: absolute;
width: 100%;
&.no-video {
background: radial-gradient(50% 50% at 50% 50%, #5B6F80 0%, #365067 100%), #FFFFFF;
text-align: center;
}
.avatar {
background: #A4B8D1;
margin: 200px auto 0 auto;
}
video {
height: 100%;
object-fit: cover;
position: absolute;
width: 100%;
}
}

View File

@ -97,5 +97,6 @@ $flagsImagePath: "../images/";
@import 'country-picker';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'premeeting-screens';
/* Modules END */

View File

@ -3,25 +3,47 @@
color: #fff;
font-size: 15px;
line-height: 24px;
&.password {
&.password-section {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
&-actions {
a {
cursor: pointer;
text-decoration: none;
font-size: 14px;
color: #6FB1EA;
}
.password {
align-items: center;
display: flex;
justify-content: space-between;
margin-top: 15px;
& > :first-child:not(:last-child) {
margin-right: 24px;
&-actions {
a {
cursor: pointer;
text-decoration: none;
font-size: 14px;
color: #6FB1EA;
}
&>a+a {
margin-left: 24px;
}
}
}
}
&> :first-child:not(:last-child) {
margin-right: 24px;
}
.separator-line {
margin: 24px 0 24px -20px;
padding: 0 20px;
width: 100%;
height: 1px;
background: #5E6D7A;
&:last-child {
display: none;
}
}
}
}
@ -34,4 +56,4 @@
background: rgba(241, 173, 51, 0.7);
border: 1px solid rgba(255, 255, 255, 0.4);
}
}
}

View File

@ -48,7 +48,7 @@ var interfaceConfig = {
*/
TOOLBAR_BUTTONS: [
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'lobby', 'profile', 'chat', 'recording',
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',

View File

@ -867,26 +867,29 @@
},
"lobby": {
"allow": "Allow",
"backToKnockModeButton": "No password, knock instead",
"backToKnockModeButton": "No password, ask to join instead",
"dialogTitle": "Lobby mode",
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
"disableDialogSubmit": "Disable",
"emailField": "Enter your email address",
"enableDialogPasswordField": "Set password (optional)",
"enableDialogSubmit": "Enable",
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approve of a moderator or by entering an optional predefined password.",
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
"enterPasswordButton": "Enter meeting password",
"enterPasswordTitle": "Enter password to join meeting",
"invalidPassword": "Invalid password",
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",
"joinWithPasswordMessage": "Trying to join with password, please wait...",
"joinRejectedMessage": "Your join request was rejected by a moderator.",
"joinTitle": "Join Meeting",
"joiningTitle": "Asking to join",
"joiningWithPasswordTitle": "Joining",
"joiningTitle": "Asking to join meeting...",
"joiningWithPasswordTitle": "Joining with password...",
"knockButton": "Ask to Join",
"knockTitle": "Someone wants to join the meeting",
"nameField": "Enter your name",
"passwordField": "Enter password",
"passwordField": "Enter meeting password",
"passwordJoinButton": "Join",
"reject": "Reject"
"reject": "Reject",
"toggleLabel": "Enable lobby"
}
}

View File

@ -249,6 +249,7 @@ export function authStatusChanged(authEnabled: boolean, authLogin: string) {
* @param {JitsiConference} conference - The JitsiConference that has failed.
* @param {string} error - The error describing/detailing the cause of the
* failure.
* @param {any} params - Rest of the params that we receive together with the event.
* @returns {{
* type: CONFERENCE_FAILED,
* conference: JitsiConference,
@ -651,28 +652,23 @@ export function setPassword(
case conference.join: {
let state = getState()['features/base/conference'];
// Make sure that the action will set a password for a conference
// that the application wants joined.
if (state.passwordRequired === conference) {
dispatch({
type: SET_PASSWORD,
conference,
method,
password
});
dispatch({
type: SET_PASSWORD,
conference,
method,
password
});
// Join the conference with the newly-set password.
// Join the conference with the newly-set password.
// Make sure that the action did set the password.
state = getState()['features/base/conference'];
if (state.password === password
&& !state.passwordRequired
// Make sure that the action did set the password.
state = getState()['features/base/conference'];
if (state.password === password
// Make sure that the application still wants the
// conference joined.
&& !state.conference) {
method.call(conference, password);
}
// Make sure that the application still wants the
// conference joined.
&& !state.conference) {
method.call(conference, password);
}
break;
}

View File

@ -145,10 +145,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
const result = next(action);
const { conference, error } = action;
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
sendAnalytics(createOfferAnswerFailedEvent());
}
// Handle specific failure reasons.
switch (error.name) {
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
@ -167,7 +163,7 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
case JitsiConferenceErrors.CONNECTION_ERROR: {
const [ msg ] = error.params;
dispatch(connectionDisconnected(getState()['features/base/connection'].connection, 'Disconnected'));
dispatch(connectionDisconnected(getState()['features/base/connection'].connection));
dispatch(showErrorNotification({
descriptionArguments: { msg },
descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
@ -176,6 +172,9 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
break;
}
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
sendAnalytics(createOfferAnswerFailedEvent());
break;
}
// FIXME: Workaround for the web version. Currently, the creation of the

View File

@ -387,34 +387,30 @@ function _setDesktopSharingEnabled(state, action) {
function _setPassword(state, { conference, method, password }) {
switch (method) {
case conference.join:
if (state.passwordRequired === conference) {
return assign(state, {
// XXX 1. The JitsiConference which transitions away from
// passwordRequired MUST remain in the redux state
// features/base/conference until it transitions into
// conference; otherwise, there is a span of time during which
// the redux state does not even know that there is a
// JitsiConference whatsoever.
//
// 2. The redux action setPassword will attempt to join the
// JitsiConference so joining is an appropriate transitional
// redux state.
//
// 3. The redux action setPassword will perform the same check
// before it proceeds with the re-join.
joining: state.conference ? state.joining : conference,
locked: LOCKED_REMOTELY,
return assign(state, {
// 1. The JitsiConference which transitions away from
// passwordRequired MUST remain in the redux state
// features/base/conference until it transitions into
// conference; otherwise, there is a span of time during which
// the redux state does not even know that there is a
// JitsiConference whatsoever.
//
// 2. The redux action setPassword will attempt to join the
// JitsiConference so joining is an appropriate transitional
// redux state.
//
// 3. The redux action setPassword will perform the same check
// before it proceeds with the re-join.
joining: state.conference ? state.joining : conference,
locked: LOCKED_REMOTELY,
/**
* The password with which the conference is to be joined.
*
* @type {string}
*/
password,
passwordRequired: undefined
});
}
break;
/**
* The password with which the conference is to be joined.
*
* @type {string}
*/
password
});
case conference.lock:
return assign(state, {

View File

@ -5,8 +5,7 @@
*
* {
* type: CONNECTION_DISCONNECTED,
* connection: JitsiConnection,
* message: string
* connection: JitsiConnection
* }
*/
export const CONNECTION_DISCONNECTED = 'CONNECTION_DISCONNECTED';

View File

@ -113,13 +113,12 @@ export function connect(id: ?string, password: ?string) {
* Dispatches {@code CONNECTION_DISCONNECTED} action when connection is
* disconnected.
*
* @param {string} message - Disconnect reason.
* @private
* @returns {void}
*/
function _onConnectionDisconnected(message: string) {
function _onConnectionDisconnected() {
unsubscribe();
dispatch(connectionDisconnected(connection, message));
dispatch(connectionDisconnected(connection));
}
/**
@ -187,19 +186,16 @@ export function connect(id: ?string, password: ?string) {
*
* @param {JitsiConnection} connection - The {@code JitsiConnection} which
* disconnected.
* @param {string} message - Error message.
* @private
* @returns {{
* type: CONNECTION_DISCONNECTED,
* connection: JitsiConnection,
* message: string
* connection: JitsiConnection
* }}
*/
export function connectionDisconnected(connection: Object, message: string) {
export function connectionDisconnected(connection: Object) {
return {
type: CONNECTION_DISCONNECTED,
connection,
message
connection
};
}

View File

@ -30,14 +30,17 @@ export function hideDialog(component: ?Object) {
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @param {boolean} rawDialog - True if the dialog is a raw dialog.
* (Doesn't inherit behavior from other common frameworks).
* @returns {{
* type: OPEN_DIALOG,
* component: React.Component,
* componentProps: (Object | undefined)
* }}
*/
export function openDialog(component: Object, componentProps: ?Object) {
export function openDialog(component: Object, componentProps: ?Object, rawDialog?: boolean) {
return {
rawDialog,
type: OPEN_DIALOG,
component,
componentProps

View File

@ -17,6 +17,11 @@ type Props = {
*/
_componentProps: Object,
/**
* True if the dialog is a raw dialog (doesn't inherit behavior from other common frameworks, such as atlaskit).
*/
_rawDialog: boolean,
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
@ -52,19 +57,16 @@ export default class AbstractDialogContainer extends Component<Props> {
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _component: React.Component,
* _componentProps: Object,
* _reducedUI: boolean
* }}
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object) {
export function abstractMapStateToProps(state: Object): $Shape<Props> {
const stateFeaturesBaseDialog = state['features/base/dialog'];
const { reducedUI } = state['features/base/responsive-ui'];
return {
_component: stateFeaturesBaseDialog.component,
_componentProps: stateFeaturesBaseDialog.componentProps,
_rawDialog: stateFeaturesBaseDialog.rawDialog,
_reducedUI: reducedUI
};
}

View File

@ -20,6 +20,10 @@ class DialogContainer extends AbstractDialogContainer {
* @returns {ReactElement}
*/
render() {
if (this.props._rawDialog) {
return this._renderDialogContent();
}
return (
<ModalTransition>
{ this._renderDialogContent() }

View File

@ -21,7 +21,8 @@ ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
if (typeof component === 'undefined' || state.component === component) {
return assign(state, {
component: undefined,
componentProps: undefined
componentProps: undefined,
rawDialog: false
});
}
break;
@ -30,7 +31,8 @@ ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
case OPEN_DIALOG:
return assign(state, {
component: action.component,
componentProps: action.componentProps
componentProps: action.componentProps,
rawDialog: action.rawDialog
});
}

View File

@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4 2.89543 4.89543 2 6 2H14C15.1046 2 16 2.89543 16 4H6V18C4.89543 18 4 17.1046 4 16V4ZM10 8V20H18V8H10ZM10 6H18C19.1046 6 20 6.89543 20 8V20C20 21.1046 19.1046 22 18 22H10C8.89543 22 8 21.1046 8 20V8C8 6.89543 8.89543 6 10 6Z" fill="#5E6D7A"/>
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4 2.89543 4.89543 2 6 2H14C15.1046 2 16 2.89543 16 4H6V18C4.89543 18 4 17.1046 4 16V4ZM10 8V20H18V8H10ZM10 6H18C19.1046 6 20 6.89543 20 8V20C20 21.1046 19.1046 22 18 22H10C8.89543 22 8 21.1046 8 20V8C8 6.89543 8.89543 6 10 6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 374 B

View File

@ -31,8 +31,8 @@ export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconDragHandle } from './drag-handle.svg';
export { default as IconE2EE } from './e2ee.svg';
export { default as IconEmail } from './envelope.svg';
export { default as IconEdit } from './edit.svg';
export { default as IconEmail } from './envelope.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExclamation } from './exclamation.svg';
export { default as IconExclamationSolid } from './exclamation-solid.svg';

View File

@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@ -0,0 +1,77 @@
// @flow
import React from 'react';
import { Icon, IconArrowDown } from '../../../icons';
type Props = {
/**
* Text of the button.
*/
children: React$Node,
/**
* Text css class of the button.
*/
className?: string,
/**
* If the button is disabled or not.
*/
disabled?: boolean,
/**
* If the button has options.
*/
hasOptions?: boolean,
/**
* The type of th button: primary, secondary, text.
*/
type: string,
/**
* OnClick button handler.
*/
onClick: Function,
/**
* Click handler for options.
*/
onOptionsClick?: Function
};
/**
* Button used for pre meeting actions.
*
* @returns {ReactElement}
*/
function ActionButton({
children,
className = '',
disabled,
hasOptions,
type = 'primary',
onClick,
onOptionsClick
}: Props) {
return (
<div
className = { `action-btn ${className} ${type} ${disabled ? 'disabled' : ''}` }
onClick = { disabled ? undefined : onClick }>
{children}
{hasOptions && <div
className = 'options'
onClick = { disabled ? undefined : onOptionsClick }>
<Icon
className = 'icon'
size = { 14 }
src = { IconArrowDown } />
</div>
}
</div>
);
}
export default ActionButton;

View File

@ -2,10 +2,10 @@
import React, { Component } from 'react';
import { getCurrentConferenceUrl } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import { Icon, IconCopy, IconCheck } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { getCurrentConferenceUrl } from '../../../connection';
import { translate } from '../../../i18n';
import { Icon, IconCopy, IconCheck } from '../../../icons';
import { connect } from '../../../redux';
import logger from '../../logger';
type Props = {
@ -108,7 +108,8 @@ class CopyMeetingUrl extends Component<Props, State> {
*/
_hideCopyLink() {
this.setState({
showCopyLink: false
showCopyLink: false,
showLinkCopied: false
});
}
@ -122,7 +123,8 @@ class CopyMeetingUrl extends Component<Props, State> {
*/
_showCopyLink() {
this.setState({
showCopyLink: true
showCopyLink: true,
showLinkCopied: false
});
}
@ -152,35 +154,30 @@ class CopyMeetingUrl extends Component<Props, State> {
const { url, t } = this.props;
const { _copyUrl, _showCopyLink, _hideCopyLink } = this;
const src = showLinkCopied ? IconCheck : IconCopy;
const iconCls = showCopyLink || showCopyLink ? 'prejoin-copy-icon--white' : 'prejoin-copy-icon--light';
return (
<div
className = 'prejoin-copy-meeting'
className = 'copy-meeting'
onMouseEnter = { _showCopyLink }
onMouseLeave = { _hideCopyLink }>
<div className = 'prejoin-copy-url'>{url}</div>
{showCopyLink && <div
className = 'prejoin-copy-badge prejoin-copy-badge--hover'
onClick = { _copyUrl }>
{t('prejoin.copyAndShare')}
</div>}
{showLinkCopied && <div
className = 'prejoin-copy-badge prejoin-copy-badge--done'>
{t('prejoin.linkCopied')}
</div>}
<Icon
className = { `prejoin-copy-icon ${iconCls}` }
onClick = { _copyUrl }
size = { 24 }
src = { src } />
<div
className = { `url ${showLinkCopied ? 'done' : ''}` }
onClick = { _copyUrl } >
{ !showCopyLink && !showLinkCopied && url }
{ showCopyLink && t('prejoin.copyAndShare') }
{ showLinkCopied && t('prejoin.linkCopied') }
<Icon
onClick = { _copyUrl }
size = { 24 }
src = { src } />
</div>
<textarea
className = 'prejoin-copy-textarea'
readOnly = { true }
ref = { this.textarea }
tabIndex = '-1'
value = { url } />
</div>);
</div>
);
}
}

View File

@ -0,0 +1,175 @@
// @flow
import React, { PureComponent } from 'react';
import { getFieldValue } from '../../../react';
type Props = {
/**
* Class name to be appended to the default class list.
*/
className?: string,
/**
* Callback for the onChange event of the field.
*/
onChange: Function,
/**
* Callback to be used when the user hits Enter in the field.
*/
onSubmit?: Function,
/**
* Placeholder text for the field.
*/
placeHolder: string,
/**
* The field type (e.g. text, password...etc).
*/
type: string,
/**
* Externally provided value.
*/
value?: string
};
type State = {
/**
* True if the field is focused, false otherwise.
*/
focused: boolean,
/**
* The current value of the field.
*/
value: string
}
/**
* Implements a pre-styled input field to be used on pre-meeting screens.
*/
export default class InputField extends PureComponent<Props, State> {
static defaultProps: {
className: '',
type: 'text'
};
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
focused: false,
value: props.value || ''
};
this._onBlur = this._onBlur.bind(this);
this._onChange = this._onChange.bind(this);
this._onFocus = this._onFocus.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
}
/**
* Implements {@code PureComponent.getDerivedStateFromProps}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, state: State) {
const { value } = props;
if (state.value !== value) {
return {
...state,
value
};
}
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
return (
<input
className = { `field ${this.state.focused ? 'focused' : ''} ${this.props.className || ''}` }
onBlur = { this._onBlur }
onChange = { this._onChange }
onFocus = { this._onFocus }
onKeyDown = { this._onKeyDown }
placeholder = { this.props.placeHolder }
type = { this.props.type }
value = { this.state.value } />
);
}
_onBlur: () => void;
/**
* Callback for the onBlur event of the field.
*
* @returns {void}
*/
_onBlur() {
this.setState({
focused: false
});
}
_onChange: Object => void;
/**
* Callback for the onChange event of the field.
*
* @param {Object} evt - The static event.
* @returns {void}
*/
_onChange(evt) {
const value = getFieldValue(evt);
this.setState({
value
});
const { onChange } = this.props;
onChange && onChange(value);
}
_onFocus: () => void;
/**
* Callback for the onFocus event of the field.
*
* @returns {void}
*/
_onFocus() {
this.setState({
focused: true
});
}
_onKeyDown: Object => void;
/**
* Joins the conference on 'Enter'.
*
* @param {Event} event - Key down event object.
* @returns {void}
*/
_onKeyDown(event) {
const { onSubmit } = this.props;
onSubmit && event.key === 'Enter' && onSubmit();
}
}

View File

@ -0,0 +1,73 @@
// @flow
import React, { PureComponent } from 'react';
import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox';
import CopyMeetingUrl from './CopyMeetingUrl';
import Preview from './Preview';
type Props = {
/**
* Children component(s) to be rendered on the screen.
*/
children: React$Node,
/**
* Footer to be rendered for the page (if any).
*/
footer?: React$Node,
/**
* Title of the screen.
*/
title: string,
/**
* True if the preview overlay should be muted, false otherwise.
*/
videoMuted?: boolean,
/**
* The video track to render as preview (if omitted, the default local track will be rendered).
*/
videoTrack?: Object
}
/**
* Implements a pre-meeting screen that can be used at various pre-meeting phases, for example
* on the prejoin screen (pre-connection) or lobby (post-connection).
*/
export default class PreMeetingScreen extends PureComponent<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { title, videoMuted, videoTrack } = this.props;
return (
<div
className = 'premeeting-screen'
id = 'lobby-screen'>
<Preview
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
<div className = 'content'>
<div className = 'title'>
{ title }
</div>
<CopyMeetingUrl />
{ this.props.children }
<div className = 'media-btn-container'>
<AudioSettingsButton visible = { true } />
<VideoSettingsButton visible = { true } />
</div>
{ this.props.footer }
</div>
</div>
);
}
}

View File

@ -0,0 +1,73 @@
// @flow
import React from 'react';
import { Avatar } from '../../../avatar';
import { Video } from '../../../media';
import { connect } from '../../../redux';
import { getLocalVideoTrack } from '../../../tracks';
export type Props = {
/**
* The name of the user that is about to join.
*/
name: string,
/**
* Flag signaling the visibility of camera preview.
*/
videoMuted: boolean,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object,
};
/**
* Component showing the video preview and device status.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
function Preview(props: Props) {
const { name, videoMuted, videoTrack } = props;
if (!videoMuted && videoTrack) {
return (
<div id = 'preview'>
<Video
className = 'flipVideoX'
videoTrack = {{ jitsiTrack: videoTrack }} />
</div>
);
}
return (
<div
className = 'no-video'
id = 'preview'>
<Avatar
className = 'preview-avatar'
displayName = { name }
size = { 200 } />
</div>
);
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
return {
videoMuted: ownProps.videoTrack ? ownProps.videoMuted : state['features/base/media'].video.muted,
videoTrack: ownProps.videoTrack || (getLocalVideoTrack(state['features/base/tracks']) || {}).jitsiTrack
};
}
export default connect(_mapStateToProps)(Preview);

View File

@ -0,0 +1,5 @@
// @flow
export { default as ActionButton } from './ActionButton';
export { default as InputField } from './InputField';
export { default as PreMeetingScreen } from './PreMeetingScreen';

View File

@ -0,0 +1,3 @@
// @flow
export * from './components';

View File

@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/premeeting');

View File

@ -209,9 +209,9 @@ class Conference extends AbstractConference<Props, *> {
{ this.renderNotificationsContainer() }
{ !filmstripOnly && _showPrejoin && <Prejoin />}
<CalleeInfoContainer />
{ !filmstripOnly && _showPrejoin && <Prejoin />}
</div>
);
}

View File

@ -19,3 +19,8 @@ export const SET_LOBBY_MODE_ENABLED = 'SET_LOBBY_MODE_ENABLED';
* Action type to set the knocking state of the participant.
*/
export const SET_KNOCKING_STATE = 'SET_KNOCKING_STATE';
/**
* Action type to set the password join failed status.
*/
export const SET_PASSWORD_JOIN_FAILED = 'SET_PASSWORD_JOIN_FAILED';

View File

@ -0,0 +1,25 @@
// @flow
import { openDialog } from '../base/dialog';
import { DisableLobbyModeDialog, EnableLobbyModeDialog } from './components/native';
export * from './actions.web';
/**
* Action to show the dialog to disable lobby mode.
*
* @returns {showNotification}
*/
export function showDisableLobbyModeDialog() {
return openDialog(DisableLobbyModeDialog);
}
/**
* Action to show the dialog to enable lobby mode.
*
* @returns {showNotification}
*/
export function showEnableLobbyModeDialog() {
return openDialog(EnableLobbyModeDialog);
}

View File

@ -3,17 +3,18 @@
import { type Dispatch } from 'redux';
import { appNavigate, maybeRedirectToWelcomePage } from '../app';
import { conferenceLeft, conferenceWillJoin, getCurrentConference } from '../base/conference';
import { openDialog } from '../base/dialog';
import { conferenceWillJoin, getCurrentConference, setPassword } from '../base/conference';
import { hideDialog, openDialog } from '../base/dialog';
import { getLocalParticipant } from '../base/participants';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
import { DisableLobbyModeDialog, EnableLobbyModeDialog, LobbyScreen } from './components';
import { LobbyScreen } from './components';
declare var APP: Object;
@ -23,7 +24,7 @@ declare var APP: Object;
* @returns {Function}
*/
export function cancelKnocking() {
return async (dispatch: Dispatch<any>, getState: Function) => {
return async (dispatch: Dispatch<any>) => {
if (typeof APP !== 'undefined') {
// when we are redirecting the library should handle any
// unload and clean of the connection.
@ -33,11 +34,33 @@ export function cancelKnocking() {
return;
}
dispatch(conferenceLeft(getCurrentConference(getState)));
dispatch(appNavigate(undefined));
};
}
/**
* Action to hide the lobby screen.
*
* @returns {hideDialog}
*/
export function hideLobbyScreen() {
return hideDialog(LobbyScreen);
}
/**
* Tries to join with a preset password.
*
* @param {string} password - The password to join with.
* @returns {Function}
*/
export function joinWithPassword(password: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
dispatch(setPassword(conference, conference.join, password));
};
}
/**
* Action to be dispatched when a knocking poarticipant leaves before any response.
*
@ -54,48 +77,13 @@ export function knockingParticipantLeft(id: string) {
};
}
/**
* Action to set the knocking state of the participant.
*
* @param {boolean} knocking - The new state.
* @returns {{
* state: boolean,
* type: SET_KNOCKING_STATE
* }}
*/
export function setKnockingState(knocking: boolean) {
return {
knocking,
type: SET_KNOCKING_STATE
};
}
/**
* Starts knocking and waiting for approval.
*
* @param {string} password - The password to bypass knocking, if any.
* @returns {Function}
*/
export function startKnocking(password?: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { membersOnly } = state['features/base/conference'];
const localParticipant = getLocalParticipant(state);
dispatch(setKnockingState(true));
dispatch(conferenceWillJoin(membersOnly));
membersOnly
&& membersOnly.joinLobby(localParticipant.name, localParticipant.email, password ? password : undefined);
};
}
/**
* Action to open the lobby screen.
*
* @returns {openDialog}
*/
export function openLobbyScreen() {
return openDialog(LobbyScreen);
return openDialog(LobbyScreen, {}, true);
}
/**
@ -123,7 +111,7 @@ export function participantIsKnockingOrUpdated(participant: Object) {
*/
export function setKnockingParticipantApproval(id: string, approved: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const { conference } = getState()['features/base/conference'];
const conference = getCurrentConference(getState);
if (conference) {
if (approved) {
@ -135,6 +123,22 @@ export function setKnockingParticipantApproval(id: string, approved: boolean) {
};
}
/**
* Action to set the knocking state of the participant.
*
* @param {boolean} knocking - The new state.
* @returns {{
* state: boolean,
* type: SET_KNOCKING_STATE
* }}
*/
export function setKnockingState(knocking: boolean) {
return {
knocking,
type: SET_KNOCKING_STATE
};
}
/**
* Action to set the new state of the lobby mode.
*
@ -152,36 +156,50 @@ export function setLobbyModeEnabled(enabled: boolean) {
}
/**
* Action to show the dialog to disable lobby mode.
* Action to be dispatched when we failed to join with a password.
*
* @returns {showNotification}
* @param {boolean} failed - True of recent password join failed.
* @returns {{
* failed: boolean,
* type: SET_PASSWORD_JOIN_FAILED
* }}
*/
export function showDisableLobbyModeDialog() {
return openDialog(DisableLobbyModeDialog);
export function setPasswordJoinFailed(failed: boolean) {
return {
failed,
type: SET_PASSWORD_JOIN_FAILED
};
}
/**
* Action to show the dialog to enable lobby mode.
* Starts knocking and waiting for approval.
*
* @returns {showNotification}
* @returns {Function}
*/
export function showEnableLobbyModeDialog() {
return openDialog(EnableLobbyModeDialog);
export function startKnocking() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { membersOnly } = state['features/base/conference'];
const localParticipant = getLocalParticipant(state);
dispatch(conferenceWillJoin(membersOnly));
membersOnly.joinLobby(localParticipant.name, localParticipant.email);
dispatch(setKnockingState(true));
};
}
/**
* Action to toggle lobby mode on or off.
*
* @param {boolean} enabled - The desired (new) state of the lobby mode.
* @param {string} password - Optional password to be set.
* @returns {Function}
*/
export function toggleLobbyMode(enabled: boolean, password?: string) {
export function toggleLobbyMode(enabled: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const { conference } = getState()['features/base/conference'];
const conference = getCurrentConference(getState);
if (enabled) {
conference.enableLobby(password);
conference.enableLobby();
} else {
conference.disableLobby();
}

View File

@ -1,47 +0,0 @@
// @flow
import { PureComponent } from 'react';
import { toggleLobbyMode } from '../actions';
export type Props = {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Abstract class to encapsulate the platform common code of the {@code DisableLobbyModeDialog}.
*/
export default class AbstractDisableLobbyModeDialog<P: Props = Props> extends PureComponent<P> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this._onDisableLobbyMode = this._onDisableLobbyMode.bind(this);
}
_onDisableLobbyMode: () => void;
/**
* Callback to be invoked when the user initiates the lobby mode disable flow.
*
* @returns {void}
*/
_onDisableLobbyMode() {
this.props.dispatch(toggleLobbyMode(false));
return true;
}
}

View File

@ -1,75 +0,0 @@
// @flow
import { PureComponent } from 'react';
import { getFieldValue } from '../../base/react';
import { toggleLobbyMode } from '../actions';
export type Props = {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
type State = {
/**
* The password value entered into the field.
*/
password: string
};
/**
* Abstract class to encapsulate the platform common code of the {@code EnableLobbyModeDialog}.
*/
export default class AbstractEnableLobbyModeDialog<P: Props = Props> extends PureComponent<P, State> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this.state = {
password: ''
};
this._onEnableLobbyMode = this._onEnableLobbyMode.bind(this);
this._onChangePassword = this._onChangePassword.bind(this);
}
_onChangePassword: Object => void;
/**
* Callback to be invoked when the user changes the password.
*
* @param {SyntheticEvent} event - The SyntheticEvent instance of the change.
* @returns {void}
*/
_onChangePassword(event) {
this.setState({
password: getFieldValue(event)
});
}
_onEnableLobbyMode: () => void;
/**
* Callback to be invoked when the user initiates the lobby mode enable flow.
*
* @returns {void}
*/
_onEnableLobbyMode() {
this.props.dispatch(toggleLobbyMode(true, this.state.password));
return true;
}
}

View File

@ -3,21 +3,15 @@
import { PureComponent } from 'react';
import { isLocalParticipantModerator } from '../../base/participants';
import { isToolboxVisible } from '../../toolbox';
import { setKnockingParticipantApproval } from '../actions';
type Props = {
export type Props = {
/**
* The list of participants.
*/
_participants: Array<Object>,
/**
* True if the toolbox is visible, so we need to adjust the position.
*/
_toolboxVisible: boolean,
/**
* True if the list should be rendered.
*/
@ -37,13 +31,13 @@ type Props = {
/**
* Abstract class to encapsulate the platform common code of the {@code KnockingParticipantList}.
*/
export default class AbstractKnockingParticipantList extends PureComponent<Props> {
export default class AbstractKnockingParticipantList<P: Props = Props> extends PureComponent<P> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: P) {
super(props);
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
@ -72,11 +66,10 @@ export default class AbstractKnockingParticipantList extends PureComponent<Props
* @returns {Props}
*/
export function mapStateToProps(state: Object): $Shape<Props> {
const _participants = state['features/lobby'].knockingParticipants;
const { knockingParticipants, lobbyEnabled } = state['features/lobby'];
return {
_participants,
_toolboxVisible: isToolboxVisible(state),
_visible: isLocalParticipantModerator(state) && Boolean(_participants?.length)
_participants: knockingParticipants,
_visible: lobbyEnabled && isLocalParticipantModerator(state) && Boolean(knockingParticipants.length)
};
}

View File

@ -6,7 +6,7 @@ import { getConferenceName } from '../../base/conference';
import { getLocalParticipant } from '../../base/participants';
import { getFieldValue } from '../../base/react';
import { updateSettings } from '../../base/settings';
import { cancelKnocking, startKnocking } from '../actions';
import { cancelKnocking, joinWithPassword, setPasswordJoinFailed, startKnocking } from '../actions';
export const SCREEN_STATES = {
EDIT: 1,
@ -41,6 +41,11 @@ export type Props = {
*/
_participantName: string;
/**
* True if a recent attempt to join with password failed.
*/
_passwordJoinFailed: boolean,
/**
* The Redux dispatch function.
*/
@ -69,6 +74,11 @@ type State = {
*/
password: string,
/**
* True if a recent attempt to join with password failed.
*/
passwordJoinFailed: boolean,
/**
* The state of the screen. One of {@code SCREEN_STATES[*]}
*/
@ -78,19 +88,20 @@ type State = {
/**
* Abstract class to encapsulate the platform common code of the {@code LobbyScreen}.
*/
export default class AbstractLobbyScreen extends PureComponent<Props, State> {
export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent<P, State> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
constructor(props: P) {
super(props);
this.state = {
displayName: props._participantName || '',
email: props._participantEmail || '',
password: '',
passwordJoinFailed: false,
screenState: props._participantName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
};
@ -100,21 +111,37 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
this._onChangeEmail = this._onChangeEmail.bind(this);
this._onChangePassword = this._onChangePassword.bind(this);
this._onEnableEdit = this._onEnableEdit.bind(this);
this._onJoinWithPassword = this._onJoinWithPassword.bind(this);
this._onSwitchToKnockMode = this._onSwitchToKnockMode.bind(this);
this._onSwitchToPasswordMode = this._onSwitchToPasswordMode.bind(this);
}
/**
* Implements {@code PureComponent.getDerivedStateFromProps}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, state: State) {
if (props._passwordJoinFailed && !state.passwordJoinFailed) {
return {
password: '',
passwordJoinFailed: true
};
}
}
/**
* Returns the screen title.
*
* @returns {string}
*/
_getScreenTitleKey() {
const withPassword = Boolean(this.state.password);
const { screenState } = this.state;
const passwordPrompt = screenState === SCREEN_STATES.PASSWORD;
return this.props._knocking
? withPassword ? 'lobby.joiningWithPasswordTitle' : 'lobby.joiningTitle'
: 'lobby.joinTitle';
return !passwordPrompt && this.props._knocking
? 'lobby.joiningTitle'
: passwordPrompt ? 'lobby.enterPasswordTitle' : 'lobby.joinTitle';
}
_onAskToJoin: () => void;
@ -125,7 +152,11 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
* @returns {void}
*/
_onAskToJoin() {
this.props.dispatch(startKnocking(this.state.password));
this.setState({
password: ''
});
this.props.dispatch(startKnocking());
return false;
}
@ -211,6 +242,20 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
});
}
_onJoinWithPassword: () => void;
/**
* Callback to be invoked when the user tries to join using a preset password.
*
* @returns {void}
*/
_onJoinWithPassword() {
this.setState({
passwordJoinFailed: false
});
this.props.dispatch(joinWithPassword(this.state.password));
}
_onSwitchToKnockMode: () => void;
/**
@ -220,8 +265,10 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
*/
_onSwitchToKnockMode() {
this.setState({
password: '',
screenState: this.state.displayName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
});
this.props.dispatch(setPasswordJoinFailed(false));
}
_onSwitchToPasswordMode: () => void;
@ -244,11 +291,10 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
*/
_renderContent() {
const { _knocking } = this.props;
const { password, screenState } = this.state;
const withPassword = Boolean(password);
const { screenState } = this.state;
if (_knocking) {
return this._renderJoining(withPassword);
if (screenState !== SCREEN_STATES.PASSWORD && _knocking) {
return this._renderJoining();
}
return (
@ -267,10 +313,9 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
/**
* Renders the joining (waiting) fragment of the screen.
*
* @param {boolean} withPassword - True if we're joining with a password. False otherwise.
* @returns {React$Element}
*/
_renderJoining: boolean => React$Element<*>;
_renderJoining: () => React$Element<*>;
/**
* Renders the participant form to let the knocking participant enter its details.
@ -301,7 +346,7 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
_renderPasswordJoinButtons: () => React$Element<*>;
/**
* Renders the standard button set.
* Renders the standard (pre-knocking) button set.
*
* @returns {React$Element}
*/
@ -317,12 +362,14 @@ export default class AbstractLobbyScreen extends PureComponent<Props, State> {
export function _mapStateToProps(state: Object): $Shape<Props> {
const localParticipant = getLocalParticipant(state);
const participantId = localParticipant?.id;
const { knocking, passwordJoinFailed } = state['features/lobby'];
return {
_knocking: state['features/lobby'].knocking,
_knocking: knocking,
_meetingName: getConferenceName(state),
_participantEmail: localParticipant.email,
_participantEmail: localParticipant?.email,
_participantId: participantId,
_participantName: localParticipant.name
_participantName: localParticipant?.name,
_passwordJoinFailed: passwordJoinFailed
};
}

View File

@ -1,5 +1,3 @@
// @flow
export * from './native';
export { default as LobbyModeButton } from './LobbyModeButton';

View File

@ -1,5 +1,3 @@
// @flow
export * from './web';
export { default as LobbyModeButton } from './LobbyModeButton';

View File

@ -1,16 +1,35 @@
// @flow
import React from 'react';
import React, { PureComponent } from 'react';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractDisableLobbyModeDialog from '../AbstractDisableLobbyModeDialog';
import { toggleLobbyMode } from '../../actions';
export type Props = {
/**
* The Redux Dispatch function.
*/
dispatch: Function
};
/**
* Implements a dialog that lets the user disable the lobby mode.
*/
class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
class DisableLobbyModeDialog extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this._onDisableLobbyMode = this._onDisableLobbyMode.bind(this);
}
/**
* Implements {@code PureComponent#render}.
*
@ -25,6 +44,17 @@ class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
}
_onDisableLobbyMode: () => void;
/**
* Callback to be invoked when the user initiates the lobby mode disable flow.
*
* @returns {void}
*/
_onDisableLobbyMode() {
this.props.dispatch(toggleLobbyMode(false));
return true;
}
}
export default translate(connect()(DisableLobbyModeDialog));

View File

@ -1,37 +1,50 @@
// @flow
import React from 'react';
import { Text, TextInput, View } from 'react-native';
import React, { PureComponent } from 'react';
import { Text, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { CustomSubmitDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractEnableLobbyModeDialog, { type Props as AbstractProps } from '../AbstractEnableLobbyModeDialog';
import { toggleLobbyMode } from '../../actions';
import styles from './styles';
type Props = AbstractProps & {
type Props = {
/**
* Color schemed common style of the dialog feature.
* The Redux Dispatch function.
*/
_dialogStyles: StyleType
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Implements a dialog that lets the user enable the lobby mode.
*/
class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog<Props> {
class EnableLobbyModeDialog extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onEnableLobbyMode = this._onEnableLobbyMode.bind(this);
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _dialogStyles, t } = this.props;
return (
<CustomSubmitDialog
okKey = 'lobby.enableDialogSubmit'
@ -39,27 +52,25 @@ class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog<Props> {
titleKey = 'lobby.dialogTitle'>
<View style = { styles.formWrapper }>
<Text>
{ t('lobby.enableDialogText') }
{ this.props.t('lobby.enableDialogText') }
</Text>
<View style = { styles.fieldRow }>
<Text>
{ t('lobby.enableDialogPasswordField') }
</Text>
<TextInput
autoCapitalize = 'none'
autoCompleteType = 'off'
onChangeText = { this._onChangePassword }
secureTextEntry = { true }
style = { _dialogStyles.field } />
</View>
</View>
</CustomSubmitDialog>
);
}
_onChangePassword: Object => void;
_onEnableLobbyMode: () => void;
/**
* Callback to be invoked when the user initiates the lobby mode enable flow.
*
* @returns {void}
*/
_onEnableLobbyMode() {
this.props.dispatch(toggleLobbyMode(true));
return true;
}
}
/**

View File

@ -6,7 +6,10 @@ import { ScrollView, Text, View, TouchableOpacity } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractKnockingParticipantList, { mapStateToProps } from '../AbstractKnockingParticipantList';
import AbstractKnockingParticipantList, {
mapStateToProps as abstractMapStateToProps,
type Props
} from '../AbstractKnockingParticipantList';
import styles from './styles';
@ -20,15 +23,16 @@ class KnockingParticipantList extends AbstractKnockingParticipantList {
* @inheritdoc
*/
render() {
const { _participants, t } = this.props;
const { _participants, _visible, t } = this.props;
// On mobile we only show a portion of the list for screen real estate reasons
const participants = _participants.slice(0, 2);
if (!_visible) {
return null;
}
return (
<ScrollView
style = { styles.knockingParticipantList }>
{ participants.map(p => (
{ _participants.map(p => (
<View
key = { p.id }
style = { styles.knockingParticipantListEntry }>
@ -75,4 +79,21 @@ class KnockingParticipantList extends AbstractKnockingParticipantList {
_onRespondToParticipant: (string, boolean) => Function;
}
export default translate(connect(mapStateToProps)(KnockingParticipantList));
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
const abstractProps = abstractMapStateToProps(state);
return {
...abstractProps,
// On mobile we only show a portion of the list for screen real estate reasons
_participants: abstractProps._participants.slice(0, 2)
};
}
export default translate(connect(_mapStateToProps)(KnockingParticipantList));

View File

@ -1,11 +1,12 @@
// @flow
import { translate } from '../../base/i18n';
import { IconMeetingUnlocked, IconMeetingLocked } from '../../base/icons';
import { isLocalParticipantModerator } from '../../base/participants';
import { connect } from '../../base/redux';
import AbstractButton, { type Props as AbstractProps } from '../../base/toolbox/components/AbstractButton';
import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../actions';
import { getCurrentConference } from '../../../base/conference';
import { translate } from '../../../base/i18n';
import { IconMeetingUnlocked, IconMeetingLocked } from '../../../base/icons';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractButton, { type Props as AbstractProps } from '../../../base/toolbox/components/AbstractButton';
import { showDisableLobbyModeDialog, showEnableLobbyModeDialog } from '../../actions.native';
type Props = AbstractProps & {
@ -63,7 +64,7 @@ class LobbyModeButton extends AbstractButton<Props, any> {
* @returns {Props}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const { conference } = state['features/base/conference'];
const conference = getCurrentConference(state);
const { lobbyEnabled } = state['features/lobby'];
const lobbySupported = conference && conference.isLobbySupported();

View File

@ -54,6 +54,8 @@ class LobbyScreen extends AbstractLobbyScreen {
_onEnableEdit: () => void;
_onJoinWithPassword: () => void;
_onSwitchToKnockMode: () => void;
_onSwitchToPasswordMode: () => void;
@ -74,6 +76,7 @@ class LobbyScreen extends AbstractLobbyScreen {
<Text style = { styles.joiningMessage }>
{ this.props.t('lobby.joiningMessage') }
</Text>
{ this._renderStandardButtons() }
</>
);
}
@ -126,8 +129,7 @@ class LobbyScreen extends AbstractLobbyScreen {
</TouchableOpacity>
<Avatar
participantId = { this.props._participantId }
size = { 64 }
style = { styles.avatar } />
size = { 64 } />
<Text style = { styles.displayNameText }>
{ displayName }
</Text>
@ -144,6 +146,8 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderPasswordForm() {
const { _passwordJoinFailed, t } = this.props;
return (
<View style = { styles.formWrapper }>
<Text style = { styles.fieldLabel }>
@ -156,6 +160,9 @@ class LobbyScreen extends AbstractLobbyScreen {
secureTextEntry = { true }
style = { styles.field }
value = { this.state.password } />
{ _passwordJoinFailed && <Text style = { styles.fieldError }>
{ t('lobby.invalidPassword') }
</Text> }
</View>
);
}
@ -172,7 +179,7 @@ class LobbyScreen extends AbstractLobbyScreen {
<>
<TouchableOpacity
disabled = { !this.state.password }
onPress = { this._onAskToJoin }
onPress = { this._onJoinWithPassword }
style = { [
styles.button,
styles.primaryButton
@ -201,11 +208,11 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderStandardButtons() {
const { t } = this.props;
const { _knocking, t } = this.props;
return (
<>
<TouchableOpacity
{ _knocking || <TouchableOpacity
disabled = { !this.state.displayName }
onPress = { this._onAskToJoin }
style = { [
@ -215,7 +222,7 @@ class LobbyScreen extends AbstractLobbyScreen {
<Text style = { styles.primaryButtonText }>
{ t('lobby.knockButton') }
</Text>
</TouchableOpacity>
</TouchableOpacity> }
<TouchableOpacity
onPress = { this._onSwitchToPasswordMode }
style = { [

View File

@ -3,4 +3,5 @@
export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbyModeButton } from './LobbyModeButton';
export { default as LobbyScreen } from './LobbyScreen';

View File

@ -1,12 +1,10 @@
// @flow
import { ColorPalette } from '../../../base/styles';
const SECONDARY_COLOR = '#B8C7E0';
export default {
avatar: {
borderColor: 'red'
},
button: {
alignItems: 'center',
borderRadius: 4,
@ -49,6 +47,11 @@ export default {
padding: 8
},
fieldError: {
color: ColorPalette.warning,
fontSize: 10
},
fieldRow: {
paddingTop: 16
},

View File

@ -1,36 +0,0 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractDisableLobbyModeDialog from '../AbstractDisableLobbyModeDialog';
/**
* Implements a dialog that lets the user disable the lobby mode.
*/
class DisableLobbyModeDialog extends AbstractDisableLobbyModeDialog {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { t } = this.props;
return (
<Dialog
className = 'lobby-screen'
okKey = 'lobby.disableDialogSubmit'
onSubmit = { this._onDisableLobbyMode }
titleKey = 'lobby.dialogTitle'>
{ t('lobby.disableDialogContent') }
</Dialog>
);
}
_onDisableLobbyMode: () => void;
}
export default translate(connect()(DisableLobbyModeDialog));

View File

@ -1,51 +0,0 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractEnableLobbyModeDialog from '../AbstractEnableLobbyModeDialog';
/**
* Implements a dialog that lets the user enable the lobby mode.
*/
class EnableLobbyModeDialog extends AbstractEnableLobbyModeDialog {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { t } = this.props;
return (
<Dialog
className = 'lobby-screen'
okKey = 'lobby.enableDialogSubmit'
onSubmit = { this._onEnableLobbyMode }
titleKey = 'lobby.dialogTitle'>
<div id = 'lobby-dialog'>
<span className = 'description'>
{ t('lobby.enableDialogText') }
</span>
<div className = 'field'>
<label htmlFor = 'password'>
{ t('lobby.enableDialogPasswordField') }
</label>
<input
onChange = { this._onChangePassword }
type = 'password'
value = { this.state.password } />
</div>
</div>
</Dialog>
);
}
_onChangePassword: Object => void;
_onEnableLobbyMode: () => void;
}
export default translate(connect()(EnableLobbyModeDialog));

View File

@ -5,12 +5,24 @@ import React from 'react';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractKnockingParticipantList, { mapStateToProps } from '../AbstractKnockingParticipantList';
import { isToolboxVisible } from '../../../toolbox';
import AbstractKnockingParticipantList, {
mapStateToProps as abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractKnockingParticipantList';
type Props = AbstractProps & {
/**
* True if the toolbox is visible, so we need to adjust the position.
*/
_toolboxVisible: boolean,
};
/**
* Component to render a list for the actively knocking participants.
*/
class KnockingParticipantList extends AbstractKnockingParticipantList {
class KnockingParticipantList extends AbstractKnockingParticipantList<Props> {
/**
* Implements {@code PureComponent#render}.
*
@ -69,4 +81,17 @@ class KnockingParticipantList extends AbstractKnockingParticipantList {
_onRespondToParticipant: (string, boolean) => Function;
}
export default translate(connect(mapStateToProps)(KnockingParticipantList));
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
return {
...abstractMapStateToProps(state),
_toolboxVisible: isToolboxVisible(state)
};
}
export default translate(connect(_mapStateToProps)(KnockingParticipantList));

View File

@ -2,13 +2,13 @@
import React from 'react';
import { Avatar } from '../../../base/avatar';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { Icon, IconEdit } from '../../../base/icons';
import { ActionButton, InputField, PreMeetingScreen } from '../../../base/premeeting';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
import AbstractLobbyScreen, {
_mapStateToProps
} from '../AbstractLobbyScreen';
/**
* Implements a waiting screen that represents the participant being in the lobby.
@ -20,27 +20,10 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
render() {
const { _meetingName, t } = this.props;
return (
<Dialog
disableBlanketClickDismiss = { false }
disableEnter = { true }
hideCancelButton = { true }
isModal = { false }
onCancel = { this._onCancel }
submitDisabled = { true }
width = 'small'>
<div id = 'lobby-screen'>
<span className = 'title'>
{ t(this._getScreenTitleKey()) }
</span>
<span className = 'roomName'>
{ _meetingName }
</span>
{ this._renderContent() }
</div>
</Dialog>
<PreMeetingScreen title = { this.props.t(this._getScreenTitleKey()) }>
{ this._renderContent() }
</PreMeetingScreen>
);
}
@ -58,6 +41,8 @@ class LobbyScreen extends AbstractLobbyScreen {
_onEnableEdit: () => void;
_onJoinWithPassword: () => void;
_onSubmit: () => boolean;
_onSwitchToKnockMode: () => void;
@ -71,42 +56,16 @@ class LobbyScreen extends AbstractLobbyScreen {
*
* @inheritdoc
*/
_renderJoining(withPassword) {
_renderJoining() {
return (
<div className = 'joiningContainer'>
<LoadingIndicator />
<span>
{ this.props.t(`lobby.${withPassword ? 'joinWithPasswordMessage' : 'joiningMessage'}`) }
<div className = 'container'>
<div className = 'spinner'>
<LoadingIndicator size = 'large' />
</div>
<span className = 'joining-message'>
{ this.props.t('lobby.joiningMessage') }
</span>
</div>
);
}
/**
* Renders the participant form to let the knocking participant enter its details.
*
* @inheritdoc
*/
_renderParticipantForm() {
const { t } = this.props;
const { displayName, email } = this.state;
return (
<div className = 'form'>
<span>
{ t('lobby.nameField') }
</span>
<input
onChange = { this._onChangeDisplayName }
type = 'text'
value = { displayName } />
<span>
{ t('lobby.emailField') }
</span>
<input
onChange = { this._onChangeEmail }
type = 'email'
value = { email } />
{ this._renderStandardButtons() }
</div>
);
}
@ -118,26 +77,21 @@ class LobbyScreen extends AbstractLobbyScreen {
*/
_renderParticipantInfo() {
const { displayName, email } = this.state;
const { _participantId } = this.props;
const { t } = this.props;
return (
<div className = 'participantInfo'>
<div className = 'editButton'>
<button
onClick = { this._onEnableEdit }
type = 'button'>
<Icon src = { IconEdit } />
</button>
<div className = 'participant-info'>
<div className = 'form'>
<InputField
onChange = { this._onChangeDisplayName }
placeHolder = { t('lobby.nameField') }
value = { displayName } />
<InputField
onChange = { this._onChangeEmail }
placeHolder = { t('lobby.emailField') }
value = { email } />
</div>
<Avatar
participantId = { _participantId }
size = { 64 } />
<span className = 'displayName'>
{ displayName }
</span>
<span className = 'email'>
{ email }
</span>
</div>
);
}
@ -148,13 +102,14 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderPasswordForm() {
const { _passwordJoinFailed, t } = this.props;
return (
<div className = 'form'>
<span>
{ this.props.t('lobby.passwordField') }
</span>
<input
<InputField
className = { _passwordJoinFailed ? 'error' : '' }
onChange = { this._onChangePassword }
placeHolder = { _passwordJoinFailed ? t('lobby.invalidPassword') : t('lobby.passwordField') }
type = 'password'
value = { this.state.password } />
</div>
@ -171,19 +126,17 @@ class LobbyScreen extends AbstractLobbyScreen {
return (
<>
<button
className = 'primary'
<ActionButton
disabled = { !this.state.password }
onClick = { this._onAskToJoin }
type = 'submit'>
onClick = { this._onJoinWithPassword }
type = 'primary'>
{ t('lobby.passwordJoinButton') }
</button>
<button
className = 'borderLess'
</ActionButton>
<ActionButton
onClick = { this._onSwitchToKnockMode }
type = 'button'>
type = 'secondary'>
{ t('lobby.backToKnockModeButton') }
</button>
</ActionButton>
</>
);
}
@ -194,23 +147,21 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderStandardButtons() {
const { t } = this.props;
const { _knocking, t } = this.props;
return (
<>
<button
className = 'primary'
{ _knocking || <ActionButton
disabled = { !this.state.displayName }
onClick = { this._onAskToJoin }
type = 'submit'>
type = 'primary'>
{ t('lobby.knockButton') }
</button>
<button
className = 'borderLess'
</ActionButton> }
<ActionButton
onClick = { this._onSwitchToPasswordMode }
type = 'button'>
type = 'secondary'>
{ t('lobby.enterPasswordButton') }
</button>
</ActionButton>
</>
);
}

View File

@ -0,0 +1,136 @@
// @flow
import React, { PureComponent } from 'react';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { Switch } from '../../../base/react';
import { connect } from '../../../base/redux';
import { toggleLobbyMode } from '../../actions';
type Props = {
/**
* True if lobby is currently enabled in the conference.
*/
_lobbyEnabled: boolean,
/**
* True if the section should be visible.
*/
_visible: boolean,
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
type State = {
/**
* True if the lobby switch is toggled on.
*/
lobbyEnabled: boolean
}
/**
* Implements a security feature section to control lobby mode.
*/
class LobbySection extends PureComponent<Props, State> {
/**
* Instantiates a new component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
lobbyEnabled: props._lobbyEnabled
};
this._onToggleLobby = this._onToggleLobby.bind(this);
}
/**
* Implements {@code PureComponent#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps, prevState) {
if (this.props._lobbyEnabled !== prevProps._lobbyEnabled
&& this.state.lobbyEnabled !== prevState.lobbyEnabled) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
lobbyEnabled: this.props._lobbyEnabled
});
}
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _visible, t } = this.props;
if (!_visible) {
return null;
}
return (
<div id = 'lobby-section'>
{ t('lobby.enableDialogText') }
<div className = 'control-row'>
<label>
{ t('lobby.toggleLabel') }
</label>
<Switch
onValueChange = { this._onToggleLobby }
value = { this.state.lobbyEnabled } />
</div>
</div>
);
}
_onToggleLobby: () => void;
/**
* Callback to be invoked when the user toggles the lobby feature on or off.
*
* @returns {void}
*/
_onToggleLobby() {
const newValue = !this.state.lobbyEnabled;
this.setState({
lobbyEnabled: newValue
});
this.props.dispatch(toggleLobbyMode(newValue));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function mapStateToProps(state: Object): $Shape<Props> {
const { conference } = state['features/base/conference'];
return {
_lobbyEnabled: state['features/lobby'].lobbyEnabled,
_visible: conference && conference.isLobbySupported() && isLocalParticipantModerator(state)
};
}
export default translate(connect(mapStateToProps)(LobbySection));

View File

@ -1,6 +1,5 @@
// @flow
export { default as DisableLobbyModeDialog } from './DisableLobbyModeDialog';
export { default as EnableLobbyModeDialog } from './EnableLobbyModeDialog';
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbySection } from './LobbySection';
export { default as LobbyScreen } from './LobbyScreen';

View File

@ -1,22 +1,6 @@
// @flow
declare var interfaceConfig: Object;
/**
* Returns a displayable name for the knocking participant.
*
* @param {string} name - The received name.
* @returns {string}
*/
export function getKnockingParticipantDisplayName(name: string) {
if (name) {
return name;
}
return typeof interfaceConfig === 'object'
? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME
: 'Fellow Jitster';
}
import { getCurrentConference } from '../base/conference';
/**
* Approves (lets in) or rejects a knocking participant.
@ -27,7 +11,7 @@ export function getKnockingParticipantDisplayName(name: string) {
* @returns {Function}
*/
export function setKnockingParticipantApproval(getState: Function, id: string, approved: boolean) {
const { conference } = getState()['features/base/conference'];
const conference = getCurrentConference(getState());
if (conference) {
if (approved) {

View File

@ -1,20 +1,22 @@
// @flow
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
import { hideDialog } from '../base/dialog';
import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getFirstLoadableAvatarUrl } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { NOTIFICATION_TYPE, showNotification } from '../notifications';
import { isPrejoinPageEnabled } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED } from './actionTypes';
import {
hideLobbyScreen,
knockingParticipantLeft,
openLobbyScreen,
participantIsKnockingOrUpdated,
setLobbyModeEnabled
setLobbyModeEnabled,
startKnocking,
setPasswordJoinFailed
} from './actions';
import { LobbyScreen } from './components';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
@ -76,25 +78,38 @@ StateListenerRegistry.register(
* @param {Object} action - The Redux action.
* @returns {Object}
*/
function _conferenceFailed({ dispatch }, next, action) {
function _conferenceFailed({ dispatch, getState }, next, action) {
const { error } = action;
const state = getState();
const nonFirstFailure = Boolean(state['features/base/conference'].membersOnly);
if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
dispatch(openLobbyScreen());
} else {
dispatch(hideDialog(LobbyScreen));
const result = next(action);
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
dispatch(showNotification({
appearance: NOTIFICATION_TYPE.ERROR,
hideErrorSupportLink: true,
titleKey: 'lobby.joinRejectedMessage'
}));
dispatch(openLobbyScreen());
if (isPrejoinPageEnabled(state) && !state['features/lobby'].knocking) {
// prejoin is enabled, so we knock automatically
dispatch(startKnocking());
}
dispatch(setPasswordJoinFailed(nonFirstFailure));
return result;
}
dispatch(hideLobbyScreen());
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
dispatch(showNotification({
appearance: NOTIFICATION_TYPE.ERROR,
hideErrorSupportLink: true,
titleKey: 'lobby.joinRejectedMessage'
}));
}
return next(action);
@ -109,7 +124,7 @@ function _conferenceFailed({ dispatch }, next, action) {
* @returns {Object}
*/
function _conferenceJoined({ dispatch }, next, action) {
dispatch(hideDialog(LobbyScreen));
dispatch(hideLobbyScreen());
return next(action);
}
@ -123,8 +138,9 @@ function _conferenceJoined({ dispatch }, next, action) {
*/
function _findLoadableAvatarForKnockingParticipant({ dispatch, getState }, { id }) {
const updatedParticipant = getState()['features/lobby'].knockingParticipants.find(p => p.id === id);
const { disableThirdPartyRequests } = getState()['features/base/config'];
if (updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
if (!disableThirdPartyRequests && updatedParticipant && !updatedParticipant.loadableAvatarUrl) {
getFirstLoadableAvatarUrl(updatedParticipant).then(loadableAvatarUrl => {
if (loadableAvatarUrl) {
dispatch(participantIsKnockingOrUpdated({

View File

@ -1,19 +1,21 @@
// @flow
import { CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_LEFT } from '../base/conference';
import { CONFERENCE_JOINED, CONFERENCE_LEFT, SET_PASSWORD } from '../base/conference';
import { ReducerRegistry } from '../base/redux';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
const DEFAULT_STATE = {
knocking: false,
knockingParticipants: [],
lobbyEnabled: false
lobbyEnabled: false,
passwordJoinFailed: false
};
/**
@ -26,12 +28,12 @@ const DEFAULT_STATE = {
*/
ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CONFERENCE_FAILED:
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
return {
...state,
knocking: false
knocking: false,
passwordJoinFailed: false
};
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
return _knockingParticipantArrivedOrUpdated(action.participant, state);
@ -43,13 +45,24 @@ ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
case SET_KNOCKING_STATE:
return {
...state,
knocking: action.knocking
knocking: action.knocking,
passwordJoinFailed: false
};
case SET_LOBBY_MODE_ENABLED:
return {
...state,
lobbyEnabled: action.enabled
};
case SET_PASSWORD:
return {
...state,
passwordJoinFailed: false
};
case SET_PASSWORD_JOIN_FAILED:
return {
...state,
passwordJoinFailed: action.failed
};
}
return state;

View File

@ -10,7 +10,7 @@ declare var APP: Object;
/**
* List of errors that are not fatal (or handled differently) so then the overlays won't kick in.
*/
const NON_FATAR_ERRORS = [
const NON_OVERLAY_ERRORS = [
JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED,
JitsiConferenceErrors.CONFERENCE_DESTROYED,
JitsiConferenceErrors.CONNECTION_ERROR
@ -31,7 +31,7 @@ StateListenerRegistry.register(
},
/* listener */ (error, { dispatch }) => {
error
&& NON_FATAR_ERRORS.indexOf(error.name) === -1
&& NON_OVERLAY_ERRORS.indexOf(error.name) === -1
&& typeof error.recoverable === 'undefined'
&& dispatch(setFatalError(error));
}

View File

@ -6,10 +6,9 @@ import React, { Component } from 'react';
import { getRoomName } from '../../base/conference';
import { translate } from '../../base/i18n';
import { Icon, IconPhone, IconVolumeOff } from '../../base/icons';
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
import { connect } from '../../base/redux';
import { getDisplayName, updateSettings } from '../../base/settings';
import { isGuest } from '../../invite';
import { VideoSettingsButton, AudioSettingsButton } from '../../toolbox';
import {
joinConference as joinConferenceAction,
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
@ -17,18 +16,15 @@ import {
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
} from '../actions';
import {
getActiveVideoTrack,
isJoinByPhoneButtonVisible,
isDeviceStatusVisible,
isJoinByPhoneDialogVisible
isJoinByPhoneDialogVisible,
isPrejoinVideoMuted
} from '../functions';
import ActionButton from './buttons/ActionButton';
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
import CopyMeetingUrl from './preview/CopyMeetingUrl';
import DeviceStatus from './preview/DeviceStatus';
import ParticipantName from './preview/ParticipantName';
import Preview from './preview/Preview';
type Props = {
@ -42,11 +38,6 @@ type Props = {
*/
hasJoinByPhoneButton: boolean,
/**
* Flag signaling if a user is logged in or not.
*/
isAnonymousUser: boolean,
/**
* Joins the current meeting.
*/
@ -82,6 +73,11 @@ type Props = {
*/
setJoinByPhoneDialogVisiblity: Function,
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean,
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
@ -91,6 +87,11 @@ type Props = {
* Used for translation.
*/
t: Function,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object,
};
type State = {
@ -211,34 +212,31 @@ class Prejoin extends Component<Props, State> {
*/
render() {
const {
deviceStatusVisible,
hasJoinByPhoneButton,
isAnonymousUser,
joinConference,
joinConferenceWithoutAudio,
name,
showCameraPreview,
showDialog,
t
t,
videoTrack
} = this.props;
const { _closeDialog, _onCheckboxChange, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
const { showJoinByPhoneButtons } = this.state;
return (
<div className = 'prejoin-full-page'>
<Preview name = { name } />
<PreMeetingScreen
footer = { this._renderFooter() }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
videoTrack = { videoTrack }>
<div className = 'prejoin-input-area-container'>
<div className = 'prejoin-input-area'>
<div className = 'prejoin-title'>
{t('prejoin.joinMeeting')}
</div>
<CopyMeetingUrl />
<ParticipantName
isEditable = { isAnonymousUser }
joinConference = { joinConference }
setName = { _setName }
<InputField
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
value = { name } />
<div className = 'prejoin-preview-dropdown-container'>
@ -275,11 +273,6 @@ class Prejoin extends Component<Props, State> {
</ActionButton>
</InlineDialog>
</div>
<div className = 'prejoin-preview-btn-container'>
<AudioSettingsButton visible = { true } />
<VideoSettingsButton visible = { true } />
</div>
</div>
<div className = 'prejoin-checkbox-container'>
@ -290,16 +283,23 @@ class Prejoin extends Component<Props, State> {
<span>{t('prejoin.doNotShow')}</span>
</div>
</div>
{ deviceStatusVisible && <DeviceStatus /> }
{ showDialog && (
<JoinByPhoneDialog
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
onClose = { _closeDialog } />
)}
</div>
</PreMeetingScreen>
);
}
/**
* Renders the screen footer if any.
*
* @returns {React$Element}
*/
_renderFooter() {
return this.props.deviceStatusVisible && <DeviceStatus />;
}
}
/**
@ -310,12 +310,13 @@ class Prejoin extends Component<Props, State> {
*/
function mapStateToProps(state): Object {
return {
isAnonymousUser: isGuest(state),
deviceStatusVisible: isDeviceStatusVisible(state),
name: getDisplayName(state),
roomName: getRoomName(state),
showDialog: isJoinByPhoneDialogVisible(state),
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state)
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
showCameraPreview: !isPrejoinVideoMuted(state),
videoTrack: getActiveVideoTrack(state)
};
}

View File

@ -1,88 +0,0 @@
// @flow
import React from 'react';
import { Icon, IconArrowDown } from '../../../base/icons';
const classNameByType = {
primary: 'prejoin-btn--primary',
secondary: 'prejoin-btn--secondary',
text: 'prejoin-btn--text'
};
type Props = {
/**
* Text of the button.
*/
children: React$Node,
/**
* Text css class of the button.
*/
className?: string,
/**
* If the button is disabled or not.
*/
disabled?: boolean,
/**
* If the button has options.
*/
hasOptions?: boolean,
/**
* The type of th button: primary, secondary, text.
*/
type: string,
/**
* OnClick button handler.
*/
onClick: Function,
/**
* Click handler for options.
*/
onOptionsClick?: Function
};
/**
* Button used for prejoin actions: Join/Join without audio/Join by phone.
*
* @returns {ReactElement}
*/
function ActionButton({ children, className, disabled, hasOptions, type, onClick, onOptionsClick }: Props) {
let ownClassName = 'prejoin-btn';
let clickHandler = onClick;
let optionsClickHandler = onOptionsClick;
if (disabled) {
clickHandler = null;
optionsClickHandler = null;
ownClassName = `${ownClassName} prejoin-btn--disabled`;
} else {
ownClassName = `${ownClassName} ${classNameByType[type]}`;
}
const cls = className ? `${className} ${ownClassName}` : ownClassName;
return (
<div
className = { cls }
onClick = { clickHandler }>
{children}
{hasOptions && <div
className = 'prejoin-btn-options'
onClick = { optionsClickHandler }>
<Icon
className = 'prejoin-btn-icon'
size = { 14 }
src = { IconArrowDown } />
</div>
}
</div>
);
}
export default ActionButton;

View File

@ -4,9 +4,9 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconArrowLeft } from '../../../base/icons';
import { ActionButton } from '../../../base/premeeting';
import { getCountryCodeFromPhone } from '../../utils';
import Label from '../Label';
import ActionButton from '../buttons/ActionButton';
type Props = {

View File

@ -4,8 +4,8 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { ActionButton } from '../../../base/premeeting';
import Label from '../Label';
import ActionButton from '../buttons/ActionButton';
import CountryPicker from '../country-picker/CountryPicker';
type Props = {

View File

@ -62,7 +62,9 @@ function DeviceStatus({ deviceStatusType, deviceStatusText, rawError, t }: Props
size = { 16 }
src = { src } />
<span className = 'prejoin-preview-error-desc'>{t(deviceStatusText)}</span>
<span>{rawError}</span>
{ rawError && <span>
{ rawError }
</span> }
</div>
);
}

View File

@ -1,110 +0,0 @@
// @flow
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
type Props = {
/**
* Flag signaling if the name is ediable or not.
*/
isEditable: boolean,
/**
* Joins the current meeting.
*/
joinConference: Function,
/**
* Sets the name for the joining user.
*/
setName: Function,
/**
* Used to obtain translations.
*/
t: Function,
/**
* The text to be displayed.
*/
value: string,
};
/**
* Participant name - can be an editable input or just the text name.
*
* @returns {ReactElement}
*/
class ParticipantName extends Component<Props> {
/**
* Initializes a new {@code ParticipantName} instance.
*
* @param {Props} props - The props of the component.
* @inheritdoc
*/
constructor(props) {
super(props);
this._onKeyDown = this._onKeyDown.bind(this);
this._onNameChange = this._onNameChange.bind(this);
}
_onKeyDown: () => void;
/**
* Joins the conference on 'Enter'.
*
* @param {Event} event - Key down event object.
* @returns {void}
*/
_onKeyDown(event) {
if (event.key === 'Enter') {
this.props.joinConference();
}
}
_onNameChange: () => void;
/**
* Handler used for changing the guest user name.
*
* @returns {undefined}
*/
_onNameChange({ target: { value } }) {
this.props.setName(value);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { value, isEditable, t } = this.props;
const { _onKeyDown, _onNameChange } = this;
return isEditable ? (
<input
autoFocus = { true }
className = 'prejoin-preview-name prejoin-preview-name--editable'
onChange = { _onNameChange }
onKeyDown = { _onKeyDown }
placeholder = { t('dialog.enterDisplayName') }
value = { value } />
)
: <div
className = 'prejoin-preview-name prejoin-preview-name--text'
onKeyDown = { _onKeyDown }
tabIndex = '0' >
{value}
</div>
;
}
}
export default translate(ParticipantName);

View File

@ -1,76 +0,0 @@
// @flow
import React from 'react';
import { Avatar } from '../../../base/avatar';
import { Video } from '../../../base/media';
import { connect } from '../../../base/redux';
import { getActiveVideoTrack, isPrejoinVideoMuted } from '../../functions';
export type Props = {
/**
* The name of the user that is about to join.
*/
name: string,
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object,
};
/**
* Component showing the video preview and device status.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
function Preview(props: Props) {
const {
name,
showCameraPreview,
videoTrack
} = props;
if (showCameraPreview && videoTrack) {
return (
<div className = 'prejoin-preview'>
<div className = 'prejoin-preview-overlay' />
<div className = 'prejoin-preview-bottom-overlay' />
<Video
className = 'flipVideoX prejoin-preview-video'
videoTrack = {{ jitsiTrack: videoTrack }} />
</div>
);
}
return (
<div className = 'prejoin-preview prejoin-preview--no-video'>
<Avatar
className = 'prejoin-preview-avatar'
displayName = { name }
size = { 200 } />
</div>
);
}
/**
* Maps the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
return {
videoTrack: getActiveVideoTrack(state),
showCameraPreview: !isPrejoinVideoMuted(state)
};
}
export default connect(mapStateToProps)(Preview);

View File

@ -259,7 +259,8 @@ export function isJoinByPhoneDialogVisible(state: Object): boolean {
* @returns {boolean}
*/
export function isPrejoinPageEnabled(state: Object): boolean {
return state['features/base/config'].prejoinPageEnabled
return navigator.product !== 'ReactNative'
&& state['features/base/config'].prejoinPageEnabled
&& !state['features/base/settings'].userSelectedSkipPrejoin;
}

View File

@ -169,19 +169,22 @@ function PasswordSection({
}
return (
<div className = 'security-dialog password'>
<div
className = 'info-dialog info-dialog-column info-dialog-password'
ref = { formRef }>
<PasswordForm
editEnabled = { passwordEditEnabled }
locked = { locked }
onSubmit = { onPasswordSubmit }
password = { password }
passwordNumberOfDigits = { passwordNumberOfDigits } />
</div>
<div className = 'security-dialog password-actions'>
{ renderPasswordActions() }
<div className = 'security-dialog password-section'>
{ t('security.about') }
<div className = 'security-dialog password'>
<div
className = 'info-dialog info-dialog-column info-dialog-password'
ref = { formRef }>
<PasswordForm
editEnabled = { passwordEditEnabled }
locked = { locked }
onSubmit = { onPasswordSubmit }
password = { password }
passwordNumberOfDigits = { passwordNumberOfDigits } />
</div>
<div className = 'security-dialog password-actions'>
{ renderPasswordActions() }
</div>
</div>
</div>
);

View File

@ -7,6 +7,7 @@ import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { LobbySection } from '../../../lobby';
import Header from './Header';
import PasswordSection from './PasswordSection';
@ -62,8 +63,7 @@ function SecurityDialog({
_locked,
_password,
_passwordNumberOfDigits,
setPassword,
t
setPassword
}: Props) {
const [ passwordEditEnabled, setPasswordEditEnabled ] = useState(false);
@ -81,8 +81,8 @@ function SecurityDialog({
titleKey = 'security.securityOptions'
width = { 'small' }>
<div className = 'security-dialog'>
{ t('security.about') }
<div className = 'invite-more-dialog separator' />
<LobbySection />
<div className = 'separator-line' />
<PasswordSection
canEditPassword = { _canEditPassword }
conference = { _conference }

View File

@ -63,9 +63,10 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
*/
function mapStateToProps(state: Object) {
const { locked } = state['features/base/conference'];
const { lobbyEnabled } = state['features/lobby'];
return {
_locked: locked
_locked: locked || lobbyEnabled
};
}

View File

@ -11,7 +11,7 @@ import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import { SharedDocumentButton } from '../../../etherpad';
import { InviteButton } from '../../../invite';
import { LobbyModeButton } from '../../../lobby';
import { LobbyModeButton } from '../../../lobby/components/native';
import { AudioRouteButton } from '../../../mobile/audio-mode';
import { LiveStreamButton, RecordButton } from '../../../recording';
import { RoomLockButton } from '../../../room-lock';

View File

@ -38,7 +38,6 @@ import { SharedDocumentButton } from '../../../etherpad';
import { openFeedbackDialog } from '../../../feedback';
import { beginAddPeople } from '../../../invite';
import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
import { LobbyModeButton } from '../../../lobby';
import {
LocalRecordingButton,
LocalRecordingInfoDialog
@ -1189,9 +1188,6 @@ class Toolbox extends Component<Props, State> {
if (this._shouldShowButton('closedcaptions')) {
buttonsLeft.push('closedcaptions');
}
if (this._shouldShowButton('lobby')) {
buttonsRight.push('lobby');
}
if (overflowHasItems) {
buttonsRight.push('overflowmenu');
}
@ -1275,8 +1271,6 @@ class Toolbox extends Component<Props, State> {
{ this._renderVideoButton() }
</div>
<div className = 'button-group-right'>
{ (buttonsRight.indexOf('lobby') !== -1)
&& <LobbyModeButton /> }
{ buttonsRight.indexOf('localrecording') !== -1
&& <LocalRecordingButton
onClick = {