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,50 @@
/**
* Action type to signal that joining is in progress.
*/
export const PREJOIN_JOINING_IN_PROGRESS = 'PREJOIN_JOINING_IN_PROGRESS';
/**
* Action type to signal that prejoin page was initialized.
*/
export const PREJOIN_INITIALIZED = 'PREJOIN_INITIALIZED';
/**
* Action type to set the status of the device.
*/
export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
/**
* Action type to set the visibility of the prejoin page when client is forcefully reloaded.
*/
export const SET_SKIP_PREJOIN_RELOAD = 'SET_SKIP_PREJOIN_RELOAD';
/**
* Action type to set the country to dial out to.
*/
export const SET_DIALOUT_COUNTRY = 'SET_DIALOUT_COUNTRY';
/**
* Action type to set the dial out number.
*/
export const SET_DIALOUT_NUMBER = 'SET_DIALOUT_NUMBER';
/**
* Action type to set the dial out status while dialing.
*/
export const SET_DIALOUT_STATUS = 'SET_DIALOUT_STATUS';
/**
* Action type to set the visibility of the 'JoinByPhone' dialog.
*/
export const SET_JOIN_BY_PHONE_DIALOG_VISIBLITY = 'SET_JOIN_BY_PHONE_DIALOG_VISIBLITY';
/**
* Action type to set the errors while creating the prejoin streams.
*/
export const SET_PREJOIN_DEVICE_ERRORS = 'SET_PREJOIN_DEVICE_ERRORS';
/**
* Action type to set the visibility of the prejoin page.
*/
export const SET_PREJOIN_PAGE_VISIBILITY = 'SET_PREJOIN_PAGE_VISIBILITY';

View File

@@ -0,0 +1,23 @@
import { IStore } from '../app/types';
import { connect } from '../base/connection/actions.native';
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { showVisitorsQueue } from '../visitors/functions';
/**
* Action used to start the conference.
*
* @param {Object} options - The config options that override the default ones (if any).
* @param {boolean} _ignoreJoiningInProgress - If true we won't check the joiningInProgress flag.
* @returns {Function}
*/
export function joinConference(options?: Object, _ignoreJoiningInProgress = false) {
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const _showVisitorsQueue = showVisitorsQueue(getState);
if (_showVisitorsQueue) {
dispatch(connect());
navigateRoot(screen.conference.root);
}
};
}

View File

@@ -0,0 +1,464 @@
import { v4 as uuidv4 } from 'uuid';
import { IStore } from '../app/types';
import { updateConfig } from '../base/config/actions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions';
import { connect } from '../base/connection/actions';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import { isVideoMutedByUser } from '../base/media/functions';
import { updateSettings } from '../base/settings/actions';
import { replaceLocalTrack } from '../base/tracks/actions';
import {
createLocalTracksF,
getLocalAudioTrack,
getLocalVideoTrack
} from '../base/tracks/functions';
import { openURLInBrowser } from '../base/util/openURLInBrowser';
import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
import { showErrorNotification } from '../notifications/actions';
import { INotificationProps } from '../notifications/types';
import {
PREJOIN_JOINING_IN_PROGRESS,
SET_DEVICE_STATUS,
SET_DIALOUT_COUNTRY,
SET_DIALOUT_NUMBER,
SET_DIALOUT_STATUS,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_PAGE_VISIBILITY,
SET_SKIP_PREJOIN_RELOAD
} from './actionTypes';
import {
getDialOutConferenceUrl,
getDialOutCountry,
getFullDialOutNumber,
isJoinByPhoneDialogVisible
} from './functions.any';
import logger from './logger';
const dialOutStatusToKeyMap = {
INITIATED: 'presenceStatus.calling',
RINGING: 'presenceStatus.ringing'
};
const DIAL_OUT_STATUS = {
INITIATED: 'INITIATED',
RINGING: 'RINGING',
CONNECTED: 'CONNECTED',
DISCONNECTED: 'DISCONNECTED',
FAILED: 'FAILED'
};
/**
* The time interval used between requests while polling for dial out status.
*/
const STATUS_REQ_FREQUENCY = 2000;
/**
* The maximum number of retries while polling for dial out status.
*/
const STATUS_REQ_CAP = 45;
/**
* Polls for status change after dial out.
* Changes dialog message based on response, closes the dialog if there is an error,
* joins the meeting when CONNECTED.
*
* @param {string} reqId - The request id used to correlate the dial out request with this one.
* @param {Function} onSuccess - Success handler.
* @param {Function} onFail - Fail handler.
* @param {number} count - The number of retried calls. When it hits STATUS_REQ_CAP it should no longer make requests.
* @returns {Function}
*/
function pollForStatus(
reqId: string,
onSuccess: Function,
onFail: Function,
count = 0) {
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const state = getState();
try {
if (!isJoinByPhoneDialogVisible(state)) {
return;
}
const res = await executeDialOutStatusRequest(getDialOutStatusUrl(state) ?? '', reqId);
switch (res) {
case DIAL_OUT_STATUS.INITIATED:
case DIAL_OUT_STATUS.RINGING: {
dispatch(setDialOutStatus(dialOutStatusToKeyMap[res as keyof typeof dialOutStatusToKeyMap]));
if (count < STATUS_REQ_CAP) {
return setTimeout(() => {
dispatch(pollForStatus(reqId, onSuccess, onFail, count + 1));
}, STATUS_REQ_FREQUENCY);
}
return onFail();
}
case DIAL_OUT_STATUS.CONNECTED: {
return onSuccess();
}
case DIAL_OUT_STATUS.DISCONNECTED: {
dispatch(showErrorNotification({
titleKey: 'prejoin.errorDialOutDisconnected'
}));
return onFail();
}
case DIAL_OUT_STATUS.FAILED: {
dispatch(showErrorNotification({
titleKey: 'prejoin.errorDialOutFailed'
}));
return onFail();
}
}
} catch (err) {
dispatch(showErrorNotification({
titleKey: 'prejoin.errorDialOutStatus'
}));
logger.error('Error getting dial out status', err);
onFail();
}
};
}
/**
* Action used for joining the meeting with phone audio.
* A dial out connection is tried and a polling mechanism is used for getting the status.
* If the connection succeeds the `onSuccess` callback is executed.
* If the phone connection fails or the number is invalid the `onFail` callback is executed.
*
* @param {Function} onSuccess - Success handler.
* @param {Function} onFail - Fail handler.
* @returns {Function}
*/
export function dialOut(onSuccess: Function, onFail: Function) {
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const state = getState();
const reqId = uuidv4();
const url = getDialOutUrl(state) ?? '';
const conferenceUrl = getDialOutConferenceUrl(state);
const phoneNumber = getFullDialOutNumber(state);
const countryCode = getDialOutCountry(state).code.toUpperCase();
const body = {
conferenceUrl,
countryCode,
name: phoneNumber,
phoneNumber
};
try {
await executeDialOutRequest(url, body, reqId);
dispatch(pollForStatus(reqId, onSuccess, onFail));
} catch (err: any) {
const notification: INotificationProps = {
titleKey: 'prejoin.errorDialOut',
titleArguments: undefined
};
if (err.status) {
if (err.messageKey === 'validation.failed') {
notification.titleKey = 'prejoin.errorValidation';
} else {
notification.titleKey = 'prejoin.errorStatusCode';
notification.titleArguments = { status: err.status };
}
}
dispatch(showErrorNotification(notification));
logger.error('Error dialing out', err);
onFail();
}
};
}
/**
* Action used to start the conference.
*
* @param {Object} options - The config options that override the default ones (if any).
* @param {boolean} ignoreJoiningInProgress - If true we won't check the joiningInProgress flag.
* @param {string?} jid - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string?} password - The XMPP user's password.
* @returns {Function}
*/
export function joinConference(options?: Object, ignoreJoiningInProgress = false,
jid?: string, password?: string) {
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
if (!ignoreJoiningInProgress) {
const state = getState();
const { joiningInProgress } = state['features/prejoin'];
if (joiningInProgress) {
return;
}
dispatch(setJoiningInProgress(true));
}
options && dispatch(updateConfig(options));
logger.info('Dispatching connect from joinConference.');
dispatch(connect(jid, password))
.catch(() => {
// There is nothing to do here. This is handled and dispatched in base/connection/actions.
});
};
}
/**
* Action used to set the flag for joining operation in progress.
*
* @param {boolean} value - The config options that override the default ones (if any).
* @returns {Function}
*/
export function setJoiningInProgress(value: boolean) {
return {
type: PREJOIN_JOINING_IN_PROGRESS,
value
};
}
/**
* Joins the conference without audio.
*
* @returns {Function}
*/
export function joinConferenceWithoutAudio() {
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const state = getState();
const { joiningInProgress } = state['features/prejoin'];
if (joiningInProgress) {
return;
}
dispatch(setJoiningInProgress(true));
const tracks = state['features/base/tracks'];
const audioTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
if (audioTrack) {
try {
await dispatch(replaceLocalTrack(audioTrack, null));
} catch (error) {
logger.error(`Failed to replace local audio with null: ${error}`);
}
}
logger.info('Dispatching joinConference action with startSilent=true from joinConferenceWithoutAudio.');
dispatch(joinConference({
startSilent: true
}, true));
};
}
/**
* Opens an external page with all the dial in numbers.
*
* @returns {Function}
*/
export function openDialInPage() {
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const dialInPage = getDialInfoPageURL(getState());
openURLInBrowser(dialInPage, true);
};
}
/**
* Creates a new audio track based on a device id and replaces the current one.
*
* @param {string} deviceId - The deviceId of the microphone.
* @returns {Function}
*/
export function replaceAudioTrackById(deviceId: string) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
try {
const tracks = getState()['features/base/tracks'];
const newTrack = await createLocalTrack('audio', deviceId);
const oldTrack = getLocalAudioTrack(tracks)?.jitsiTrack;
const micDeviceId = newTrack.getDeviceId();
logger.info(`Switching audio input device to ${micDeviceId}`);
dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
micDeviceId
}));
});
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.audioTrackError'));
logger.log('Error replacing audio track', err);
}
};
}
/**
* Creates a new video track based on a device id and replaces the current one.
*
* @param {string} deviceId - The deviceId of the camera.
* @returns {Function}
*/
export function replaceVideoTrackById(deviceId: string) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
try {
const tracks = getState()['features/base/tracks'];
const wasVideoMuted = isVideoMutedByUser(getState());
const [ newTrack ] = await createLocalTracksF(
{ cameraDeviceId: deviceId,
devices: [ 'video' ] },
{ dispatch,
getState }
);
const oldTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const cameraDeviceId = newTrack.getDeviceId();
logger.info(`Switching camera to ${cameraDeviceId}`);
dispatch(replaceLocalTrack(oldTrack, newTrack)).then(() => {
dispatch(updateSettings({
cameraDeviceId
}));
});
wasVideoMuted && newTrack.mute();
} catch (err) {
dispatch(setDeviceStatusWarning('prejoin.videoTrackError'));
logger.log('Error replacing video track', err);
}
};
}
/**
* Sets the device status as OK with the corresponding text.
*
* @param {string} deviceStatusText - The text to be set.
* @returns {Object}
*/
export function setDeviceStatusOk(deviceStatusText: string) {
return {
type: SET_DEVICE_STATUS,
value: {
deviceStatusText,
deviceStatusType: 'ok'
}
};
}
/**
* Sets the device status as 'warning' with the corresponding text.
*
* @param {string} deviceStatusText - The text to be set.
* @returns {Object}
*/
export function setDeviceStatusWarning(deviceStatusText: string) {
return {
type: SET_DEVICE_STATUS,
value: {
deviceStatusText,
deviceStatusType: 'warning'
}
};
}
/**
* Action used to set the dial out status.
*
* @param {string} value - The status.
* @returns {Object}
*/
function setDialOutStatus(value: string) {
return {
type: SET_DIALOUT_STATUS,
value
};
}
/**
* Action used to set the dial out country.
*
* @param {{ name: string, dialCode: string, code: string }} value - The country.
* @returns {Object}
*/
export function setDialOutCountry(value: Object) {
return {
type: SET_DIALOUT_COUNTRY,
value
};
}
/**
* Action used to set the dial out number.
*
* @param {string} value - The dial out number.
* @returns {Object}
*/
export function setDialOutNumber(value: string) {
return {
type: SET_DIALOUT_NUMBER,
value
};
}
/**
* Sets the visibility of the prejoin page when a client reload
* is triggered as a result of call migration initiated by Jicofo.
*
* @param {boolean} value - The visibility value.
* @returns {Object}
*/
export function setSkipPrejoinOnReload(value: boolean) {
return {
type: SET_SKIP_PREJOIN_RELOAD,
value
};
}
/**
* Action used to set the visiblitiy of the 'JoinByPhoneDialog'.
*
* @param {boolean} value - The value.
* @returns {Object}
*/
export function setJoinByPhoneDialogVisiblity(value: boolean) {
return {
type: SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
value
};
}
/**
* Action used to set the initial errors after creating the tracks.
*
* @param {Object} value - The track errors.
* @returns {Object}
*/
export function setPrejoinDeviceErrors(value: Object) {
return {
type: SET_PREJOIN_DEVICE_ERRORS,
value
};
}
/**
* Action used to set the visibility of the prejoin page.
*
* @param {boolean} value - The value.
* @returns {Object}
*/
export function setPrejoinPageVisibility(value: boolean) {
return {
type: SET_PREJOIN_PAGE_VISIBILITY,
value
};
}

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;

View File

@@ -0,0 +1,215 @@
import { IReduxState } from '../app/types';
import { getRoomName } from '../base/conference/functions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions.any';
import {
MEETING_NAME_ENABLED,
UNSAFE_ROOM_WARNING
} from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { isAudioMuted, isVideoMutedByUser } from '../base/media/functions';
import { getLobbyConfig } from '../lobby/functions';
/**
* Selector for the visibility of the 'join by phone' button.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isJoinByPhoneButtonVisible(state: IReduxState): boolean {
return Boolean(getDialOutUrl(state) && getDialOutStatusUrl(state));
}
/**
* Selector for determining if the device status strip is visible or not.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isDeviceStatusVisible(state: IReduxState): boolean {
return !(isAudioMuted(state) && isVideoMutedByUser(state))
&& !state['features/base/config'].startSilent
// This handles the case where disableInitialGUM=true and we haven't yet tried to create any tracks. In this
// case we shouldn't display the the device status indicator. But once we create some tracks we can show it
// because we would know if we created the tracks successfully or not.
&& (!state['features/base/config'].disableInitialGUM || state['features/base/tracks']?.length > 0);
}
/**
* Selector for determining if the display name is mandatory.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isDisplayNameRequired(state: IReduxState): boolean {
return Boolean(state['features/lobby']?.isDisplayNameRequiredError
|| state['features/base/config']?.requireDisplayName);
}
/**
* Selector for determining if the prejoin page is enabled in config. Defaults to `true`.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinEnabledInConfig(state: IReduxState): boolean {
return state['features/base/config'].prejoinConfig?.enabled ?? true;
}
/**
* Selector for determining if the prejoin display name field is visible.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinDisplayNameVisible(state: IReduxState): boolean {
return !state['features/base/config'].prejoinConfig?.hideDisplayName;
}
/**
* Returns the text for the prejoin status bar.
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getDeviceStatusText(state: IReduxState): string {
return state['features/prejoin']?.deviceStatusText;
}
/**
* Returns the type of the prejoin status bar: 'ok'|'warning'.
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getDeviceStatusType(state: IReduxState): string {
return state['features/prejoin']?.deviceStatusType;
}
/**
* Returns the 'conferenceUrl' used for dialing out.
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getDialOutConferenceUrl(state: IReduxState): string {
return `${getRoomName(state)}@${state['features/base/config'].hosts?.muc}`;
}
/**
* Selector for getting the dial out country.
*
* @param {IReduxState} state - The state of the app.
* @returns {Object}
*/
export function getDialOutCountry(state: IReduxState) {
return state['features/prejoin'].dialOutCountry;
}
/**
* Selector for getting the dial out number (without prefix).
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getDialOutNumber(state: IReduxState): string {
return state['features/prejoin'].dialOutNumber;
}
/**
* Selector for getting the dial out status while calling.
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getDialOutStatus(state: IReduxState): string {
return state['features/prejoin'].dialOutStatus;
}
/**
* Returns the full dial out number (containing country code and +).
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getFullDialOutNumber(state: IReduxState): string {
const dialOutNumber = getDialOutNumber(state);
const country = getDialOutCountry(state);
return `+${country.dialCode}${dialOutNumber}`;
}
/**
* Selector for getting the error if any while creating streams.
*
* @param {IReduxState} state - The state of the app.
* @returns {string}
*/
export function getRawError(state: IReduxState): string {
return state['features/prejoin']?.rawError;
}
/**
* Selector for getting the visibility state for the 'JoinByPhoneDialog'.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isJoinByPhoneDialogVisible(state: IReduxState): boolean {
return state['features/prejoin']?.showJoinByPhoneDialog;
}
/**
* Returns true if the prejoin page is enabled and no flag
* to bypass showing the page is present.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isPrejoinPageVisible(state: IReduxState): boolean {
return Boolean(navigator.product !== 'ReactNative'
&& isPrejoinEnabledInConfig(state)
&& state['features/prejoin']?.showPrejoin
&& !(state['features/base/config'].enableForcedReload && state['features/prejoin'].skipPrejoinOnReload));
}
/**
* Returns true if we should auto-knock in case lobby is enabled for the room.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function shouldAutoKnock(state: IReduxState): boolean {
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
const { autoKnock } = getLobbyConfig(state);
return Boolean(((isPrejoinEnabledInConfig(state))
|| autoKnock || (iAmRecorder && iAmSipGateway))
&& !state['features/lobby'].knocking);
}
/**
* Returns true if the unsafe room warning flag is enabled.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isUnsafeRoomWarningEnabled(state: IReduxState): boolean {
const { enableInsecureRoomNameWarning = false } = state['features/base/config'];
return getFeatureFlag(state, UNSAFE_ROOM_WARNING, enableInsecureRoomNameWarning);
}
/**
* Returns true if the room name is enabled.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isRoomNameEnabled(state: IReduxState): boolean {
const { hideConferenceSubject = false } = state['features/base/config'];
return getFeatureFlag(state, MEETING_NAME_ENABLED, true)
&& !hideConferenceSubject;
}

View File

@@ -0,0 +1 @@
export * from './functions.any';

View File

@@ -0,0 +1,32 @@
import { batch } from 'react-redux';
import { IStore } from '../app/types';
import { trackAdded } from '../base/tracks/actions.any';
import { PREJOIN_INITIALIZED } from './actionTypes';
import { setPrejoinDeviceErrors } from './actions.web';
export * from './functions.any';
/**
* Adds all the newly created tracks to store on init.
*
* @param {Object[]} tracks - The newly created tracks.
* @param {Object} errors - The errors from creating the tracks.
* @param {Function} dispatch - The redux dispatch function.
* @returns {void}
*/
export function initPrejoin(tracks: Object[], errors: Object, dispatch?: IStore['dispatch']) {
if (!dispatch) {
return;
}
batch(() => {
dispatch(setPrejoinDeviceErrors(errors));
dispatch({
type: PREJOIN_INITIALIZED
});
tracks.forEach(track => dispatch(trackAdded(track)));
});
}

View File

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

View File

@@ -0,0 +1,93 @@
import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { updateSettings } from '../base/settings/actions';
import {
TRACK_ADDED,
TRACK_NO_DATA_FROM_SOURCE
} from '../base/tracks/actionTypes';
import {
setDeviceStatusOk,
setDeviceStatusWarning,
setJoiningInProgress
} from './actions';
import { isPrejoinPageVisible } from './functions.any';
/**
* The redux middleware for {@link PrejoinPage}.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_AUDIO_MUTED: {
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(updateSettings({
startWithAudioMuted: Boolean(action.muted)
}));
}
break;
}
case SET_VIDEO_MUTED: {
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(updateSettings({
startWithVideoMuted: Boolean(action.muted)
}));
}
break;
}
case TRACK_ADDED:
case TRACK_NO_DATA_FROM_SOURCE: {
const state = store.getState();
if (isPrejoinPageVisible(state)) {
const { track: { jitsiTrack: track } } = action;
const { deviceStatusType, deviceStatusText } = state['features/prejoin'];
if (!track.isAudioTrack()) {
break;
}
if (track.isReceivingData()) {
if (deviceStatusType === 'warning'
&& deviceStatusText === 'prejoin.audioDeviceProblem') {
store.dispatch(setDeviceStatusOk('prejoin.lookGood'));
}
} else if (deviceStatusType === 'ok') {
store.dispatch(setDeviceStatusWarning('prejoin.audioDeviceProblem'));
}
}
break;
}
case CONFERENCE_FAILED:
case CONNECTION_FAILED:
store.dispatch(setJoiningInProgress(false));
break;
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
}
return next(action);
});
/**
* Handles cleanup of prejoin state when a conference is joined.
*
* @param {Object} store - The Redux store.
* @param {Function} next - The Redux next function.
* @param {Object} action - The Redux action.
* @returns {Object}
*/
function _conferenceJoined({ dispatch }: IStore, next: Function, action: AnyAction) {
dispatch(setJoiningInProgress(false));
return next(action);
}

View File

@@ -0,0 +1,179 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
PREJOIN_JOINING_IN_PROGRESS,
SET_DEVICE_STATUS,
SET_DIALOUT_COUNTRY,
SET_DIALOUT_NUMBER,
SET_DIALOUT_STATUS,
SET_JOIN_BY_PHONE_DIALOG_VISIBLITY,
SET_PREJOIN_DEVICE_ERRORS,
SET_PREJOIN_PAGE_VISIBILITY,
SET_SKIP_PREJOIN_RELOAD
} from './actionTypes';
const DEFAULT_STATE = {
country: '',
deviceStatusText: 'prejoin.configuringDevices',
deviceStatusType: 'pending',
dialOutCountry: {
name: 'United States',
dialCode: '1',
code: 'us'
},
dialOutNumber: '',
dialOutStatus: 'prejoin.dialing',
name: '',
rawError: '',
showPrejoin: true,
skipPrejoinOnReload: false,
showJoinByPhoneDialog: false
};
export interface IPrejoinState {
country: string;
deviceStatusText: string;
deviceStatusType: string;
dialOutCountry: {
code: string;
dialCode: string;
name: string;
};
dialOutNumber: string;
dialOutStatus: string;
joiningInProgress?: boolean;
name: string;
rawError: string;
showJoinByPhoneDialog: boolean;
showPrejoin: boolean;
skipPrejoinOnReload: boolean;
}
/**
* Sets up the persistence of the feature {@code prejoin}.
*/
PersistenceRegistry.register('features/prejoin', {
skipPrejoinOnReload: true
}, DEFAULT_STATE);
/**
* Listen for actions that mutate the prejoin state.
*/
ReducerRegistry.register<IPrejoinState>(
'features/prejoin', (state = DEFAULT_STATE, action): IPrejoinState => {
switch (action.type) {
case PREJOIN_JOINING_IN_PROGRESS:
return {
...state,
joiningInProgress: action.value
};
case SET_SKIP_PREJOIN_RELOAD: {
return {
...state,
skipPrejoinOnReload: action.value
};
}
case SET_PREJOIN_PAGE_VISIBILITY:
return {
...state,
showPrejoin: action.value
};
case SET_PREJOIN_DEVICE_ERRORS: {
const status = getStatusFromErrors(action.value);
return {
...state,
...status
};
}
case SET_DEVICE_STATUS: {
const { deviceStatusType, deviceStatusText } = action.value;
return {
...state,
deviceStatusText,
deviceStatusType
};
}
case SET_DIALOUT_NUMBER: {
return {
...state,
dialOutNumber: action.value
};
}
case SET_DIALOUT_COUNTRY: {
return {
...state,
dialOutCountry: action.value
};
}
case SET_DIALOUT_STATUS: {
return {
...state,
dialOutStatus: action.value
};
}
case SET_JOIN_BY_PHONE_DIALOG_VISIBLITY: {
return {
...state,
showJoinByPhoneDialog: action.value
};
}
default:
return state;
}
}
);
/**
* Returns a suitable error object based on the track errors.
*
* @param {Object} errors - The errors got while creating local tracks.
* @returns {Object}
*/
function getStatusFromErrors(errors: {
audioAndVideoError?: { message: string; };
audioOnlyError?: { message: string; };
videoOnlyError?: { message: string; }; }
) {
const { audioOnlyError, videoOnlyError, audioAndVideoError } = errors;
if (audioAndVideoError) {
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.audioAndVideoError',
rawError: audioAndVideoError.message
};
}
if (audioOnlyError) {
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.audioOnlyError',
rawError: audioOnlyError.message
};
}
if (videoOnlyError) {
return {
deviceStatusType: 'warning',
deviceStatusText: 'prejoin.videoOnlyError',
rawError: videoOnlyError.message
};
}
return {
deviceStatusType: 'ok',
deviceStatusText: 'prejoin.lookGood',
rawError: ''
};
}

View File

@@ -0,0 +1,3 @@
export interface IPrejoinProps {
navigation: any;
}

View File

@@ -0,0 +1,805 @@
export const countries = [
{ name: 'Afghanistan',
dialCode: '93',
code: 'af' },
{ name: 'Aland Islands',
dialCode: '358',
code: 'ax' },
{ name: 'Albania',
dialCode: '355',
code: 'al' },
{ name: 'Algeria',
dialCode: '213',
code: 'dz' },
{ name: 'AmericanSamoa',
dialCode: '1684',
code: 'as' },
{ name: 'Andorra',
dialCode: '376',
code: 'ad' },
{ name: 'Angola',
dialCode: '244',
code: 'ao' },
{ name: 'Anguilla',
dialCode: '1264',
code: 'ai' },
{ name: 'Antarctica',
dialCode: '672',
code: 'aq' },
{ name: 'Antigua and Barbuda',
dialCode: '1268',
code: 'ag' },
{ name: 'Argentina',
dialCode: '54',
code: 'ar' },
{ name: 'Armenia',
dialCode: '374',
code: 'am' },
{ name: 'Aruba',
dialCode: '297',
code: 'aw' },
{ name: 'Australia',
dialCode: '61',
code: 'au' },
{ name: 'Austria',
dialCode: '43',
code: 'at' },
{ name: 'Azerbaijan',
dialCode: '994',
code: 'az' },
{ name: 'Bahamas',
dialCode: '1242',
code: 'bs' },
{ name: 'Bahrain',
dialCode: '973',
code: 'bh' },
{ name: 'Bangladesh',
dialCode: '880',
code: 'bd' },
{ name: 'Barbados',
dialCode: '1246',
code: 'bb' },
{ name: 'Belarus',
dialCode: '375',
code: 'by' },
{ name: 'Belgium',
dialCode: '32',
code: 'be' },
{ name: 'Belize',
dialCode: '501',
code: 'bz' },
{ name: 'Benin',
dialCode: '229',
code: 'bj' },
{ name: 'Bermuda',
dialCode: '1441',
code: 'bm' },
{ name: 'Bhutan',
dialCode: '975',
code: 'bt' },
{ name: 'Bolivia, Plurinational State of',
dialCode: '591',
code: 'bo' },
{ name: 'Bosnia and Herzegovina',
dialCode: '387',
code: 'ba' },
{ name: 'Botswana',
dialCode: '267',
code: 'bw' },
{ name: 'Brazil',
dialCode: '55',
code: 'br' },
{ name: 'British Indian Ocean Territory',
dialCode: '246',
code: 'io' },
{ name: 'Brunei Darussalam',
dialCode: '673',
code: 'bn' },
{ name: 'Bulgaria',
dialCode: '359',
code: 'bg' },
{ name: 'Burkina Faso',
dialCode: '226',
code: 'bf' },
{ name: 'Burundi',
dialCode: '257',
code: 'bi' },
{ name: 'Cambodia',
dialCode: '855',
code: 'kh' },
{ name: 'Cameroon',
dialCode: '237',
code: 'cm' },
{ name: 'Canada',
dialCode: '1',
code: 'ca' },
{ name: 'Cape Verde',
dialCode: '238',
code: 'cv' },
{ name: 'Cayman Islands',
dialCode: ' 345',
code: 'ky' },
{ name: 'Central African Republic',
dialCode: '236',
code: 'cf' },
{ name: 'Chad',
dialCode: '235',
code: 'td' },
{ name: 'Chile',
dialCode: '56',
code: 'cl' },
{ name: 'China',
dialCode: '86',
code: 'cn' },
{ name: 'Christmas Island',
dialCode: '61',
code: 'cx' },
{ name: 'Cocos (Keeling) Islands',
dialCode: '61',
code: 'cc' },
{ name: 'Colombia',
dialCode: '57',
code: 'co' },
{ name: 'Comoros',
dialCode: '269',
code: 'km' },
{ name: 'Congo',
dialCode: '242',
code: 'cg' },
{
name: 'Congo, The Democratic Republic of the Congo',
dialCode: '243',
code: 'cd'
},
{ name: 'Cook Islands',
dialCode: '682',
code: 'ck' },
{ name: 'Costa Rica',
dialCode: '506',
code: 'cr' },
{ name: 'Cote d\'Ivoire',
dialCode: '225',
code: 'ci' },
{ name: 'Croatia',
dialCode: '385',
code: 'hr' },
{ name: 'Cuba',
dialCode: '53',
code: 'cu' },
{ name: 'Cyprus',
dialCode: '357',
code: 'cy' },
{ name: 'Czech Republic',
dialCode: '420',
code: 'cz' },
{ name: 'Denmark',
dialCode: '45',
code: 'dk' },
{ name: 'Djibouti',
dialCode: '253',
code: 'dj' },
{ name: 'Dominica',
dialCode: '1767',
code: 'dm' },
{ name: 'Dominican Republic',
dialCode: '1849',
code: 'do' },
{ name: 'Ecuador',
dialCode: '593',
code: 'ec' },
{ name: 'Egypt',
dialCode: '20',
code: 'eg' },
{ name: 'El Salvador',
dialCode: '503',
code: 'sv' },
{ name: 'Equatorial Guinea',
dialCode: '240',
code: 'gq' },
{ name: 'Eritrea',
dialCode: '291',
code: 'er' },
{ name: 'Estonia',
dialCode: '372',
code: 'ee' },
{ name: 'Ethiopia',
dialCode: '251',
code: 'et' },
{ name: 'Falkland Islands (Malvinas)',
dialCode: '500',
code: 'fk' },
{ name: 'Faroe Islands',
dialCode: '298',
code: 'fo' },
{ name: 'Fiji',
dialCode: '679',
code: 'fj' },
{ name: 'Finland',
dialCode: '358',
code: 'fi' },
{ name: 'France',
dialCode: '33',
code: 'fr' },
{ name: 'French Guiana',
dialCode: '594',
code: 'gf' },
{ name: 'French Polynesia',
dialCode: '689',
code: 'pf' },
{ name: 'Gabon',
dialCode: '241',
code: 'ga' },
{ name: 'Gambia',
dialCode: '220',
code: 'gm' },
{ name: 'Georgia',
dialCode: '995',
code: 'ge' },
{ name: 'Germany',
dialCode: '49',
code: 'de' },
{ name: 'Ghana',
dialCode: '233',
code: 'gh' },
{ name: 'Gibraltar',
dialCode: '350',
code: 'gi' },
{ name: 'Greece',
dialCode: '30',
code: 'gr' },
{ name: 'Greenland',
dialCode: '299',
code: 'gl' },
{ name: 'Grenada',
dialCode: '1473',
code: 'gd' },
{ name: 'Guadeloupe',
dialCode: '590',
code: 'gp' },
{ name: 'Guam',
dialCode: '1671',
code: 'gu' },
{ name: 'Guatemala',
dialCode: '502',
code: 'gt' },
{ name: 'Guernsey',
dialCode: '44',
code: 'gg' },
{ name: 'Guinea',
dialCode: '224',
code: 'gn' },
{ name: 'Guinea-Bissau',
dialCode: '245',
code: 'gw' },
{ name: 'Guyana',
dialCode: '595',
code: 'gy' },
{ name: 'Haiti',
dialCode: '509',
code: 'ht' },
{ name: 'Holy See (Vatican City State)',
dialCode: '379',
code: 'va' },
{ name: 'Honduras',
dialCode: '504',
code: 'hn' },
{ name: 'Hong Kong',
dialCode: '852',
code: 'hk' },
{ name: 'Hungary',
dialCode: '36',
code: 'hu' },
{ name: 'Iceland',
dialCode: '354',
code: 'is' },
{ name: 'India',
dialCode: '91',
code: 'in' },
{ name: 'Indonesia',
dialCode: '62',
code: 'id' },
{
name: 'Iran, Islamic Republic of Persian Gulf',
dialCode: '98',
code: 'ir'
},
{ name: 'Iraq',
dialCode: '964',
code: 'iq' },
{ name: 'Ireland',
dialCode: '353',
code: 'ie' },
{ name: 'Isle of Man',
dialCode: '44',
code: 'im' },
{ name: 'Israel',
dialCode: '972',
code: 'il' },
{ name: 'Italy',
dialCode: '39',
code: 'it' },
{ name: 'Jamaica',
dialCode: '1876',
code: 'jm' },
{ name: 'Japan',
dialCode: '81',
code: 'jp' },
{ name: 'Jersey',
dialCode: '44',
code: 'je' },
{ name: 'Jordan',
dialCode: '962',
code: 'jo' },
{ name: 'Kazakhstan',
dialCode: '77',
code: 'kz' },
{ name: 'Kenya',
dialCode: '254',
code: 'ke' },
{ name: 'Kiribati',
dialCode: '686',
code: 'ki' },
{
name: 'Korea, Democratic People\'s Republic of Korea',
dialCode: '850',
code: 'kp'
},
{ name: 'Korea, Republic of South Korea',
dialCode: '82',
code: 'kr' },
{ name: 'Kuwait',
dialCode: '965',
code: 'kw' },
{ name: 'Kyrgyzstan',
dialCode: '996',
code: 'kg' },
{ name: 'Laos',
dialCode: '856',
code: 'la' },
{ name: 'Latvia',
dialCode: '371',
code: 'lv' },
{ name: 'Lebanon',
dialCode: '961',
code: 'lb' },
{ name: 'Lesotho',
dialCode: '266',
code: 'ls' },
{ name: 'Liberia',
dialCode: '231',
code: 'lr' },
{ name: 'Libyan Arab Jamahiriya',
dialCode: '218',
code: 'ly' },
{ name: 'Liechtenstein',
dialCode: '423',
code: 'li' },
{ name: 'Lithuania',
dialCode: '370',
code: 'lt' },
{ name: 'Luxembourg',
dialCode: '352',
code: 'lu' },
{ name: 'Macao',
dialCode: '853',
code: 'mo' },
{ name: 'Macedonia',
dialCode: '389',
code: 'mk' },
{ name: 'Madagascar',
dialCode: '261',
code: 'mg' },
{ name: 'Malawi',
dialCode: '265',
code: 'mw' },
{ name: 'Malaysia',
dialCode: '60',
code: 'my' },
{ name: 'Maldives',
dialCode: '960',
code: 'mv' },
{ name: 'Mali',
dialCode: '223',
code: 'ml' },
{ name: 'Malta',
dialCode: '356',
code: 'mt' },
{ name: 'Marshall Islands',
dialCode: '692',
code: 'mh' },
{ name: 'Martinique',
dialCode: '596',
code: 'mq' },
{ name: 'Mauritania',
dialCode: '222',
code: 'mr' },
{ name: 'Mauritius',
dialCode: '230',
code: 'mu' },
{ name: 'Mayotte',
dialCode: '262',
code: 'yt' },
{ name: 'Mexico',
dialCode: '52',
code: 'mx' },
{
name: 'Micronesia, Federated States of Micronesia',
dialCode: '691',
code: 'fm'
},
{ name: 'Moldova',
dialCode: '373',
code: 'md' },
{ name: 'Monaco',
dialCode: '377',
code: 'mc' },
{ name: 'Mongolia',
dialCode: '976',
code: 'mn' },
{ name: 'Montenegro',
dialCode: '382',
code: 'me' },
{ name: 'Montserrat',
dialCode: '1664',
code: 'ms' },
{ name: 'Morocco',
dialCode: '212',
code: 'ma' },
{ name: 'Mozambique',
dialCode: '258',
code: 'mz' },
{ name: 'Myanmar',
dialCode: '95',
code: 'mm' },
{ name: 'Namibia',
dialCode: '264',
code: 'na' },
{ name: 'Nauru',
dialCode: '674',
code: 'nr' },
{ name: 'Nepal',
dialCode: '977',
code: 'np' },
{ name: 'Netherlands',
dialCode: '31',
code: 'nl' },
{ name: 'Netherlands Antilles',
dialCode: '599',
code: 'an' },
{ name: 'New Caledonia',
dialCode: '687',
code: 'nc' },
{ name: 'New Zealand',
dialCode: '64',
code: 'nz' },
{ name: 'Nicaragua',
dialCode: '505',
code: 'ni' },
{ name: 'Niger',
dialCode: '227',
code: 'ne' },
{ name: 'Nigeria',
dialCode: '234',
code: 'ng' },
{ name: 'Niue',
dialCode: '683',
code: 'nu' },
{ name: 'Norfolk Island',
dialCode: '672',
code: 'nf' },
{ name: 'Northern Mariana Islands',
dialCode: '1670',
code: 'mp' },
{ name: 'Norway',
dialCode: '47',
code: 'no' },
{ name: 'Oman',
dialCode: '968',
code: 'om' },
{ name: 'Pakistan',
dialCode: '92',
code: 'pk' },
{ name: 'Palau',
dialCode: '680',
code: 'pw' },
{ name: 'Palestinian Territory, Occupied',
dialCode: '970',
code: 'ps' },
{ name: 'Panama',
dialCode: '507',
code: 'pa' },
{ name: 'Papua New Guinea',
dialCode: '675',
code: 'pg' },
{ name: 'Paraguay',
dialCode: '595',
code: 'py' },
{ name: 'Peru',
dialCode: '51',
code: 'pe' },
{ name: 'Philippines',
dialCode: '63',
code: 'ph' },
{ name: 'Pitcairn',
dialCode: '872',
code: 'pn' },
{ name: 'Poland',
dialCode: '48',
code: 'pl' },
{ name: 'Portugal',
dialCode: '351',
code: 'pt' },
{ name: 'Puerto Rico',
dialCode: '1939',
code: 'pr' },
{ name: 'Qatar',
dialCode: '974',
code: 'qa' },
{ name: 'Romania',
dialCode: '40',
code: 'ro' },
{ name: 'Russia',
dialCode: '7',
code: 'ru' },
{ name: 'Rwanda',
dialCode: '250',
code: 'rw' },
{ name: 'Reunion',
dialCode: '262',
code: 're' },
{ name: 'Saint Barthelemy',
dialCode: '590',
code: 'bl' },
{
name: 'Saint Helena, Ascension and Tristan Da Cunha',
dialCode: '290',
code: 'sh'
},
{ name: 'Saint Kitts and Nevis',
dialCode: '1869',
code: 'kn' },
{ name: 'Saint Lucia',
dialCode: '1758',
code: 'lc' },
{ name: 'Saint Martin',
dialCode: '590',
code: 'mf' },
{ name: 'Saint Pierre and Miquelon',
dialCode: '508',
code: 'pm' },
{ name: 'Saint Vincent and the Grenadines',
dialCode: '1784',
code: 'vc' },
{ name: 'Samoa',
dialCode: '685',
code: 'ws' },
{ name: 'San Marino',
dialCode: '378',
code: 'sm' },
{ name: 'Sao Tome and Principe',
dialCode: '239',
code: 'st' },
{ name: 'Saudi Arabia',
dialCode: '966',
code: 'sa' },
{ name: 'Senegal',
dialCode: '221',
code: 'sn' },
{ name: 'Serbia',
dialCode: '381',
code: 'rs' },
{ name: 'Seychelles',
dialCode: '248',
code: 'sc' },
{ name: 'Sierra Leone',
dialCode: '232',
code: 'sl' },
{ name: 'Singapore',
dialCode: '65',
code: 'sg' },
{ name: 'Slovakia',
dialCode: '421',
code: 'sk' },
{ name: 'Slovenia',
dialCode: '386',
code: 'si' },
{ name: 'Solomon Islands',
dialCode: '677',
code: 'sb' },
{ name: 'Somalia',
dialCode: '252',
code: 'so' },
{ name: 'South Africa',
dialCode: '27',
code: 'za' },
{ name: 'South Sudan',
dialCode: '211',
code: 'ss' },
{
name: 'South Georgia and the South Sandwich Islands',
dialCode: '500',
code: 'gs'
},
{ name: 'Spain',
dialCode: '34',
code: 'es' },
{ name: 'Sri Lanka',
dialCode: '94',
code: 'lk' },
{ name: 'Sudan',
dialCode: '249',
code: 'sd' },
{ name: 'Suriname',
dialCode: '597',
code: 'sr' },
{ name: 'Svalbard and Jan Mayen',
dialCode: '47',
code: 'sj' },
{ name: 'Swaziland',
dialCode: '268',
code: 'sz' },
{ name: 'Sweden',
dialCode: '46',
code: 'se' },
{ name: 'Switzerland',
dialCode: '41',
code: 'ch' },
{ name: 'Syrian Arab Republic',
dialCode: '963',
code: 'sy' },
{ name: 'Taiwan',
dialCode: '886',
code: 'tw' },
{ name: 'Tajikistan',
dialCode: '992',
code: 'tj' },
{
name: 'Tanzania, United Republic of Tanzania',
dialCode: '255',
code: 'tz'
},
{ name: 'Thailand',
dialCode: '66',
code: 'th' },
{ name: 'Timor-Leste',
dialCode: '670',
code: 'tl' },
{ name: 'Togo',
dialCode: '228',
code: 'tg' },
{ name: 'Tokelau',
dialCode: '690',
code: 'tk' },
{ name: 'Tonga',
dialCode: '676',
code: 'to' },
{ name: 'Trinidad and Tobago',
dialCode: '1868',
code: 'tt' },
{ name: 'Tunisia',
dialCode: '216',
code: 'tn' },
{ name: 'Turkey',
dialCode: '90',
code: 'tr' },
{ name: 'Turkmenistan',
dialCode: '993',
code: 'tm' },
{ name: 'Turks and Caicos Islands',
dialCode: '1649',
code: 'tc' },
{ name: 'Tuvalu',
dialCode: '688',
code: 'tv' },
{ name: 'Uganda',
dialCode: '256',
code: 'ug' },
{ name: 'Ukraine',
dialCode: '380',
code: 'ua' },
{ name: 'United Arab Emirates',
dialCode: '971',
code: 'ae' },
{ name: 'United Kingdom',
dialCode: '44',
code: 'gb' },
{ name: 'United States',
dialCode: '1',
code: 'us' },
{ name: 'Uruguay',
dialCode: '598',
code: 'uy' },
{ name: 'Uzbekistan',
dialCode: '998',
code: 'uz' },
{ name: 'Vanuatu',
dialCode: '678',
code: 'vu' },
{
name: 'Venezuela, Bolivarian Republic of Venezuela',
dialCode: '58',
code: 've'
},
{ name: 'Vietnam',
dialCode: '84',
code: 'vn' },
{ name: 'Virgin Islands, British',
dialCode: '1284',
code: 'vg' },
{ name: 'Virgin Islands, U.S.',
dialCode: '1340',
code: 'vi' },
{ name: 'Wallis and Futuna',
dialCode: '681',
code: 'wf' },
{ name: 'Yemen',
dialCode: '967',
code: 'ye' },
{ name: 'Zambia',
dialCode: '260',
code: 'zm' },
{ name: 'Zimbabwe',
dialCode: '263',
code: 'zw' }
];
const countriesByCodeMap = countries.reduce<any>((result, country) => {
result[country.dialCode] = country;
return result;
}, {});
/**
* Map between country dial codes and country objects.
*
*/
const codesByNumbersMap = countries.reduce<any>((result, country) => {
result[country.dialCode] = country.code;
return result;
}, {});
/**
* Returns the corresponding country code from a phone number.
*
* @param {string} phoneNumber - The phone number.
* @returns {string}
*/
export function getCountryCodeFromPhone(phoneNumber: string): string {
const number = phoneNumber.replace(/[+.\s]/g, '');
for (let i = 4; i > 0; i--) {
const prefix = number.slice(0, i);
if (codesByNumbersMap[prefix]) {
return codesByNumbersMap[prefix];
}
}
return '';
}
/**
* Returns the corresponding country for a text starting with the dial code.
*
* @param {string} text - The text containing the dial code.
* @returns {Object}
*/
export function getCountryFromDialCodeText(text: string) {
return (
countriesByCodeMap[text.slice(0, 4)]
|| countriesByCodeMap[text.slice(0, 3)]
|| countriesByCodeMap[text.slice(0, 2)]
|| countriesByCodeMap[text.slice(0, 1)]
|| null
);
}
/**
* Returns whether the display name is present.
*
* @param {string} value - The display name.
* @returns {boolean}
*/
export const hasDisplayName = (value: string): boolean => Boolean(value) && value.trim() !== '';