From 908712b96f8af74e5d72e7fd10eb4b35f78b1693 Mon Sep 17 00:00:00 2001 From: Vlad Piersec Date: Thu, 7 May 2020 10:42:55 +0300 Subject: [PATCH] feat(prejoin_page): Rework prejoin page * Add a checkbox for skipping the prejoin page on next use. (This is hidden for now, until we also have a settings entry for it). * Rework 'Join by Phone' buttons and add new overlay. * Update the device status accordingly if there were errors while adding devices. * The input is filled with the display name if there was one previously used. * Join the meeting on 'Enter' press. --- css/_prejoin.scss | 90 ++++++++- lang/main.json | 3 +- react/features/base/devices/middleware.js | 22 ++- react/features/base/icons/svg/index.js | 1 + react/features/base/icons/svg/volume-off.svg | 3 + react/features/base/settings/functions.web.js | 11 ++ react/features/base/settings/reducer.js | 3 +- react/features/prejoin/actionTypes.js | 10 +- react/features/prejoin/actions.js | 27 ++- react/features/prejoin/components/Prejoin.js | 179 ++++++++++++++---- .../components/buttons/ActionButton.js | 23 ++- .../components/preview/CopyMeetingUrl.js | 1 + .../components/preview/ParticipantName.js | 33 +++- .../prejoin/components/preview/Preview.js | 4 +- react/features/prejoin/functions.js | 13 +- react/features/prejoin/middleware.js | 39 ++-- react/features/prejoin/reducer.js | 10 +- 17 files changed, 355 insertions(+), 117 deletions(-) create mode 100644 react/features/base/icons/svg/volume-off.svg diff --git a/css/_prejoin.scss b/css/_prejoin.scss index 99686f96e..83f1de9d3 100644 --- a/css/_prejoin.scss +++ b/css/_prejoin.scss @@ -9,9 +9,9 @@ &-input-area-container { position: absolute; - bottom: 128px; + bottom: 48px; width: 100%; - z-index: 1; + z-index: 2; } &-input-area { @@ -34,8 +34,8 @@ display: inline-block; font-size: 15px; line-height: 24px; - margin-bottom: 16px; padding: 7px 16px; + position: relative; text-align: center; width: 286px; @@ -51,11 +51,24 @@ &--text { width: auto; + font-size: 13px; margin: 0; padding: 0; } } + &-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; @@ -69,6 +82,25 @@ text-align: center; width: 100%; } + + &-checkbox { + border: 0; + height: 16px; + margin-right: 8px; + padding: 0; + width: 16px; + } + + &-checkbox-container { + align-items: center; + color: #fff; + display: none; + font-size: 13px; + justify-content: center; + line-height: 20px; + margin-top: 16px; + width: 100%; + } } @mixin name-placeholder { @@ -128,10 +160,8 @@ &-btn-container { display: flex; justify-content: center; - position: absolute; - bottom: 50px; + margin-top: 32px; width: 100%; - z-index: 1; &> div { margin: 0 12px; @@ -151,7 +181,16 @@ position: absolute; width: 100%; z-index: 1; - background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)), linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 54.25%); + background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)); + } + + &-bottom-overlay { + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.9) 100%); + bottom: 0; + height: 50%; + position: absolute; + width: 100%; + z-index: 1; } &-status { @@ -192,6 +231,43 @@ width: 49px; margin: 0 8px; } + + &-dropdown-btns { + width: 320px; + padding: 8px 0; + } + + &-dropdown-btn { + align-items: center; + color: #1C2025; + cursor: pointer; + display: flex; + height: 40px; + font-size: 15px; + line-height: 24px; + padding: 0 16px; + + &:hover { + background-color: #DAEBFA; + } + } + + &-dropdown-icon { + display: inline-block; + margin-right: 16px; + + & > svg { + fill: #1C2025; + } + } + + &-dropdown-container { + & > div > div:nth-child(2) > div > div { + background: #fff; + padding: 0; + } + } + } .prejoin-copy { diff --git a/lang/main.json b/lang/main.json index 267340cfb..e6234bff0 100644 --- a/lang/main.json +++ b/lang/main.json @@ -488,13 +488,14 @@ "dialInMeeting": "Dial into the meeting", "dialInPin": "Dial into the meeting and enter PIN code:", "dialing": "Dialing", + "doNotShow": "Don't show this again", "iWantToDialIn": "I want to dial in", "joinAudioByPhone": "Join with phone audio", "joinMeeting": "Join meeting", "joinWithoutAudio": "Join without audio", "initiated": "Call initiated", "linkCopied": "Link copied to clipboard", - "lookGood": "Speaker and microphone look good", + "lookGood": "It sounds like your microphone is working properly", "or": "or", "calling": "Calling", "startWithPhone": "Start with phone audio", diff --git a/react/features/base/devices/middleware.js b/react/features/base/devices/middleware.js index 74cc2b59e..12849279c 100644 --- a/react/features/base/devices/middleware.js +++ b/react/features/base/devices/middleware.js @@ -18,7 +18,7 @@ import { SET_AUDIO_INPUT_DEVICE, SET_VIDEO_INPUT_DEVICE } from './actionTypes'; -import { replaceAudioTrackById, replaceVideoTrackById } from '../../prejoin/actions'; +import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions'; import { isPrejoinPageVisible } from '../../prejoin/functions'; import { showNotification, showWarningNotification } from '../../notifications'; import { updateSettings } from '../settings'; @@ -65,14 +65,19 @@ MiddlewareRegistry.register(store => next => action => { || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP .camera[JitsiTrackErrors.GENERAL]; const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message; + const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED + ? 'deviceError.cameraPermission' : 'deviceError.cameraError'; store.dispatch(showWarningNotification({ description: additionalCameraErrorMsg, descriptionKey: cameraErrorMsg, - titleKey: name === JitsiTrackErrors.PERMISSION_DENIED - ? 'deviceError.cameraPermission' : 'deviceError.cameraError' + titleKey })); + if (isPrejoinPageVisible(store.getState())) { + store.dispatch(setDeviceStatusWarning(titleKey)); + } + break; } case NOTIFY_MIC_ERROR: { @@ -88,15 +93,20 @@ MiddlewareRegistry.register(store => next => action => { || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP .microphone[JitsiTrackErrors.GENERAL]; const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message; + const titleKey = name === JitsiTrackErrors.PERMISSION_DENIED + ? 'deviceError.microphonePermission' + : 'deviceError.microphoneError'; store.dispatch(showWarningNotification({ description: additionalMicErrorMsg, descriptionKey: micErrorMsg, - titleKey: name === JitsiTrackErrors.PERMISSION_DENIED - ? 'deviceError.microphonePermission' - : 'deviceError.microphoneError' + titleKey })); + if (isPrejoinPageVisible(store.getState())) { + store.dispatch(setDeviceStatusWarning(titleKey)); + } + break; } case SET_AUDIO_INPUT_DEVICE: diff --git a/react/features/base/icons/svg/index.js b/react/features/base/icons/svg/index.js index 61007d945..77ac33df9 100644 --- a/react/features/base/icons/svg/index.js +++ b/react/features/base/icons/svg/index.js @@ -85,3 +85,4 @@ export { default as IconVideoQualityLD } from './LD.svg'; export { default as IconVideoQualitySD } from './SD.svg'; export { default as IconVolume } from './volume.svg'; export { default as IconVolumeEmpty } from './volume-empty.svg'; +export { default as IconVolumeOff } from './volume-off.svg'; diff --git a/react/features/base/icons/svg/volume-off.svg b/react/features/base/icons/svg/volume-off.svg new file mode 100644 index 000000000..e3733234e --- /dev/null +++ b/react/features/base/icons/svg/volume-off.svg @@ -0,0 +1,3 @@ + + + diff --git a/react/features/base/settings/functions.web.js b/react/features/base/settings/functions.web.js index f646ae4d5..0ca12e127 100644 --- a/react/features/base/settings/functions.web.js +++ b/react/features/base/settings/functions.web.js @@ -32,6 +32,17 @@ export function getCurrentOutputDeviceId(state: Object) { return state['features/base/settings'].audioOutputDeviceId; } +/** + * Returns the saved display name. + * + * @param {Object} state - The state of the application. + * @returns {string} + */ +export function getDisplayName(state: Object): string { + return state['features/base/settings'].displayName || ''; +} + + /** * Handles changes to the `disableCallIntegration` setting. * Noop on web. diff --git a/react/features/base/settings/reducer.js b/react/features/base/settings/reducer.js index f1bfed5b2..b4e9bac75 100644 --- a/react/features/base/settings/reducer.js +++ b/react/features/base/settings/reducer.js @@ -39,7 +39,8 @@ const DEFAULT_STATE = { userSelectedMicDeviceId: undefined, userSelectedAudioOutputDeviceLabel: undefined, userSelectedCameraDeviceLabel: undefined, - userSelectedMicDeviceLabel: undefined + userSelectedMicDeviceLabel: undefined, + userSelectedSkipPrejoin: undefined }; const STORE_NAME = 'features/base/settings'; diff --git a/react/features/prejoin/actionTypes.js b/react/features/prejoin/actionTypes.js index 146af2744..990b6717f 100644 --- a/react/features/prejoin/actionTypes.js +++ b/react/features/prejoin/actionTypes.js @@ -24,6 +24,11 @@ export const PREJOIN_START_CONFERENCE = 'PREJOIN_START_CONFERENCE'; */ export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS'; +/** + * Action type to set the visiblity of the prejoin page for the future. + */ +export const SET_SKIP_PREJOIN = 'SET_SKIP_PREJOIN'; + /** * Action type to set the visiblity of the 'JoinByPhone' dialog. */ @@ -44,11 +49,6 @@ export const SET_PREJOIN_AUDIO_MUTED = 'SET_PREJOIN_AUDIO_MUTED'; */ export const SET_PREJOIN_DEVICE_ERRORS = 'SET_PREJOIN_DEVICE_ERRORS'; -/** - * Action type to set the name of the user. - */ -export const SET_PREJOIN_NAME = 'SET_PREJOIN_NAME'; - /** * Action type to set the visibility of the prejoin page. */ diff --git a/react/features/prejoin/actions.js b/react/features/prejoin/actions.js index 03b4d29a3..dc95c2309 100644 --- a/react/features/prejoin/actions.js +++ b/react/features/prejoin/actions.js @@ -6,11 +6,11 @@ import { ADD_PREJOIN_VIDEO_TRACK, PREJOIN_START_CONFERENCE, SET_DEVICE_STATUS, + SET_SKIP_PREJOIN, SET_JOIN_BY_PHONE_DIALOG_VISIBLITY, SET_PREJOIN_AUDIO_DISABLED, SET_PREJOIN_AUDIO_MUTED, SET_PREJOIN_DEVICE_ERRORS, - SET_PREJOIN_NAME, SET_PREJOIN_PAGE_VISIBILITY, SET_PREJOIN_VIDEO_DISABLED, SET_PREJOIN_VIDEO_MUTED @@ -273,6 +273,18 @@ export function setDeviceStatusWarning(deviceStatusText: string) { }; } +/** + * Sets the visibility of the prejoin page for future uses. + * + * @param {boolean} value - The visibility value. + * @returns {Object} + */ +export function setSkipPrejoin(value: boolean) { + return { + type: SET_SKIP_PREJOIN, + value + }; +} /** * Action used to set the visiblitiy of the 'JoinByPhoneDialog'. @@ -300,19 +312,6 @@ export function setPrejoinDeviceErrors(value: Object) { }; } -/** - * Action used to set the name of the guest user. - * - * @param {string} value - The name. - * @returns {Object} - */ -export function setPrejoinName(value: string) { - return { - type: SET_PREJOIN_NAME, - value - }; -} - /** * Action used to set the visiblity of the prejoin page. * diff --git a/react/features/prejoin/components/Prejoin.js b/react/features/prejoin/components/Prejoin.js index bc1934df2..713e206e9 100644 --- a/react/features/prejoin/components/Prejoin.js +++ b/react/features/prejoin/components/Prejoin.js @@ -1,19 +1,21 @@ // @flow import React, { Component } from 'react'; +import InlineDialog from '@atlaskit/inline-dialog'; import { joinConference as joinConferenceAction, joinConferenceWithoutAudio as joinConferenceWithoutAudioAction, - setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction, - setPrejoinName + setSkipPrejoin as setSkipPrejoinAction, + setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction } from '../actions'; import { getRoomName } from '../../base/conference'; +import { Icon, IconPhone, IconVolumeOff } from '../../base/icons'; import { translate } from '../../base/i18n'; import { connect } from '../../base/redux'; +import { getDisplayName, updateSettings } from '../../base/settings'; import ActionButton from './buttons/ActionButton'; import { areJoinByPhoneButtonsVisible, - getPrejoinName, isDeviceStatusVisible, isJoinByPhoneDialogVisible } from '../functions'; @@ -52,9 +54,9 @@ type Props = { name: string, /** - * Sets the name for the joining user. + * Updates settings. */ - setName: Function, + updateSettings: Function, /** * The name of the meeting that is about to be joined. @@ -62,7 +64,12 @@ type Props = { roomName: string, /** - * Sets visibilit of the 'JoinByPhoneDialog'. + * Sets visibility of the prejoin page for the next sessions. + */ + setSkipPrejoin: Function, + + /** + * Sets visibility of the 'JoinByPhoneDialog'. */ setJoinByPhoneDialogVisiblity: Function, @@ -74,7 +81,7 @@ type Props = { /** * If join by phone buttons should be visible. */ - showJoinByPhoneButtons: boolean, + hasJoinByPhoneButtons: boolean, /** * Used for translation. @@ -82,10 +89,18 @@ type Props = { t: Function, }; +type State = { + + /** + * Flag controlling the visibility of the 'join by phone' buttons. + */ + showJoinByPhoneButtons: boolean +} + /** * This component is displayed before joining a meeting. */ -class Prejoin extends Component { +class Prejoin extends Component { /** * Initializes a new {@code Prejoin} instance. * @@ -94,7 +109,69 @@ class Prejoin extends Component { constructor(props) { super(props); + this.state = { + showJoinByPhoneButtons: false + }; this._showDialog = this._showDialog.bind(this); + this._onCheckboxChange = this._onCheckboxChange.bind(this); + this._onDropdownClose = this._onDropdownClose.bind(this); + this._onOptionsClick = this._onOptionsClick.bind(this); + this._setName = this._setName.bind(this); + } + + _onCheckboxChange: () => void; + + /** + * Handler for the checkbox. + * + * @param {Object} e - The synthetic event. + * @returns {void} + */ + _onCheckboxChange(e) { + this.props.setSkipPrejoin(e.target.checked); + } + + _onDropdownClose: () => void; + + /** + * Closes the dropdown. + * + * @returns {void} + */ + _onDropdownClose() { + this.setState({ + showJoinByPhoneButtons: false + }); + } + + _onOptionsClick: () => void; + + /** + * Displays the join by phone buttons dropdown. + * + * @param {Object} e - The synthetic event. + * @returns {void} + */ + _onOptionsClick(e) { + e.stopPropagation(); + + this.setState({ + showJoinByPhoneButtons: !this.state.showJoinByPhoneButtons + }); + } + + _setName: () => void; + + /** + * Sets the guest participant name. + * + * @param {string} displayName - Participant name. + * @returns {void} + */ + _setName(displayName) { + this.props.updateSettings({ + displayName + }); } _showDialog: () => void; @@ -121,49 +198,78 @@ class Prejoin extends Component { joinConference, joinConferenceWithoutAudio, name, - setName, - showJoinByPhoneButtons, + hasJoinByPhoneButtons, t } = this.props; - const { _showDialog } = this; + const { _onCheckboxChange, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this; + const { showJoinByPhoneButtons } = this.state; return (
- +
{t('prejoin.joinMeeting')}
+ + - - { t('calendarSync.join') } - - {showJoinByPhoneButtons - &&
+ +
+ +
+ + { t('prejoin.joinWithoutAudio') } +
+
+ + { t('prejoin.joinAudioByPhone') } +
+
} + isOpen = { showJoinByPhoneButtons } + onClose = { _onDropdownClose }> - { t('prejoin.joinWithoutAudio') } + hasOptions = { hasJoinByPhoneButtons } + onClick = { joinConference } + onOptionsClick = { _onOptionsClick } + type = 'primary'> + { t('prejoin.joinMeeting') } - - { t('prejoin.joinAudioByPhone') } - -
} + +
+ +
+ + +
+
+ +
+ + {t('prejoin.doNotShow')}
-
- - -
+ { deviceStatusVisible && } ); @@ -180,10 +286,10 @@ function mapStateToProps(state): Object { return { isAnonymousUser: isGuest(state), deviceStatusVisible: isDeviceStatusVisible(state), - name: getPrejoinName(state), + name: getDisplayName(state), roomName: getRoomName(state), showDialog: isJoinByPhoneDialogVisible(state), - showJoinByPhoneButtons: areJoinByPhoneButtonsVisible(state) + hasJoinByPhoneButtons: areJoinByPhoneButtonsVisible(state) }; } @@ -191,7 +297,8 @@ const mapDispatchToProps = { joinConferenceWithoutAudio: joinConferenceWithoutAudioAction, joinConference: joinConferenceAction, setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction, - setName: setPrejoinName + setSkipPrejoin: setSkipPrejoinAction, + updateSettings }; export default connect(mapStateToProps, mapDispatchToProps)(translate(Prejoin)); diff --git a/react/features/prejoin/components/buttons/ActionButton.js b/react/features/prejoin/components/buttons/ActionButton.js index ffc07a617..783e17235 100644 --- a/react/features/prejoin/components/buttons/ActionButton.js +++ b/react/features/prejoin/components/buttons/ActionButton.js @@ -1,6 +1,8 @@ // @flow import React from 'react'; +import { Icon, IconArrowDown } from '../../../base/icons'; + const classNameByType = { primary: 'prejoin-btn--primary', secondary: 'prejoin-btn--secondary', @@ -19,6 +21,11 @@ type Props = { */ className?: string, + /** + * If the button has options. + */ + hasOptions?: boolean, + /** * The type of th button: primary, secondary, text. */ @@ -28,6 +35,11 @@ type Props = { * OnClick button handler. */ onClick: Function, + + /** + * Click handler for options. + */ + onOptionsClick?: Function }; /** @@ -35,7 +47,7 @@ type Props = { * * @returns {ReactElement} */ -function ActionButton({ children, className, type, onClick }: Props) { +function ActionButton({ children, className, hasOptions, type, onClick, onOptionsClick }: Props) { const ownClassName = `prejoin-btn ${classNameByType[type]}`; const cls = className ? `${className} ${ownClassName}` : ownClassName; @@ -44,6 +56,15 @@ function ActionButton({ children, className, type, onClick }: Props) { className = { cls } onClick = { onClick }> {children} + {hasOptions &&
+ +
+ } ); } diff --git a/react/features/prejoin/components/preview/CopyMeetingUrl.js b/react/features/prejoin/components/preview/CopyMeetingUrl.js index 69c992d84..a839e6877 100644 --- a/react/features/prejoin/components/preview/CopyMeetingUrl.js +++ b/react/features/prejoin/components/preview/CopyMeetingUrl.js @@ -170,6 +170,7 @@ class CopyMeetingUrl extends Component { }