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