init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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;

View File

@@ -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;

View 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;

View 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
}
};

View 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;

View 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);

View 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>
);
}
}

View 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));

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View 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);

View 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);

View File

@@ -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);

View File

@@ -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;