This commit is contained in:
226
react/features/prejoin/components/native/Prejoin.tsx
Normal file
226
react/features/prejoin/components/native/Prejoin.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
BackHandler,
|
||||
Platform,
|
||||
StyleProp,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { setPermanentProperty } from '../../../analytics/actions';
|
||||
import { appNavigate } from '../../../app/actions.native';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { setAudioOnly } from '../../../base/audio-only/actions';
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import { isNameReadOnly } from '../../../base/config/functions.any';
|
||||
import { connect } from '../../../base/connection/actions.native';
|
||||
import { PREJOIN_PAGE_HIDE_DISPLAY_NAME } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getFieldValue } from '../../../base/react/functions';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { openDisplayNamePrompt } from '../../../display-name/actions';
|
||||
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
|
||||
import LargeVideo from '../../../large-video/components/LargeVideo.native';
|
||||
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
|
||||
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
|
||||
import { isDisplayNameRequired, isRoomNameEnabled } from '../../functions';
|
||||
import { IPrejoinProps } from '../../types';
|
||||
import { hasDisplayName } from '../../utils';
|
||||
|
||||
import { preJoinStyles as styles } from './styles';
|
||||
|
||||
|
||||
const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFocused = useIsFocused();
|
||||
const { t } = useTranslation();
|
||||
const aspectRatio = useSelector(
|
||||
(state: IReduxState) => state['features/base/responsive-ui']?.aspectRatio
|
||||
);
|
||||
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
const isDisplayNameMandatory = useSelector((state: IReduxState) => isDisplayNameRequired(state));
|
||||
const isDisplayNameVisible
|
||||
= useSelector((state: IReduxState) => !getFeatureFlag(state, PREJOIN_PAGE_HIDE_DISPLAY_NAME, false));
|
||||
const isDisplayNameReadonly = useSelector(isNameReadOnly);
|
||||
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
|
||||
const roomNameEnabled = useSelector((state: IReduxState) => isRoomNameEnabled(state));
|
||||
const participantName = localParticipant?.name;
|
||||
const [ displayName, setDisplayName ]
|
||||
= useState(participantName || '');
|
||||
const isDisplayNameMissing = useMemo(
|
||||
() => !displayName && isDisplayNameMandatory, [ displayName, isDisplayNameMandatory ]);
|
||||
const showDisplayNameError = useMemo(
|
||||
() => !isDisplayNameReadonly && isDisplayNameMissing && isDisplayNameVisible,
|
||||
[ isDisplayNameMissing, isDisplayNameReadonly, isDisplayNameVisible ]);
|
||||
const showDisplayNameInput = useMemo(
|
||||
() => isDisplayNameVisible && (displayName || !isDisplayNameReadonly),
|
||||
[ displayName, isDisplayNameReadonly, isDisplayNameVisible ]);
|
||||
const onChangeDisplayName = useCallback(event => {
|
||||
const fieldValue = getFieldValue(event);
|
||||
|
||||
setDisplayName(fieldValue);
|
||||
dispatch(updateSettings({
|
||||
displayName: fieldValue
|
||||
}));
|
||||
}, [ displayName ]);
|
||||
|
||||
const onJoin = useCallback(() => {
|
||||
dispatch(connect());
|
||||
navigateRoot(screen.conference.root);
|
||||
}, [ dispatch ]);
|
||||
|
||||
const maybeJoin = useCallback(() => {
|
||||
if (isDisplayNameMissing) {
|
||||
dispatch(openDisplayNamePrompt({
|
||||
onPostSubmit: onJoin,
|
||||
validateInput: hasDisplayName
|
||||
}));
|
||||
} else {
|
||||
onJoin();
|
||||
}
|
||||
}, [ dispatch, hasDisplayName, isDisplayNameMissing, onJoin ]);
|
||||
|
||||
const onJoinLowBandwidth = useCallback(() => {
|
||||
dispatch(setAudioOnly(true));
|
||||
maybeJoin();
|
||||
}, [ dispatch ]);
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
dispatch(appNavigate(undefined));
|
||||
|
||||
return true;
|
||||
}, [ dispatch ]);
|
||||
|
||||
const { PRIMARY, TERTIARY } = BUTTON_TYPES;
|
||||
|
||||
useEffect(() => {
|
||||
const hardwareBackPressSubscription = BackHandler.addEventListener('hardwareBackPress', goBack);
|
||||
|
||||
dispatch(setPermanentProperty({
|
||||
wasPrejoinDisplayed: true
|
||||
}));
|
||||
|
||||
return () => hardwareBackPressSubscription.remove();
|
||||
}, []); // dispatch is not in the dependency list because we want the action to be dispatched only once when
|
||||
// the component is mounted.
|
||||
|
||||
const headerLeft = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
label = { t('dialog.close') }
|
||||
onPress = { goBack } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
onPress = { goBack }
|
||||
src = { IconCloseLarge } />
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft,
|
||||
headerTitle: t('prejoin.joinMeeting')
|
||||
});
|
||||
}, [ navigation ]);
|
||||
|
||||
let contentWrapperStyles;
|
||||
let contentContainerStyles;
|
||||
let largeVideoContainerStyles;
|
||||
|
||||
if (aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
contentWrapperStyles = styles.contentWrapper;
|
||||
contentContainerStyles = styles.contentContainer;
|
||||
largeVideoContainerStyles = styles.largeVideoContainer;
|
||||
} else {
|
||||
contentWrapperStyles = styles.contentWrapperWide;
|
||||
contentContainerStyles = styles.contentContainerWide;
|
||||
largeVideoContainerStyles = styles.largeVideoContainerWide;
|
||||
}
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { contentWrapperStyles }>
|
||||
<BrandingImageBackground />
|
||||
{
|
||||
isFocused
|
||||
&& <View style = { largeVideoContainerStyles as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.conferenceInfo as StyleProp<ViewStyle> }>
|
||||
{roomNameEnabled && (
|
||||
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<LargeVideo />
|
||||
</View>
|
||||
}
|
||||
<View style = { contentContainerStyles as ViewStyle }>
|
||||
<View style = { styles.toolboxContainer as ViewStyle }>
|
||||
<AudioMuteButton
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
<VideoMuteButton
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
</View>
|
||||
{
|
||||
showDisplayNameInput && <Input
|
||||
customStyles = {{ input: styles.customInput }}
|
||||
disabled = { isDisplayNameReadonly }
|
||||
error = { showDisplayNameError }
|
||||
onChange = { onChangeDisplayName }
|
||||
placeholder = { t('dialog.enterDisplayName') }
|
||||
value = { displayName } />
|
||||
}
|
||||
{
|
||||
showDisplayNameError && (
|
||||
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
|
||||
<Text style = { styles.error as StyleProp<TextStyle> }>
|
||||
{ t('prejoin.errorMissingName') }
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
accessibilityLabel = 'prejoin.joinMeeting'
|
||||
disabled = { showDisplayNameError }
|
||||
labelKey = 'prejoin.joinMeeting'
|
||||
onClick = { maybeJoin }
|
||||
style = { styles.joinButton }
|
||||
type = { PRIMARY } />
|
||||
<Button
|
||||
accessibilityLabel = 'prejoin.joinMeetingInLowBandwidthMode'
|
||||
disabled = { showDisplayNameError }
|
||||
labelKey = 'prejoin.joinMeetingInLowBandwidthMode'
|
||||
onClick = { onJoinLowBandwidth }
|
||||
style = { styles.joinButton }
|
||||
type = { TERTIARY } />
|
||||
</View>
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default Prejoin;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
StyleProp,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
|
||||
import { preJoinStyles as styles } from './styles';
|
||||
|
||||
|
||||
const RecordingWarning = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style = { styles.recordingWarning as StyleProp<ViewStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.recordingWarningText as StyleProp<TextStyle> }>
|
||||
{ t('prejoin.recordingWarning') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordingWarning;
|
||||
120
react/features/prejoin/components/native/UnsafeRoomWarning.tsx
Normal file
120
react/features/prejoin/components/native/UnsafeRoomWarning.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import React, { useCallback, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Platform,
|
||||
StyleProp,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../../../app/actions.native';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge, IconWarning } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import getUnsafeRoomText from '../../../base/util/getUnsafeRoomText.native';
|
||||
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { IPrejoinProps } from '../../types';
|
||||
|
||||
import { preJoinStyles as styles } from './styles';
|
||||
|
||||
|
||||
const UnsafeRoomWarning: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
|
||||
const aspectRatio = useSelector(
|
||||
(state: IReduxState) => state['features/base/responsive-ui']?.aspectRatio
|
||||
);
|
||||
const unsafeRoomText = useSelector((state: IReduxState) => getUnsafeRoomText(state, t, 'prejoin'));
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
dispatch(appNavigate(undefined));
|
||||
|
||||
return true;
|
||||
}, [ dispatch ]);
|
||||
|
||||
const onProceed = useCallback(() => {
|
||||
navigateRoot(screen.preJoin);
|
||||
|
||||
return true;
|
||||
}, [ dispatch ]);
|
||||
|
||||
const headerLeft = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
label = { t('dialog.close') }
|
||||
onPress = { goBack } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
onPress = { goBack }
|
||||
src = { IconCloseLarge } />
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft,
|
||||
headerTitle: t('prejoin.joinMeeting')
|
||||
});
|
||||
}, [ navigation ]);
|
||||
|
||||
let unsafeRoomContentContainer;
|
||||
|
||||
if (aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
unsafeRoomContentContainer = styles.unsafeRoomContentContainer;
|
||||
} else {
|
||||
unsafeRoomContentContainer = styles.unsafeRoomContentContainerWide;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { styles.unsafeRoomWarningContainer } >
|
||||
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
</View>
|
||||
<View style = { unsafeRoomContentContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.warningIconWrapper as StyleProp<ViewStyle> }>
|
||||
<Icon
|
||||
src = { IconWarning }
|
||||
style = { styles.warningIcon } />
|
||||
</View>
|
||||
|
||||
<Text
|
||||
dataDetectorType = 'link'
|
||||
style = { styles.warningText as StyleProp<TextStyle> }>
|
||||
{ unsafeRoomText }
|
||||
</Text>
|
||||
<Button
|
||||
accessibilityLabel = 'prejoin.proceedAnyway'
|
||||
disabled = { false }
|
||||
labelKey = 'prejoin.proceedAnyway'
|
||||
onClick = { onProceed }
|
||||
style = { styles.joinButton }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
</View>
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsafeRoomWarning;
|
||||
182
react/features/prejoin/components/native/styles.ts
Normal file
182
react/features/prejoin/components/native/styles.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const preJoinStyles = {
|
||||
|
||||
joinButton: {
|
||||
marginTop: BaseTheme.spacing[3],
|
||||
width: 352
|
||||
},
|
||||
|
||||
buttonStylesBorderless: {
|
||||
iconStyle: {
|
||||
color: BaseTheme.palette.icon01,
|
||||
fontSize: 24
|
||||
},
|
||||
style: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: BaseTheme.spacing[3],
|
||||
height: 24,
|
||||
width: 24
|
||||
},
|
||||
underlayColor: 'transparent'
|
||||
},
|
||||
|
||||
contentWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
contentWrapperWide: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
largeVideoContainer: {
|
||||
height: '60%'
|
||||
},
|
||||
|
||||
largeVideoContainerWide: {
|
||||
height: '100%',
|
||||
marginRight: 'auto',
|
||||
position: 'absolute',
|
||||
width: '50%'
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
height: 280,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
contentContainerWide: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: '50%',
|
||||
padding: BaseTheme.spacing[3],
|
||||
position: 'absolute',
|
||||
width: '50%'
|
||||
},
|
||||
|
||||
toolboxContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: 60,
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
paddingHorizontal: BaseTheme.spacing[2],
|
||||
width: 148
|
||||
},
|
||||
|
||||
customInput: {
|
||||
textAlign: 'center',
|
||||
width: 352
|
||||
},
|
||||
|
||||
errorContainer: {
|
||||
backgroundColor: BaseTheme.palette.actionDanger,
|
||||
borderBottomRightRadius: BaseTheme.shape.borderRadius,
|
||||
borderBottomLeftRadius: BaseTheme.shape.borderRadius,
|
||||
boxSizing: 'border-box',
|
||||
marginTop: -BaseTheme.spacing[2],
|
||||
overflow: 'visible',
|
||||
wordBreak: 'normal',
|
||||
width: 352
|
||||
},
|
||||
|
||||
error: {
|
||||
padding: BaseTheme.spacing[1],
|
||||
color: BaseTheme.palette.text01,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
preJoinRoomName: {
|
||||
...BaseTheme.typography.heading5,
|
||||
color: BaseTheme.palette.text01,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
conferenceInfo: {
|
||||
alignSelf: 'center',
|
||||
marginTop: BaseTheme.spacing[3],
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
paddingVertical: BaseTheme.spacing[1],
|
||||
position: 'absolute',
|
||||
maxWidth: 273,
|
||||
zIndex: 1
|
||||
},
|
||||
displayRoomNameBackdrop: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
opacity: 0.7,
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
paddingVertical: BaseTheme.spacing[1]
|
||||
},
|
||||
recordingWarning: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 22,
|
||||
marginBottom: BaseTheme.spacing[2],
|
||||
marginTop: BaseTheme.spacing[1],
|
||||
width: 'auto'
|
||||
},
|
||||
recordingWarningText: {
|
||||
color: BaseTheme.palette.text03
|
||||
},
|
||||
unsafeRoomWarningContainer: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
},
|
||||
unsafeRoomContentContainer: {
|
||||
justifySelf: 'center',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
unsafeRoomContentContainerWide: {
|
||||
alignItems: 'center',
|
||||
justifySelf: 'center',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginLeft: BaseTheme.spacing[7],
|
||||
paddingHorizontal: BaseTheme.spacing[6]
|
||||
},
|
||||
|
||||
warningText: {
|
||||
...BaseTheme.typography.bodyLongRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
textAlign: 'center',
|
||||
marginBottom: BaseTheme.spacing[4]
|
||||
},
|
||||
warningIconWrapper: {
|
||||
backgroundColor: BaseTheme.palette.warning01,
|
||||
borderRadius: BaseTheme.shape.circleRadius,
|
||||
padding: BaseTheme.spacing[4],
|
||||
marginBottom: BaseTheme.spacing[4],
|
||||
zIndex: 0
|
||||
|
||||
},
|
||||
warningIcon: {
|
||||
color: BaseTheme.palette.ui01,
|
||||
fontSize: 40
|
||||
}
|
||||
};
|
||||
46
react/features/prejoin/components/web/Label.tsx
Normal file
46
react/features/prejoin/components/web/Label.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The text for the Label.
|
||||
*/
|
||||
children: React.ReactElement;
|
||||
|
||||
/**
|
||||
* The CSS class of the label.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The (round) number prefix for the Label.
|
||||
*/
|
||||
number?: string | number;
|
||||
|
||||
/**
|
||||
* The click handler.
|
||||
*/
|
||||
onClick?: (e?: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Label for the dialogs.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Label({ children, className, number, onClick }: IProps) {
|
||||
const containerClass = className
|
||||
? `prejoin-dialog-label ${className}`
|
||||
: 'prejoin-dialog-label';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { containerClass }
|
||||
onClick = { onClick }>
|
||||
{number && <div className = 'prejoin-dialog-label-num'>{number}</div>}
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Label;
|
||||
533
react/features/prejoin/components/web/Prejoin.tsx
Normal file
533
react/features/prejoin/components/web/Prejoin.tsx
Normal file
@@ -0,0 +1,533 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { isNameReadOnly } from '../../../base/config/functions.web';
|
||||
import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons/svg';
|
||||
import { isVideoMutedByUser } from '../../../base/media/functions';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import ActionButton from '../../../base/premeeting/components/web/ActionButton';
|
||||
import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeetingScreen';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import { getDisplayName } from '../../../base/settings/functions.web';
|
||||
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
|
||||
import isInsecureRoomName from '../../../base/util/isInsecureRoomName';
|
||||
import { openDisplayNamePrompt } from '../../../display-name/actions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
import {
|
||||
joinConference as joinConferenceAction,
|
||||
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
|
||||
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
|
||||
} from '../../actions.web';
|
||||
import {
|
||||
isDeviceStatusVisible,
|
||||
isDisplayNameRequired,
|
||||
isJoinByPhoneButtonVisible,
|
||||
isJoinByPhoneDialogVisible,
|
||||
isPrejoinDisplayNameVisible
|
||||
} from '../../functions';
|
||||
import logger from '../../logger';
|
||||
import { hasDisplayName } from '../../utils';
|
||||
|
||||
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Flag signaling if the device status is visible or not.
|
||||
*/
|
||||
deviceStatusVisible: boolean;
|
||||
|
||||
/**
|
||||
* If join by phone button should be visible.
|
||||
*/
|
||||
hasJoinByPhoneButton: boolean;
|
||||
|
||||
/**
|
||||
* Flag signaling if the display name is visible or not.
|
||||
*/
|
||||
isDisplayNameVisible: boolean;
|
||||
|
||||
/**
|
||||
* Joins the current meeting.
|
||||
*/
|
||||
joinConference: Function;
|
||||
|
||||
/**
|
||||
* Joins the current meeting without audio.
|
||||
*/
|
||||
joinConferenceWithoutAudio: Function;
|
||||
|
||||
/**
|
||||
* Whether conference join is in progress.
|
||||
*/
|
||||
joiningInProgress?: boolean;
|
||||
|
||||
/**
|
||||
* The name of the user that is about to join.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Local participant id.
|
||||
*/
|
||||
participantId?: string;
|
||||
|
||||
/**
|
||||
* The prejoin config.
|
||||
*/
|
||||
prejoinConfig?: any;
|
||||
|
||||
/**
|
||||
* Whether the name input should be read only or not.
|
||||
*/
|
||||
readOnlyName: boolean;
|
||||
|
||||
/**
|
||||
* Sets visibility of the 'JoinByPhoneDialog'.
|
||||
*/
|
||||
setJoinByPhoneDialogVisiblity: Function;
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
showCameraPreview: boolean;
|
||||
|
||||
/**
|
||||
* If 'JoinByPhoneDialog' is visible or not.
|
||||
*/
|
||||
showDialog: boolean;
|
||||
|
||||
/**
|
||||
* If should show an error when joining without a name.
|
||||
*/
|
||||
showErrorOnJoin: boolean;
|
||||
|
||||
/**
|
||||
* If the recording warning is visible or not.
|
||||
*/
|
||||
showRecordingWarning: boolean;
|
||||
|
||||
/**
|
||||
* If should show unsafe room warning when joining.
|
||||
*/
|
||||
showUnsafeRoomWarning: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user has approved to join a room with unsafe name.
|
||||
*/
|
||||
unsafeRoomConsent?: boolean;
|
||||
|
||||
/**
|
||||
* Updates settings.
|
||||
*/
|
||||
updateSettings: Function;
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
videoTrack?: Object;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
inputContainer: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
input: {
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(3),
|
||||
|
||||
'& input': {
|
||||
textAlign: 'center'
|
||||
}
|
||||
},
|
||||
|
||||
avatarContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
avatar: {
|
||||
margin: `${theme.spacing(2)} auto ${theme.spacing(3)}`
|
||||
},
|
||||
|
||||
avatarName: {
|
||||
...theme.typography.bodyShortBoldLarge,
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(5),
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
error: {
|
||||
backgroundColor: theme.palette.actionDanger,
|
||||
color: theme.palette.text01,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
width: '100%',
|
||||
...theme.typography.labelRegular,
|
||||
boxSizing: 'border-box',
|
||||
padding: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
marginTop: `-${theme.spacing(2)}`,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
dropdownContainer: {
|
||||
position: 'relative',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
dropdownButtons: {
|
||||
width: '300px',
|
||||
padding: '8px 0',
|
||||
backgroundColor: theme.palette.action02,
|
||||
color: theme.palette.text04,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
position: 'relative',
|
||||
top: `-${theme.spacing(3)}`,
|
||||
|
||||
'@media (max-width: 511px)': {
|
||||
margin: '0 auto',
|
||||
top: 0
|
||||
},
|
||||
|
||||
'@media (max-width: 420px)': {
|
||||
top: 0,
|
||||
width: 'calc(100% - 32px)'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Prejoin = ({
|
||||
deviceStatusVisible,
|
||||
hasJoinByPhoneButton,
|
||||
isDisplayNameVisible,
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
joiningInProgress,
|
||||
name,
|
||||
participantId,
|
||||
prejoinConfig,
|
||||
readOnlyName,
|
||||
setJoinByPhoneDialogVisiblity,
|
||||
showCameraPreview,
|
||||
showDialog,
|
||||
showErrorOnJoin,
|
||||
showRecordingWarning,
|
||||
showUnsafeRoomWarning,
|
||||
unsafeRoomConsent,
|
||||
updateSettings: dispatchUpdateSettings,
|
||||
videoTrack
|
||||
}: IProps) => {
|
||||
const showDisplayNameField = useMemo(
|
||||
() => isDisplayNameVisible && !readOnlyName,
|
||||
[ isDisplayNameVisible, readOnlyName ]);
|
||||
const showErrorOnField = useMemo(
|
||||
() => showDisplayNameField && showErrorOnJoin,
|
||||
[ showDisplayNameField, showErrorOnJoin ]);
|
||||
const [ showJoinByPhoneButtons, setShowJoinByPhoneButtons ] = useState(false);
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
/**
|
||||
* Handler for the join button.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onJoinButtonClick = () => {
|
||||
if (showErrorOnJoin) {
|
||||
dispatch(openDisplayNamePrompt({
|
||||
onPostSubmit: joinConference,
|
||||
validateInput: hasDisplayName
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Prejoin join button clicked.');
|
||||
|
||||
joinConference();
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the dropdown.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDropdownClose = () => {
|
||||
setShowJoinByPhoneButtons(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the join by phone buttons dropdown.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onOptionsClick = (e?: React.KeyboardEvent | React.MouseEvent | undefined) => {
|
||||
e?.stopPropagation();
|
||||
|
||||
setShowJoinByPhoneButtons(show => !show);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the guest participant name.
|
||||
*
|
||||
* @param {string} displayName - Participant name.
|
||||
* @returns {void}
|
||||
*/
|
||||
const setName = (displayName: string) => {
|
||||
dispatchUpdateSettings({
|
||||
displayName
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the join by phone dialog.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
const closeDialog = () => {
|
||||
setJoinByPhoneDialogVisiblity(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays the dialog for joining a meeting by phone.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
const doShowDialog = () => {
|
||||
setJoinByPhoneDialogVisiblity(true);
|
||||
onDropdownClose();
|
||||
};
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const showDialogKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
doShowDialog();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onJoinConferenceWithoutAudioKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (joinConferenceWithoutAudio
|
||||
&& (e.key === ' '
|
||||
|| e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
logger.info('Prejoin joinConferenceWithoutAudio dispatched on a key pressed.');
|
||||
joinConferenceWithoutAudio();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list of extra join buttons.
|
||||
*
|
||||
* @returns {Object} - The list of extra buttons.
|
||||
*/
|
||||
const getExtraJoinButtons = () => {
|
||||
const noAudio = {
|
||||
key: 'no-audio',
|
||||
testId: 'prejoin.joinWithoutAudio',
|
||||
icon: IconVolumeOff,
|
||||
label: t('prejoin.joinWithoutAudio'),
|
||||
onClick: () => {
|
||||
logger.info('Prejoin join conference without audio pressed.');
|
||||
joinConferenceWithoutAudio();
|
||||
},
|
||||
onKeyPress: onJoinConferenceWithoutAudioKeyPress
|
||||
};
|
||||
|
||||
const byPhone = {
|
||||
key: 'by-phone',
|
||||
testId: 'prejoin.joinByPhone',
|
||||
icon: IconPhoneRinging,
|
||||
label: t('prejoin.joinAudioByPhone'),
|
||||
onClick: doShowDialog,
|
||||
onKeyPress: showDialogKeyPress
|
||||
};
|
||||
|
||||
return {
|
||||
noAudio,
|
||||
byPhone
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle keypress on input.
|
||||
*
|
||||
* @param {KeyboardEvent} e - Keyboard event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onInputKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
logger.info('Dispatching join conference on Enter key press from the prejoin screen.');
|
||||
joinConference();
|
||||
}
|
||||
};
|
||||
|
||||
const extraJoinButtons = getExtraJoinButtons();
|
||||
let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: any) =>
|
||||
!(prejoinConfig?.hideExtraJoinButtons || []).includes(val.key)
|
||||
);
|
||||
|
||||
if (!hasJoinByPhoneButton) {
|
||||
extraButtonsToRender = extraButtonsToRender.filter((btn: any) => btn.key !== 'by-phone');
|
||||
}
|
||||
const hasExtraJoinButtons = Boolean(extraButtonsToRender.length);
|
||||
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
showDeviceStatus = { deviceStatusVisible }
|
||||
showRecordingWarning = { showRecordingWarning }
|
||||
showUnsafeRoomWarning = { showUnsafeRoomWarning }
|
||||
title = { t('prejoin.joinMeeting') }
|
||||
videoMuted = { !showCameraPreview }
|
||||
videoTrack = { videoTrack }>
|
||||
<div
|
||||
className = { classes.inputContainer }
|
||||
data-testid = 'prejoin.screen'>
|
||||
{showDisplayNameField ? (<Input
|
||||
accessibilityLabel = { t('dialog.enterDisplayName') }
|
||||
autoComplete = { 'name' }
|
||||
autoFocus = { true }
|
||||
className = { classes.input }
|
||||
error = { showErrorOnField }
|
||||
id = 'premeeting-name-input'
|
||||
onChange = { setName }
|
||||
onKeyPress = { showUnsafeRoomWarning && !unsafeRoomConsent ? undefined : onInputKeyPress }
|
||||
placeholder = { t('dialog.enterDisplayName') }
|
||||
readOnly = { readOnlyName }
|
||||
value = { name } />
|
||||
) : (
|
||||
<div className = { classes.avatarContainer }>
|
||||
<Avatar
|
||||
className = { classes.avatar }
|
||||
displayName = { name }
|
||||
participantId = { participantId }
|
||||
size = { 72 } />
|
||||
{isDisplayNameVisible && <div className = { classes.avatarName }>{name}</div>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showErrorOnField && <div
|
||||
className = { classes.error }
|
||||
data-testid = 'prejoin.errorMessage'>
|
||||
<p aria-live = 'polite' >
|
||||
{t('prejoin.errorMissingName')}
|
||||
</p>
|
||||
</div>}
|
||||
|
||||
<div className = { classes.dropdownContainer }>
|
||||
<Popover
|
||||
content = { hasExtraJoinButtons && <div className = { classes.dropdownButtons }>
|
||||
{extraButtonsToRender.map(({ key, ...rest }) => (
|
||||
<Button
|
||||
disabled = { joiningInProgress || showErrorOnField }
|
||||
fullWidth = { true }
|
||||
key = { key }
|
||||
type = { BUTTON_TYPES.SECONDARY }
|
||||
{ ...rest } />
|
||||
))}
|
||||
</div> }
|
||||
onPopoverClose = { onDropdownClose }
|
||||
position = 'bottom'
|
||||
trigger = 'click'
|
||||
visible = { showJoinByPhoneButtons }>
|
||||
<ActionButton
|
||||
OptionsIcon = { showJoinByPhoneButtons ? IconArrowUp : IconArrowDown }
|
||||
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
|
||||
ariaLabel = { t('prejoin.joinMeeting') }
|
||||
ariaPressed = { showJoinByPhoneButtons }
|
||||
disabled = { joiningInProgress
|
||||
|| (showUnsafeRoomWarning && !unsafeRoomConsent)
|
||||
|| showErrorOnField }
|
||||
hasOptions = { hasExtraJoinButtons }
|
||||
onClick = { onJoinButtonClick }
|
||||
onOptionsClick = { onOptionsClick }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }
|
||||
testId = 'prejoin.joinMeeting'
|
||||
type = 'primary'>
|
||||
{t('prejoin.joinMeeting')}
|
||||
</ActionButton>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
{showDialog && (
|
||||
<JoinByPhoneDialog
|
||||
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
|
||||
onClose = { closeDialog } />
|
||||
)}
|
||||
</PreMeetingScreen>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const name = getDisplayName(state);
|
||||
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
|
||||
const { id: participantId } = getLocalParticipant(state) ?? {};
|
||||
const { joiningInProgress } = state['features/prejoin'];
|
||||
const { room } = state['features/base/conference'];
|
||||
const { unsafeRoomConsent } = state['features/base/premeeting'];
|
||||
const { showPrejoinWarning: showRecordingWarning } = state['features/base/config'].recordings ?? {};
|
||||
|
||||
return {
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
|
||||
isDisplayNameVisible: isPrejoinDisplayNameVisible(state),
|
||||
joiningInProgress,
|
||||
name,
|
||||
participantId,
|
||||
prejoinConfig: state['features/base/config'].prejoinConfig,
|
||||
readOnlyName: isNameReadOnly(state),
|
||||
showCameraPreview: !isVideoMutedByUser(state),
|
||||
showDialog: isJoinByPhoneDialogVisible(state),
|
||||
showErrorOnJoin,
|
||||
showRecordingWarning: Boolean(showRecordingWarning),
|
||||
showUnsafeRoomWarning: isInsecureRoomName(room) && isUnsafeRoomWarningEnabled(state),
|
||||
unsafeRoomConsent,
|
||||
videoTrack: getLocalJitsiVideoTrack(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
|
||||
joinConference: joinConferenceAction,
|
||||
setJoinByPhoneDialogVisiblity: setJoinByPhoneDialogVisiblityAction,
|
||||
updateSettings
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Prejoin);
|
||||
96
react/features/prejoin/components/web/PrejoinApp.tsx
Normal file
96
react/features/prejoin/components/web/PrejoinApp.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import BaseApp from '../../../base/app/components/BaseApp';
|
||||
import { setConfig } from '../../../base/config/actions';
|
||||
import { createPrejoinTracks } from '../../../base/tracks/functions.web';
|
||||
import GlobalStyles from '../../../base/ui/components/GlobalStyles.web';
|
||||
import JitsiThemeProvider from '../../../base/ui/components/JitsiThemeProvider.web';
|
||||
import DialogContainer from '../../../base/ui/components/web/DialogContainer';
|
||||
import { setupInitialDevices } from '../../../conference/actions.web';
|
||||
import { initPrejoin } from '../../functions.web';
|
||||
|
||||
import PrejoinThirdParty from './PrejoinThirdParty';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates the style type that needs to be applied.
|
||||
*/
|
||||
styleType: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper application for prejoin.
|
||||
*
|
||||
* @augments BaseApp
|
||||
*/
|
||||
export default class PrejoinApp extends BaseApp<Props> {
|
||||
|
||||
/**
|
||||
* Navigates to {@link Prejoin} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
const { store } = this.state;
|
||||
const { dispatch } = store ?? {};
|
||||
const { styleType } = this.props;
|
||||
|
||||
super._navigate({
|
||||
component: PrejoinThirdParty,
|
||||
props: {
|
||||
className: styleType
|
||||
}
|
||||
});
|
||||
|
||||
const { startWithAudioMuted, startWithVideoMuted } = store
|
||||
? store.getState()['features/base/settings']
|
||||
: { startWithAudioMuted: undefined,
|
||||
startWithVideoMuted: undefined };
|
||||
|
||||
dispatch?.(setConfig({
|
||||
prejoinConfig: {
|
||||
enabled: true
|
||||
},
|
||||
startWithAudioMuted,
|
||||
startWithVideoMuted
|
||||
}));
|
||||
|
||||
await dispatch?.(setupInitialDevices());
|
||||
const { tryCreateLocalTracks, errors } = createPrejoinTracks();
|
||||
|
||||
const tracks = await tryCreateLocalTracks;
|
||||
|
||||
initPrejoin(tracks, errors, dispatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
override _createMainElement(component: ComponentType<any>, props: Object) {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<GlobalStyles />
|
||||
{ super._createMainElement(component, props) }
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
override _renderDialogContainer() {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<DialogContainer />
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
81
react/features/prejoin/components/web/PrejoinThirdParty.tsx
Normal file
81
react/features/prejoin/components/web/PrejoinThirdParty.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { isVideoMutedByUser } from '../../../base/media/functions';
|
||||
import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeetingScreen';
|
||||
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
|
||||
import { isDeviceStatusVisible } from '../../functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The JitsiLocalTrack to display.
|
||||
*/
|
||||
videoTrack?: Object;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This component is displayed before joining a meeting.
|
||||
*/
|
||||
class PrejoinThirdParty extends Component<IProps> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
className,
|
||||
deviceStatusVisible,
|
||||
showCameraPreview,
|
||||
videoTrack
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
className = { `prejoin-third-party ${className}` }
|
||||
showDeviceStatus = { deviceStatusVisible }
|
||||
skipPrejoinButton = { false }
|
||||
thirdParty = { true }
|
||||
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: IReduxState) {
|
||||
return {
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
showCameraPreview: !isVideoMutedByUser(state),
|
||||
videoTrack: getLocalJitsiVideoTrack(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(translate(PrejoinThirdParty));
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { countries } from '../../../utils';
|
||||
|
||||
import CountryRow from './CountryRow';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Click handler for a single entry.
|
||||
*/
|
||||
onEntryClick: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
height: '190px',
|
||||
width: '343px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: theme.palette.ui01
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This component displays the dropdown for the country picker.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function CountryDropdown({ onEntryClick }: IProps) {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{countries.map(country => (
|
||||
<CountryRow
|
||||
country = { country }
|
||||
key = { `${country.code}` }
|
||||
onEntryClick = { onEntryClick } />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CountryDropdown;
|
||||
@@ -0,0 +1,163 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import Popover from '../../../../base/popover/components/Popover.web';
|
||||
import { setDialOutCountry, setDialOutNumber } from '../../../actions.web';
|
||||
import { getDialOutCountry, getDialOutNumber } from '../../../functions';
|
||||
import { getCountryFromDialCodeText } from '../../../utils';
|
||||
|
||||
import CountryDropDown from './CountryDropdown';
|
||||
import CountrySelector from './CountrySelector';
|
||||
|
||||
const PREFIX_REG = /^(00)|\+/;
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The country to dial out to.
|
||||
*/
|
||||
dialOutCountry: { code: string; dialCode: string; name: string; };
|
||||
|
||||
/**
|
||||
* The number to dial out to.
|
||||
*/
|
||||
dialOutNumber: string;
|
||||
|
||||
/**
|
||||
* Handler used when user presses 'Enter'.
|
||||
*/
|
||||
onSubmit: Function;
|
||||
|
||||
/**
|
||||
* Sets the dial out country.
|
||||
*/
|
||||
setDialOutCountry: Function;
|
||||
|
||||
/**
|
||||
* Sets the dial out number.
|
||||
*/
|
||||
setDialOutNumber: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
border: 0,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
display: 'flex',
|
||||
backgroundColor: theme.palette.ui03
|
||||
},
|
||||
|
||||
input: {
|
||||
padding: '0 4px',
|
||||
margin: 0,
|
||||
border: 0,
|
||||
background: 'transparent',
|
||||
color: theme.palette.text01,
|
||||
flexGrow: 1,
|
||||
...theme.typography.bodyShortRegular
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const CountryPicker = (props: IProps) => {
|
||||
const [ isOpen, setIsOpen ] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { classes } = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const onChange = ({ target: { value: newValue } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (PREFIX_REG.test(newValue)) {
|
||||
const textWithDialCode = newValue.replace(PREFIX_REG, '');
|
||||
|
||||
if (textWithDialCode.length >= 4) {
|
||||
const country = getCountryFromDialCodeText(textWithDialCode);
|
||||
|
||||
if (country) {
|
||||
const rest = textWithDialCode.replace(country.dialCode, '');
|
||||
|
||||
props.setDialOutCountry(country);
|
||||
props.setDialOutNumber(rest);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
props.setDialOutNumber(newValue);
|
||||
};
|
||||
|
||||
const onCountrySelectorClick = () => {
|
||||
setIsOpen(open => !open);
|
||||
};
|
||||
|
||||
const onDropdownClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onEntryClick = (country: { code: string; dialCode: string; name: string; }) => {
|
||||
props.setDialOutCountry(country);
|
||||
onDropdownClose();
|
||||
};
|
||||
|
||||
const onKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
props.onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
<Popover
|
||||
content = { <CountryDropDown onEntryClick = { onEntryClick } /> }
|
||||
onPopoverClose = { onDropdownClose }
|
||||
position = 'bottom'
|
||||
trigger = 'click'
|
||||
visible = { isOpen }>
|
||||
<div className = { classes.container }>
|
||||
<CountrySelector
|
||||
country = { props.dialOutCountry }
|
||||
onClick = { onCountrySelectorClick } />
|
||||
<input
|
||||
className = { classes.input }
|
||||
onChange = { onChange }
|
||||
onKeyPress = { onKeyPress }
|
||||
ref = { inputRef }
|
||||
value = { props.dialOutNumber } />
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
dialOutCountry: getDialOutCountry(state),
|
||||
dialOutNumber: getDialOutNumber(state)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux actions to the props of the component.
|
||||
*
|
||||
* @type {{
|
||||
* setDialOutCountry: Function,
|
||||
* setDialOutNumber: Function
|
||||
* }}
|
||||
*/
|
||||
const mapDispatchToProps = {
|
||||
setDialOutCountry,
|
||||
setDialOutNumber
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CountryPicker);
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Country of the entry.
|
||||
*/
|
||||
country: { code: string; dialCode: string; name: string; };
|
||||
|
||||
/**
|
||||
* Entry click handler.
|
||||
*/
|
||||
onEntryClick: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
padding: '10px',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.palette.action03,
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action03Hover
|
||||
}
|
||||
},
|
||||
|
||||
flag: {
|
||||
marginRight: theme.spacing(2)
|
||||
},
|
||||
|
||||
text: {
|
||||
color: theme.palette.text01,
|
||||
...theme.typography.bodyShortRegular,
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const CountryRow = ({ country, onEntryClick }: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const _onClick = () => {
|
||||
onEntryClick(country);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { _onClick }>
|
||||
<div className = { cx(classes.flag, 'iti-flag', country.code) } />
|
||||
<div className = { classes.text }>
|
||||
{`${country.name} (+${country.dialCode})`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountryRow;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconArrowDown } from '../../../../base/icons/svg';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Country object of the entry.
|
||||
*/
|
||||
country: { code: string; dialCode: string; name: string; };
|
||||
|
||||
/**
|
||||
* Click handler for the selector.
|
||||
*/
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
padding: '8px 10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.palette.ui01,
|
||||
borderRight: `1px solid ${theme.palette.ui03}`,
|
||||
color: theme.palette.text01,
|
||||
...theme.typography.bodyShortRegular,
|
||||
position: 'relative',
|
||||
width: '88px',
|
||||
borderTopLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius
|
||||
},
|
||||
|
||||
text: {
|
||||
flexGrow: 1
|
||||
},
|
||||
|
||||
flag: {
|
||||
marginRight: theme.spacing(2)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This component displays the country selector with the flag.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function CountrySelector({ country: { code, dialCode }, onClick }: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
const onKeyPressHandler = useCallback(e => {
|
||||
if (onClick && (e.key === ' ' || e.key === 'Enter')) {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}
|
||||
}, [ onClick ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
onClick = { onClick }
|
||||
onKeyPress = { onKeyPressHandler }>
|
||||
<div className = { cx(classes.flag, 'iti-flag', code) } />
|
||||
<span className = { classes.text }>{`+${dialCode}`}</span>
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconArrowDown } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CountrySelector;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Avatar from '../../../../base/avatar/components/Avatar';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../../../base/icons/svg';
|
||||
import Label from '../Label';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The phone number that is being called.
|
||||
*/
|
||||
number: string;
|
||||
|
||||
/**
|
||||
* Closes the dialog.
|
||||
*/
|
||||
onClose: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* The status of the call.
|
||||
*/
|
||||
status: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
callingDialog: {
|
||||
padding: theme.spacing(3),
|
||||
textAlign: 'center',
|
||||
|
||||
'& .prejoin-dialog-calling-header': {
|
||||
textAlign: 'right'
|
||||
},
|
||||
|
||||
'& .prejoin-dialog-calling-label': {
|
||||
fontSize: '1rem',
|
||||
margin: `${theme.spacing(2)} 0 ${theme.spacing(3)} 0`
|
||||
},
|
||||
|
||||
'& .prejoin-dialog-calling-number': {
|
||||
fontSize: '1.25rem',
|
||||
lineHeight: '1.75rem',
|
||||
margin: `${theme.spacing(3)} 0`
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Dialog displayed when the user gets called by the meeting.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function CallingDialog(props: IProps) {
|
||||
const { number, onClose, status, t } = props;
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.callingDialog }>
|
||||
<div className = 'prejoin-dialog-calling-header'>
|
||||
<Icon
|
||||
className = 'prejoin-dialog-icon'
|
||||
onClick = { onClose }
|
||||
role = 'button'
|
||||
size = { 24 }
|
||||
src = { IconCloseLarge } />
|
||||
</div>
|
||||
<Label className = 'prejoin-dialog-calling-label'>
|
||||
{t(status)}
|
||||
</Label>
|
||||
<Avatar size = { 72 } />
|
||||
<div className = 'prejoin-dialog-calling-number'>{number}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(CallingDialog);
|
||||
172
react/features/prejoin/components/web/dialogs/DialInDialog.tsx
Normal file
172
react/features/prejoin/components/web/dialogs/DialInDialog.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconArrowLeft } from '../../../../base/icons/svg';
|
||||
import Button from '../../../../base/ui/components/web/Button';
|
||||
import { getCountryCodeFromPhone } from '../../../utils';
|
||||
import Label from '../Label';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The number to call in order to join the conference.
|
||||
*/
|
||||
number: string | null;
|
||||
|
||||
/**
|
||||
* Handler used when clicking the back button.
|
||||
*/
|
||||
onBack: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Click handler for primary button.
|
||||
*/
|
||||
onPrimaryButtonClick: Function;
|
||||
|
||||
/**
|
||||
* Click handler for the small additional text.
|
||||
*/
|
||||
onSmallTextClick: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Click handler for the text button.
|
||||
*/
|
||||
onTextButtonClick: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* The passCode of the conference.
|
||||
*/
|
||||
passCode?: string | number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialInDialog: {
|
||||
textAlign: 'center',
|
||||
|
||||
'& .prejoin-dialog-dialin-header': {
|
||||
alignItems: 'center',
|
||||
margin: `${theme.spacing(3)} 0 ${theme.spacing(5)} ${theme.spacing(3)}`,
|
||||
display: 'flex'
|
||||
},
|
||||
'& .prejoin-dialog-dialin-icon': {
|
||||
marginRight: theme.spacing(3)
|
||||
},
|
||||
'& .prejoin-dialog-dialin-num': {
|
||||
background: '#3e474f',
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
margin: theme.spacing(1),
|
||||
padding: theme.spacing(2),
|
||||
userSelect: 'text',
|
||||
|
||||
'& .prejoin-dialog-dialin-num-container': {
|
||||
minHeight: '48px',
|
||||
margin: `${theme.spacing(2)} 0`
|
||||
},
|
||||
|
||||
'& span': {
|
||||
userSelect: 'text'
|
||||
}
|
||||
},
|
||||
|
||||
'& .prejoin-dialog-dialin-link': {
|
||||
color: '#6FB1EA',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-block',
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.5rem',
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
'& .prejoin-dialog-dialin-spaced-label': {
|
||||
marginBottom: theme.spacing(3),
|
||||
marginTop: '28px'
|
||||
},
|
||||
'& .prejoin-dialog-dialin-btns > div': {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This component displays the dialog with all the information
|
||||
* to join a meeting by calling it.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
function DialinDialog(props: IProps) {
|
||||
const {
|
||||
number,
|
||||
onBack,
|
||||
onPrimaryButtonClick,
|
||||
onSmallTextClick,
|
||||
onTextButtonClick,
|
||||
passCode,
|
||||
t
|
||||
} = props;
|
||||
const { classes } = useStyles();
|
||||
const flagClassName = `prejoin-dialog-flag iti-flag ${getCountryCodeFromPhone(
|
||||
number ?? ''
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<div className = { classes.dialInDialog }>
|
||||
<div className = 'prejoin-dialog-dialin-header'>
|
||||
<Icon
|
||||
className = 'prejoin-dialog-icon prejoin-dialog-dialin-icon'
|
||||
onClick = { onBack }
|
||||
role = 'button'
|
||||
size = { 24 }
|
||||
src = { IconArrowLeft } />
|
||||
<div className = 'prejoin-dialog-title'>
|
||||
{t('prejoin.dialInMeeting')}
|
||||
</div>
|
||||
</div>
|
||||
<Label number = { 1 }>{ t('prejoin.dialInPin') }</Label>
|
||||
|
||||
<div className = 'prejoin-dialog-dialin-num-container'>
|
||||
<div className = 'prejoin-dialog-dialin-num'>
|
||||
<div className = { flagClassName } />
|
||||
<span>{number}</span>
|
||||
</div>
|
||||
<div className = 'prejoin-dialog-dialin-num'>{passCode}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className = 'prejoin-dialog-dialin-link'
|
||||
onClick = { onSmallTextClick }>
|
||||
{t('prejoin.viewAllNumbers')}
|
||||
</span>
|
||||
</div>
|
||||
<div className = 'prejoin-dialog-delimiter' />
|
||||
<Label
|
||||
className = 'prejoin-dialog-dialin-spaced-label'
|
||||
number = { 2 }>
|
||||
{t('prejoin.connectedWithAudioQ')}
|
||||
</Label>
|
||||
<div className = 'prejoin-dialog-dialin-btns'>
|
||||
<Button
|
||||
className = 'prejoin-dialog-btn'
|
||||
fullWidth = { true }
|
||||
labelKey = 'prejoin.joinMeeting'
|
||||
onClick = { onPrimaryButtonClick }
|
||||
type = 'primary' />
|
||||
<Button
|
||||
className = 'prejoin-dialog-btn'
|
||||
fullWidth = { true }
|
||||
labelKey = 'dialog.Cancel'
|
||||
onClick = { onTextButtonClick }
|
||||
type = 'tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(DialinDialog);
|
||||
100
react/features/prejoin/components/web/dialogs/DialOutDialog.tsx
Normal file
100
react/features/prejoin/components/web/dialogs/DialOutDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../../../base/icons/svg';
|
||||
import Button from '../../../../base/ui/components/web/Button';
|
||||
import Label from '../Label';
|
||||
import CountryPicker from '../country-picker/CountryPicker';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Closes a dialog.
|
||||
*/
|
||||
onClose: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Submit handler.
|
||||
*/
|
||||
onSubmit: Function;
|
||||
|
||||
/**
|
||||
* Handler for text button.
|
||||
*/
|
||||
onTextButtonClick: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialOutDialog: {
|
||||
padding: theme.spacing(3)
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
picker: {
|
||||
margin: `${theme.spacing(2)} 0 ${theme.spacing(3)} 0`
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* This component displays the dialog from which the user can enter the
|
||||
* phone number in order to be called by the meeting.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
function DialOutDialog(props: IProps) {
|
||||
const { onClose, onTextButtonClick, onSubmit, t } = props;
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.dialOutDialog }>
|
||||
<div className = { classes.header }>
|
||||
<div className = 'prejoin-dialog-title'>
|
||||
{t('prejoin.startWithPhone')}
|
||||
</div>
|
||||
<Icon
|
||||
className = 'prejoin-dialog-icon'
|
||||
onClick = { onClose }
|
||||
role = 'button'
|
||||
size = { 24 }
|
||||
src = { IconCloseLarge } />
|
||||
</div>
|
||||
<Label>{t('prejoin.callMeAtNumber')}</Label>
|
||||
<div className = { classes.picker }>
|
||||
<CountryPicker onSubmit = { onSubmit } />
|
||||
</div>
|
||||
<Button
|
||||
className = 'prejoin-dialog-btn'
|
||||
fullWidth = { true }
|
||||
labelKey = 'prejoin.callMe'
|
||||
onClick = { onSubmit }
|
||||
type = 'primary' />
|
||||
<div className = 'prejoin-dialog-delimiter-container'>
|
||||
<div className = 'prejoin-dialog-delimiter' />
|
||||
<div className = 'prejoin-dialog-delimiter-txt-container'>
|
||||
<span className = 'prejoin-dialog-delimiter-txt'>
|
||||
{t('prejoin.or')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'prejoin-dialog-dialin-container'>
|
||||
<Button
|
||||
className = 'prejoin-dialog-btn'
|
||||
fullWidth = { true }
|
||||
labelKey = 'prejoin.iWantToDialIn'
|
||||
onClick = { onTextButtonClick }
|
||||
type = 'tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(DialOutDialog);
|
||||
@@ -0,0 +1,239 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { updateDialInNumbers } from '../../../../invite/actions.web';
|
||||
import { getConferenceId, getDefaultDialInNumber } from '../../../../invite/functions';
|
||||
import {
|
||||
dialOut as dialOutAction,
|
||||
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
|
||||
openDialInPage as openDialInPageAction
|
||||
} from '../../../actions.web';
|
||||
import { getDialOutStatus, getFullDialOutNumber } from '../../../functions';
|
||||
|
||||
import CallingDialog from './CallingDialog';
|
||||
import DialInDialog from './DialInDialog';
|
||||
import DialOutDialog from './DialOutDialog';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The number to call in order to join the conference.
|
||||
*/
|
||||
dialInNumber: string | null;
|
||||
|
||||
/**
|
||||
* The action by which the meeting calls the user.
|
||||
*/
|
||||
dialOut: Function;
|
||||
|
||||
/**
|
||||
* The number the conference should call.
|
||||
*/
|
||||
dialOutNumber: string;
|
||||
|
||||
/**
|
||||
* The status of the call when the meeting calls the user.
|
||||
*/
|
||||
dialOutStatus: string;
|
||||
|
||||
/**
|
||||
* Fetches conference dial in numbers & conference id.
|
||||
*/
|
||||
fetchConferenceDetails: Function;
|
||||
|
||||
/**
|
||||
* Joins the conference without audio.
|
||||
*/
|
||||
joinConferenceWithoutAudio: Function;
|
||||
|
||||
/**
|
||||
* Closes the dialog.
|
||||
*/
|
||||
onClose: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Opens a web page with all the dial in numbers.
|
||||
*/
|
||||
openDialInPage: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* The passCode of the conference used when joining a meeting by phone.
|
||||
*/
|
||||
passCode?: string | number;
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The dialout call is ongoing, 'CallingDialog' is shown;.
|
||||
*/
|
||||
isCalling: boolean;
|
||||
|
||||
/**
|
||||
* If should show 'DialInDialog'.
|
||||
*/
|
||||
showDialIn: boolean;
|
||||
|
||||
/**
|
||||
* If should show 'DialOutDialog'.
|
||||
*/
|
||||
showDialOut: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the dialog shown when a user wants to join with phone audio.
|
||||
*/
|
||||
class JoinByPhoneDialog extends PureComponent<IProps, State> {
|
||||
/**
|
||||
* Initializes a new {@code JoinByPhoneDialog} instance.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isCalling: false,
|
||||
showDialOut: true,
|
||||
showDialIn: false
|
||||
};
|
||||
|
||||
this._dialOut = this._dialOut.bind(this);
|
||||
this._showDialInDialog = this._showDialInDialog.bind(this);
|
||||
this._showDialOutDialog = this._showDialOutDialog.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Meeting calls the user & shows the 'CallingDialog'.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_dialOut() {
|
||||
const { dialOut, joinConferenceWithoutAudio } = this.props;
|
||||
|
||||
this.setState({
|
||||
isCalling: true,
|
||||
showDialOut: false,
|
||||
showDialIn: false
|
||||
});
|
||||
dialOut(joinConferenceWithoutAudio, this._showDialOutDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the 'DialInDialog'.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_showDialInDialog() {
|
||||
this.setState({
|
||||
isCalling: false,
|
||||
showDialOut: false,
|
||||
showDialIn: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the 'DialOutDialog'.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_showDialOutDialog() {
|
||||
this.setState({
|
||||
isCalling: false,
|
||||
showDialOut: true,
|
||||
showDialIn: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this.props.fetchConferenceDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
dialOutStatus,
|
||||
dialInNumber,
|
||||
dialOutNumber,
|
||||
joinConferenceWithoutAudio,
|
||||
passCode,
|
||||
onClose,
|
||||
openDialInPage
|
||||
} = this.props;
|
||||
const {
|
||||
_dialOut,
|
||||
_showDialInDialog,
|
||||
_showDialOutDialog
|
||||
} = this;
|
||||
const { isCalling, showDialOut, showDialIn } = this.state;
|
||||
const className = isCalling
|
||||
? 'prejoin-dialog prejoin-dialog--small'
|
||||
: 'prejoin-dialog';
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-dialog-container'>
|
||||
<div className = { className }>
|
||||
{showDialOut && (
|
||||
<DialOutDialog
|
||||
onClose = { onClose }
|
||||
onSubmit = { _dialOut }
|
||||
onTextButtonClick = { _showDialInDialog } />
|
||||
)}
|
||||
{showDialIn && (
|
||||
<DialInDialog
|
||||
number = { dialInNumber }
|
||||
onBack = { _showDialOutDialog }
|
||||
onPrimaryButtonClick = { joinConferenceWithoutAudio }
|
||||
onSmallTextClick = { openDialInPage }
|
||||
onTextButtonClick = { onClose }
|
||||
passCode = { passCode } />
|
||||
)}
|
||||
{isCalling && (
|
||||
<CallingDialog
|
||||
number = { dialOutNumber }
|
||||
onClose = { onClose }
|
||||
status = { dialOutStatus } />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} _ownProps - Component's own props.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
return {
|
||||
dialInNumber: getDefaultDialInNumber(state),
|
||||
dialOutNumber: getFullDialOutNumber(state),
|
||||
dialOutStatus: getDialOutStatus(state),
|
||||
passCode: getConferenceId(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dialOut: dialOutAction,
|
||||
fetchConferenceDetails: updateDialInNumbers,
|
||||
joinConferenceWithoutAudio: joinConferenceWithoutAudioAction,
|
||||
openDialInPage: openDialInPageAction
|
||||
};
|
||||
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JoinByPhoneDialog);
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { ColorPalette } from '../../../../base/styles/components/styles/ColorPalette';
|
||||
import {
|
||||
getDeviceStatusText,
|
||||
getDeviceStatusType
|
||||
} from '../../../functions';
|
||||
|
||||
const useStyles = makeStyles<{ deviceStatusType?: string; }>()((theme, { deviceStatusType = 'pending' }) => {
|
||||
return {
|
||||
deviceStatus: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...theme.typography.bodyShortRegular,
|
||||
color: '#fff',
|
||||
marginTop: theme.spacing(4),
|
||||
|
||||
'& span': {
|
||||
marginLeft: theme.spacing(3)
|
||||
},
|
||||
|
||||
'&.device-status-error': {
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: theme.palette.warning01,
|
||||
borderRadius: '6px',
|
||||
color: theme.palette.uiBackground,
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
marginTop: theme.spacing(2)
|
||||
},
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
marginTop: 0
|
||||
}
|
||||
},
|
||||
indicator: {
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
borderRadius: '100%',
|
||||
backgroundColor: deviceStatusType === 'ok' ? theme.palette.success01 : ColorPalette.darkGrey
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Strip showing the current status of the devices.
|
||||
* User is informed if there are missing or malfunctioning devices.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function DeviceStatus() {
|
||||
const { t } = useTranslation();
|
||||
const deviceStatusType = useSelector(getDeviceStatusType);
|
||||
const deviceStatusText = useSelector(getDeviceStatusText);
|
||||
const { classes, cx } = useStyles({ deviceStatusType });
|
||||
const hasError = deviceStatusType === 'warning';
|
||||
const containerClassName = cx(classes.deviceStatus, { 'device-status-error': hasError });
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { containerClassName }
|
||||
role = 'alert'
|
||||
tabIndex = { -1 }>
|
||||
{!hasError && <div className = { classes.indicator } />}
|
||||
<span
|
||||
aria-level = { 3 }
|
||||
role = 'heading'>
|
||||
{hasError ? t('prejoin.errorNoPermissions') : t(deviceStatusText ?? '')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeviceStatus;
|
||||
Reference in New Issue
Block a user