mirror of https://github.com/jitsi/jitsi-meet
Improve premeeting screens ux (#9726)
* feat(prejoin) move invite to toolbar section * feat(premeeting) redesign prejoin and lobby screens * code review changes * fix prejoin flicker and avatar id * fix password error message and native lobby dialog close positionpull/9765/head jitsi-meet_6211
parent
49a73ac446
commit
1ad9046a38
@ -1,153 +0,0 @@ |
||||
.prejoin { |
||||
|
||||
&-input-area { |
||||
margin: 0 auto; |
||||
text-align: center; |
||||
|
||||
&-label { |
||||
display: block; |
||||
margin-bottom: 5px; |
||||
color: #ffffff; |
||||
font-weight: 300; |
||||
font-size: 15px; |
||||
line-height: 24px; |
||||
} |
||||
} |
||||
|
||||
&-title { |
||||
color: #fff; |
||||
font-size: 24px; |
||||
line-height: 32px; |
||||
margin-bottom: 16px; |
||||
} |
||||
|
||||
&-text-btns { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
&-input-label { |
||||
color: #A4B8D1; |
||||
font-size: 13px; |
||||
line-height: 20px; |
||||
margin-top: 32px 0 8px 0; |
||||
text-align: center; |
||||
width: 100%; |
||||
} |
||||
|
||||
&-checkbox { |
||||
border: 0; |
||||
height: 16px; |
||||
margin-right: 8px; |
||||
padding: 0; |
||||
width: 16px; |
||||
} |
||||
|
||||
&-checkbox-container { |
||||
margin-bottom: 14px; |
||||
width: 100%; |
||||
} |
||||
|
||||
&-error { |
||||
color: white; |
||||
background-color: rgba(225, 45, 45, 0.6); |
||||
border-radius: 3px; |
||||
width: 100%; |
||||
padding: 2px; |
||||
box-sizing: border-box; |
||||
margin-top: 4px; |
||||
font-size: 13px; |
||||
text-align: center; |
||||
} |
||||
} |
||||
|
||||
@mixin name-placeholder { |
||||
color: #fff; |
||||
font-weight: 300; |
||||
opacity: 0.6; |
||||
} |
||||
|
||||
.prejoin-preview { |
||||
&-status { |
||||
align-items: center; |
||||
align-self: stretch; |
||||
bottom: 0; |
||||
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, 1); |
||||
} |
||||
&--ok { |
||||
background: rgba(49, 183, 106, 1); |
||||
} |
||||
} |
||||
|
||||
&-icon { |
||||
background-position: center; |
||||
background-repeat: no-repeat; |
||||
display: inline-block; |
||||
height: 16px; |
||||
margin-right: 8px; |
||||
width: 16px; |
||||
} |
||||
|
||||
&-error-desc { |
||||
margin-right: 4px; |
||||
color: #fff; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
.settings-button-container { |
||||
width: 49px; |
||||
margin: 0 8px; |
||||
} |
||||
|
||||
&-dropdown-btns { |
||||
width: 320px; |
||||
padding: 8px 0; |
||||
|
||||
@include adjust-for-max-width(320px, 8px); |
||||
} |
||||
|
||||
&-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 { |
||||
margin-top: 16px; |
||||
|
||||
& > div:nth-child(2) { |
||||
background: #fff; |
||||
padding: 0; |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,35 @@ |
||||
.device { |
||||
&-status { |
||||
align-items: center; |
||||
align-self: stretch; |
||||
color: #fff; |
||||
display: flex; |
||||
font-size: 14px; |
||||
font-weight: 400; |
||||
justify-content: center; |
||||
line-height: 20px; |
||||
margin-top: 8px; |
||||
padding: 6px; |
||||
text-align: center; |
||||
} |
||||
|
||||
&-icon { |
||||
background-position: center; |
||||
background-repeat: no-repeat; |
||||
display: inline-block; |
||||
height: 16px; |
||||
margin-right: 10px; |
||||
width: 16px; |
||||
|
||||
&--warning { |
||||
svg path { |
||||
fill: rgba(241, 173, 51, 1); |
||||
} |
||||
} |
||||
&--ok { |
||||
svg path { |
||||
fill: #189b55; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
@import 'connection-status'; |
||||
@import 'device-status'; |
||||
@import 'lobby'; |
||||
@import 'premeeting-screens'; |
||||
@import 'prejoin'; |
||||
@import 'prejoin-dialog'; |
||||
@import 'prejoin-third-party'; |
@ -0,0 +1,39 @@ |
||||
$sidePanelWidth: 300px; |
||||
|
||||
.prejoin-third-party { |
||||
flex-direction: column-reverse; |
||||
|
||||
.content { |
||||
height: auto; |
||||
margin: 0 auto; |
||||
|
||||
.new-toolbox { |
||||
width: auto; |
||||
} |
||||
} |
||||
|
||||
#preview { |
||||
background-color: transparent; |
||||
bottom: 0; |
||||
left: 0; |
||||
position: absolute; |
||||
right: 0; |
||||
top: 0; |
||||
|
||||
.avatar { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
&.splash { |
||||
.content { |
||||
margin-left: calc((100% - #{$prejoinDefaultContentWidth} + #{$sidePanelWidth}) / 2) |
||||
} |
||||
} |
||||
|
||||
&.guest { |
||||
.content { |
||||
margin-bottom: auto; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,73 @@ |
||||
.prejoin { |
||||
&-input-area { |
||||
width: 100%; |
||||
} |
||||
|
||||
&-checkbox-container { |
||||
margin-bottom: 16px; |
||||
width: 100%; |
||||
text-align: center; |
||||
} |
||||
|
||||
&-error { |
||||
color: white; |
||||
background-color: #E04757; |
||||
border-radius: 6px; |
||||
padding: 4px; |
||||
box-sizing: border-box; |
||||
margin-bottom: 16px; |
||||
margin-top: -8px; |
||||
font-size: 12px; |
||||
text-align: center; |
||||
width: 100%; |
||||
} |
||||
} |
||||
|
||||
.prejoin-preview { |
||||
&-dropdown-btns { |
||||
padding: 8px 0; |
||||
width: calc(100% - 48px); |
||||
} |
||||
|
||||
&-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 { |
||||
position: relative; |
||||
width: 100%; |
||||
|
||||
/** |
||||
* Override default InlineDialog behaviour, since it does not play nicely with relative widths |
||||
*/ |
||||
& > div:nth-child(2) { |
||||
background: #fff; |
||||
padding: 0; |
||||
position: absolute !important; |
||||
top: 48px !important; |
||||
transform: none !important; |
||||
width: 100%; |
||||
} |
||||
} |
||||
} |
||||
|
After Width: | Height: | Size: 699 B |
@ -1,67 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import CopyMeetingLinkSection |
||||
from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection'; |
||||
import { getCurrentConferenceUrl } from '../../../connection'; |
||||
import { translate } from '../../../i18n'; |
||||
import { connect } from '../../../redux'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* The meeting url. |
||||
*/ |
||||
url: string, |
||||
|
||||
/** |
||||
* Used for translation. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* Used to determine if invitation link should be automatically copied |
||||
* after creating a meeting. |
||||
*/ |
||||
_enableAutomaticUrlCopy: boolean, |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Component used to copy meeting url on prejoin page. |
||||
*/ |
||||
class CopyMeetingUrl extends Component<Props> { |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<div className = 'copy-meeting'> |
||||
<CopyMeetingLinkSection url = { this.props.url } /> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the React {@code Component} props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {Object} |
||||
*/ |
||||
function mapStateToProps(state) { |
||||
const { enableAutomaticUrlCopy } = state['features/base/config']; |
||||
const { customizationReady } = state['features/dynamic-branding']; |
||||
|
||||
return { |
||||
url: customizationReady ? getCurrentConferenceUrl(state) : '', |
||||
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false |
||||
}; |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(translate(CopyMeetingUrl)); |
@ -0,0 +1,44 @@ |
||||
// @flow
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../../../analytics'; |
||||
import { translate } from '../../../../base/i18n'; |
||||
import { IconAddPeople } from '../../../../base/icons'; |
||||
import { connect } from '../../../../base/redux'; |
||||
import { AbstractButton, type AbstractButtonProps } from '../../../../base/toolbox/components'; |
||||
import { beginAddPeople } from '../../../actions.any'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link EmbedMeetingButton}. |
||||
*/ |
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Function |
||||
}; |
||||
|
||||
/** |
||||
* Implementation of a button for opening invite people dialog. |
||||
*/ |
||||
class InviteButton extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.invite'; |
||||
icon = IconAddPeople; |
||||
label = 'toolbar.invite'; |
||||
tooltip = 'toolbar.invite'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and opens the appropriate dialog. |
||||
* |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
const { dispatch } = this.props; |
||||
|
||||
sendAnalytics(createToolbarEvent('invite')); |
||||
dispatch(beginAddPeople()); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(InviteButton)); |
@ -1,3 +1,4 @@ |
||||
// @flow
|
||||
|
||||
export { default as AddPeopleDialog } from './AddPeopleDialog'; |
||||
export { default as InviteButton } from './InviteButton'; |
||||
|
@ -0,0 +1,87 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
import { isVideoMutedByUser } from '../../base/media'; |
||||
import { PreMeetingScreen } from '../../base/premeeting'; |
||||
import { connect } from '../../base/redux'; |
||||
import { getLocalJitsiVideoTrack } from '../../base/tracks'; |
||||
import { isDeviceStatusVisible } from '../functions'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Indicates the className that needs to be applied. |
||||
*/ |
||||
className: string, |
||||
|
||||
/** |
||||
* Flag signaling if the device status is visible or not. |
||||
*/ |
||||
deviceStatusVisible: boolean, |
||||
|
||||
/** |
||||
* Flag signaling the visibility of camera preview. |
||||
*/ |
||||
showCameraPreview: boolean, |
||||
|
||||
/** |
||||
* Used for translation. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* The JitsiLocalTrack to display. |
||||
*/ |
||||
videoTrack: ?Object |
||||
}; |
||||
|
||||
const buttons = [ 'microphone', 'camera', 'select-background' ]; |
||||
|
||||
/** |
||||
* This component is displayed before joining a meeting. |
||||
*/ |
||||
class PrejoinThirdParty extends Component<Props> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { |
||||
className, |
||||
deviceStatusVisible, |
||||
showCameraPreview, |
||||
videoTrack |
||||
} = this.props; |
||||
|
||||
return ( |
||||
<PreMeetingScreen |
||||
className = { `prejoin-third-party ${className}` } |
||||
showDeviceStatus = { deviceStatusVisible } |
||||
skipPrejoinButton = { false } |
||||
toolbarButtons = { buttons } |
||||
videoMuted = { !showCameraPreview } |
||||
videoTrack = { videoTrack } /> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the React {@code Component} props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @param {Object} ownProps - The props passed to the component. |
||||
* @returns {Object} |
||||
*/ |
||||
function mapStateToProps(state): Object { |
||||
return { |
||||
deviceStatusVisible: isDeviceStatusVisible(state), |
||||
showCameraPreview: !isVideoMutedByUser(state), |
||||
videoTrack: getLocalJitsiVideoTrack(state) |
||||
}; |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(translate(PrejoinThirdParty)); |
@ -0,0 +1,19 @@ |
||||
// @flow
|
||||
|
||||
export type PREJOIN_SCREEN_STATE = "hidden" | "loading" | true; |
||||
|
||||
|
||||
type PREJOIN_SCREEN_STATE_TYPE = { |
||||
HIDDEN: PREJOIN_SCREEN_STATE, |
||||
LOADING: PREJOIN_SCREEN_STATE, |
||||
VISIBLE: PREJOIN_SCREEN_STATE |
||||
} |
||||
|
||||
/** |
||||
* Enum of possible prejoin screen states. |
||||
*/ |
||||
export const PREJOIN_SCREEN_STATES: PREJOIN_SCREEN_STATE_TYPE = { |
||||
HIDDEN: 'hidden', |
||||
LOADING: 'loading', |
||||
VISIBLE: true // backwards compatibility with old boolean implementation
|
||||
}; |
Loading…
Reference in new issue