This commit is contained in:
8
react/features/conference/actionTypes.ts
Normal file
8
react/features/conference/actionTypes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* The type of (redux) action which dismisses calendar notification.
|
||||
*
|
||||
* {
|
||||
* type: DISMISS_CALENDAR_NOTIFICATION
|
||||
* }
|
||||
*/
|
||||
export const DISMISS_CALENDAR_NOTIFICATION = 'DISMISS_CALENDAR_NOTIFICATION';
|
||||
78
react/features/conference/actions.native.ts
Normal file
78
react/features/conference/actions.native.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
import AlertDialog from '../base/dialog/components/native/AlertDialog';
|
||||
import { getParticipantDisplayName } from '../base/participants/functions';
|
||||
|
||||
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
|
||||
|
||||
/**
|
||||
* Notify that we've been kicked out of the conference.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
|
||||
* instance which initiated the kick event.
|
||||
* @param {?Function} submit - The function to execute after submitting the dialog.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function notifyKickedOut(participant: any, submit?: Function) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (participant?.isReplaced?.()) {
|
||||
submit?.();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(openDialog(AlertDialog, {
|
||||
contentKey: {
|
||||
key: participant ? 'dialog.kickTitle' : 'dialog.kickSystemTitle',
|
||||
params: {
|
||||
participantDisplayName: participant && getParticipantDisplayName(getState, participant.getId())
|
||||
}
|
||||
},
|
||||
onSubmit: submit
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that we've been kicked out of the conference.
|
||||
*
|
||||
* @param {string} reasonKey - The translation key for the reason why the conference failed.
|
||||
* @param {?Function} submit - The function to execute after submitting the dialog.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
if (!reasonKey) {
|
||||
submit?.();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// we have to push the opening of the dialog to the queue
|
||||
// so that we make sure it will be visible after the events
|
||||
// of conference destroyed are done
|
||||
setTimeout(() => dispatch(openDialog(AlertDialog, {
|
||||
contentKey: {
|
||||
key: reasonKey
|
||||
},
|
||||
params: {
|
||||
},
|
||||
onSubmit: () => {
|
||||
submit?.();
|
||||
dispatch(hideDialog(AlertDialog));
|
||||
}
|
||||
})));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses calendar notification about next or ongoing event.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function dismissCalendarNotification() {
|
||||
return {
|
||||
type: DISMISS_CALENDAR_NOTIFICATION
|
||||
};
|
||||
}
|
||||
91
react/features/conference/actions.web.ts
Normal file
91
react/features/conference/actions.web.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { configureInitialDevices, getAvailableDevices } from '../base/devices/actions.web';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { getJitsiMeetGlobalNSConnectionTimes } from '../base/util/helpers';
|
||||
import { getBackendSafeRoomName } from '../base/util/uri';
|
||||
|
||||
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
import LeaveReasonDialog from './components/web/LeaveReasonDialog.web';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Opens {@code LeaveReasonDialog}.
|
||||
*
|
||||
* @param {string} [title] - The dialog title.
|
||||
*
|
||||
* @returns {Promise} Resolved when the dialog is closed.
|
||||
*/
|
||||
export function openLeaveReasonDialog(title?: string) {
|
||||
return (dispatch: IStore['dispatch']): Promise<void> => new Promise(resolve => {
|
||||
dispatch(openDialog(LeaveReasonDialog, {
|
||||
onClose: resolve,
|
||||
title
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses calendar notification about next or ongoing event.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function dismissCalendarNotification() {
|
||||
return {
|
||||
type: DISMISS_CALENDAR_NOTIFICATION
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups initial devices. Makes sure we populate availableDevices list before configuring.
|
||||
*
|
||||
* @param {boolean} recordTimeMetrics - If true, an analytics time metrics will be sent.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export function setupInitialDevices(recordTimeMetrics = false) {
|
||||
return async (dispatch: IStore['dispatch']) => {
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['setupInitialDevices.start'] = window.performance.now();
|
||||
}
|
||||
|
||||
await dispatch(getAvailableDevices());
|
||||
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['setupInitialDevices.getAD.finished'] = window.performance.now();
|
||||
}
|
||||
|
||||
await dispatch(configureInitialDevices());
|
||||
|
||||
const now = window.performance.now();
|
||||
|
||||
if (recordTimeMetrics) {
|
||||
getJitsiMeetGlobalNSConnectionTimes()['setupInitialDevices.end'] = now;
|
||||
}
|
||||
logger.debug(`(TIME) setupInitialDevices finished: ${now}`);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Init.
|
||||
*
|
||||
* @param {boolean} shouldDispatchConnect - Whether or not connect should be dispatched. This should be false only when
|
||||
* prejoin is enabled.
|
||||
* @returns {Promise<JitsiConnection>}
|
||||
*/
|
||||
export function init(shouldDispatchConnect: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
logger.debug(`(TIME) init action dispatched: ${window.performance.now()}`);
|
||||
|
||||
const room = getBackendSafeRoomName(getState()['features/base/conference'].room);
|
||||
|
||||
// XXX For web based version we use conference initialization logic
|
||||
// from the old app (at the moment of writing).
|
||||
return dispatch(setupInitialDevices(true)).then(
|
||||
() => APP.conference.init({
|
||||
roomName: room,
|
||||
shouldDispatchConnect
|
||||
}).catch((error: Error) => {
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
logger.error(error);
|
||||
}));
|
||||
};
|
||||
}
|
||||
80
react/features/conference/components/AbstractConference.ts
Normal file
80
react/features/conference/components/AbstractConference.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { NotificationsContainer } from '../../notifications/components';
|
||||
import { shouldDisplayTileView } from '../../video-layout/functions.any';
|
||||
import { shouldDisplayNotifications } from '../functions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractLabels}.
|
||||
*/
|
||||
export type AbstractProps = {
|
||||
|
||||
/**
|
||||
* Set to {@code true} when the notifications are to be displayed.
|
||||
*
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
_notificationsVisible: boolean;
|
||||
|
||||
/**
|
||||
* Conference room name.
|
||||
*
|
||||
* @protected
|
||||
* @type {string}
|
||||
*/
|
||||
_room: string;
|
||||
|
||||
/**
|
||||
* Whether or not the layout should change to support tile view mode.
|
||||
*
|
||||
* @protected
|
||||
* @type {boolean}
|
||||
*/
|
||||
_shouldDisplayTileView: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A container to hold video status labels, including recording status and
|
||||
* current large video quality.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
export class AbstractConference<P extends AbstractProps, S>
|
||||
extends Component<P, S> {
|
||||
|
||||
/**
|
||||
* Renders the {@code LocalRecordingLabel}.
|
||||
*
|
||||
* @param {Object} props - The properties to be passed to
|
||||
* the {@code NotificationsContainer}.
|
||||
* @protected
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
renderNotificationsContainer(props?: any) {
|
||||
if (this.props._notificationsVisible) {
|
||||
return (
|
||||
React.createElement(NotificationsContainer, props)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props of the {@link Labels}
|
||||
* {@code Component}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {AbstractProps}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_notificationsVisible: shouldDisplayNotifications(state),
|
||||
_room: state['features/base/conference'].room ?? '',
|
||||
_shouldDisplayTileView: shouldDisplayTileView(state)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import isInsecureRoomName from '../../base/util/isInsecureRoomName';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../prejoin/functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* True of the label should be visible.
|
||||
*/
|
||||
_visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for the {@Code InsecureRoomNameLabel} component.
|
||||
*/
|
||||
export default class AbstractInsecureRoomNameLabel extends PureComponent<IProps> {
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
if (!this.props._visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform dependent content.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_render() {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
const { locked, room } = state['features/base/conference'];
|
||||
const { lobbyEnabled } = state['features/lobby'];
|
||||
|
||||
return {
|
||||
_visible: Boolean(isUnsafeRoomWarningEnabled(state)
|
||||
&& room && isInsecureRoomName(room)
|
||||
&& !(lobbyEnabled || Boolean(locked)))
|
||||
};
|
||||
}
|
||||
107
react/features/conference/components/ConferenceTimer.tsx
Normal file
107
react/features/conference/components/ConferenceTimer.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConferenceTimestamp } from '../../base/conference/functions';
|
||||
import { getLocalizedDurationFormatter } from '../../base/i18n/dateUtil';
|
||||
|
||||
import { ConferenceTimerDisplay } from './index';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ConferenceTimer}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Style to be applied to the rendered text.
|
||||
*/
|
||||
textStyle?: Object;
|
||||
}
|
||||
|
||||
export interface IDisplayProps {
|
||||
|
||||
/**
|
||||
* Style to be applied to text (native only).
|
||||
*/
|
||||
textStyle?: Object;
|
||||
|
||||
/**
|
||||
* String to display as time.
|
||||
*/
|
||||
timerValue: string;
|
||||
}
|
||||
|
||||
const ConferenceTimer = ({ textStyle }: IProps) => {
|
||||
const startTimestamp = useSelector(getConferenceTimestamp);
|
||||
const [ timerValue, setTimerValue ] = useState(getLocalizedDurationFormatter(0));
|
||||
const interval = useRef<number>();
|
||||
|
||||
/**
|
||||
* Sets the current state values that will be used to render the timer.
|
||||
*
|
||||
* @param {number} refValueUTC - The initial UTC timestamp value.
|
||||
* @param {number} currentValueUTC - The current UTC timestamp value.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const setStateFromUTC = useCallback((refValueUTC, currentValueUTC) => {
|
||||
if (!refValueUTC || !currentValueUTC) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentValueUTC < refValueUTC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timerMsValue = currentValueUTC - refValueUTC;
|
||||
|
||||
const localizedTime = getLocalizedDurationFormatter(timerMsValue);
|
||||
|
||||
setTimerValue(localizedTime);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Start conference timer.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const startTimer = useCallback(() => {
|
||||
if (!interval.current && startTimestamp) {
|
||||
setStateFromUTC(startTimestamp, new Date().getTime());
|
||||
|
||||
interval.current = window.setInterval(() => {
|
||||
setStateFromUTC(startTimestamp, new Date().getTime());
|
||||
}, 1000);
|
||||
}
|
||||
}, [ startTimestamp, interval ]);
|
||||
|
||||
/**
|
||||
* Stop conference timer.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const stopTimer = useCallback(() => {
|
||||
if (interval.current) {
|
||||
clearInterval(interval.current);
|
||||
interval.current = undefined;
|
||||
}
|
||||
|
||||
setTimerValue(getLocalizedDurationFormatter(0));
|
||||
}, [ interval ]);
|
||||
|
||||
useEffect(() => {
|
||||
startTimer();
|
||||
|
||||
return () => stopTimer();
|
||||
}, [ startTimestamp ]);
|
||||
|
||||
|
||||
if (!startTimestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (<ConferenceTimerDisplay
|
||||
textStyle = { textStyle }
|
||||
timerValue = { timerValue } />);
|
||||
};
|
||||
|
||||
export default ConferenceTimer;
|
||||
15
react/features/conference/components/constants.ts
Normal file
15
react/features/conference/components/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const CONFERENCE_INFO = {
|
||||
alwaysVisible: [ 'raised-hands-count', 'recording' ],
|
||||
autoHide: [
|
||||
'highlight-moment',
|
||||
'subject',
|
||||
'conference-timer',
|
||||
'participants-count',
|
||||
'e2ee',
|
||||
'transcribing',
|
||||
'video-quality',
|
||||
'visitors-count',
|
||||
'insecure-room',
|
||||
'top-panel-toggle'
|
||||
]
|
||||
};
|
||||
22
react/features/conference/components/functions.any.ts
Normal file
22
react/features/conference/components/functions.any.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
import { CONFERENCE_INFO } from './constants';
|
||||
|
||||
/**
|
||||
* Retrieves the conference info labels based on config values and defaults.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object} The conferenceInfo object.
|
||||
*/
|
||||
export const getConferenceInfo = (state: IReduxState) => {
|
||||
const { conferenceInfo } = state['features/base/config'];
|
||||
|
||||
if (conferenceInfo) {
|
||||
return {
|
||||
alwaysVisible: conferenceInfo.alwaysVisible ?? CONFERENCE_INFO.alwaysVisible,
|
||||
autoHide: conferenceInfo.autoHide ?? CONFERENCE_INFO.autoHide
|
||||
};
|
||||
}
|
||||
|
||||
return CONFERENCE_INFO;
|
||||
};
|
||||
32
react/features/conference/components/functions.native.ts
Normal file
32
react/features/conference/components/functions.native.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Returns whether the conference is in connecting state.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean} Whether conference is connecting.
|
||||
*/
|
||||
export const isConnecting = (state: IReduxState) => {
|
||||
const { connecting, connection } = state['features/base/connection'];
|
||||
const {
|
||||
conference,
|
||||
joining,
|
||||
membersOnly,
|
||||
leaving
|
||||
} = state['features/base/conference'];
|
||||
|
||||
// XXX There is a window of time between the successful establishment of the
|
||||
// XMPP connection and the subsequent commencement of joining the MUC during
|
||||
// which the app does not appear to be doing anything according to the redux
|
||||
// state. In order to not toggle the _connecting props during the window of
|
||||
// time in question, define _connecting as follows:
|
||||
// - the XMPP connection is connecting, or
|
||||
// - the XMPP connection is connected and the conference is joining, or
|
||||
// - the XMPP connection is connected and we have no conference yet, nor we
|
||||
// are leaving one.
|
||||
return Boolean(
|
||||
connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))))
|
||||
);
|
||||
};
|
||||
12
react/features/conference/components/functions.web.ts
Normal file
12
react/features/conference/components/functions.web.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Whether or not there are always on labels.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAlwaysOnTitleBarEmpty() {
|
||||
const bar = document.querySelector('#alwaysVisible>div');
|
||||
|
||||
return bar?.childNodes.length === 0;
|
||||
}
|
||||
1
react/features/conference/components/index.native.ts
Normal file
1
react/features/conference/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ConferenceTimerDisplay } from './native/ConferenceTimerDisplay';
|
||||
1
react/features/conference/components/index.web.ts
Normal file
1
react/features/conference/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as ConferenceTimerDisplay } from './web/ConferenceTimerDisplay';
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { openHighlightDialog } from '../../../recording/actions.native';
|
||||
import HighlightButton from '../../../recording/components/Recording/native/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/native/RecordingLabel';
|
||||
import { isLiveStreamingRunning } from '../../../recording/functions';
|
||||
import VisitorsCountLabel from '../../../visitors/components/native/VisitorsCountLabel';
|
||||
|
||||
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
|
||||
import {
|
||||
LABEL_ID_RAISED_HANDS_COUNT,
|
||||
LABEL_ID_RECORDING,
|
||||
LABEL_ID_STREAMING,
|
||||
LABEL_ID_VISITORS_COUNT,
|
||||
LabelHitSlop
|
||||
} from './constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Creates a function to be invoked when the onPress of the touchables are
|
||||
* triggered.
|
||||
*/
|
||||
createOnPress: Function;
|
||||
}
|
||||
|
||||
const AlwaysOnLabels = ({ createOnPress }: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const isStreaming = useSelector(isLiveStreamingRunning);
|
||||
const openHighlightDialogCallback = useCallback(() =>
|
||||
dispatch(openHighlightDialog()), [ dispatch ]);
|
||||
|
||||
return (<>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_RECORDING) } >
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
</TouchableOpacity>
|
||||
{
|
||||
isStreaming
|
||||
&& <TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_STREAMING) } >
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</TouchableOpacity>
|
||||
}
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { openHighlightDialogCallback }>
|
||||
<HighlightButton />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_RAISED_HANDS_COUNT) } >
|
||||
<RaisedHandsCountLabel />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_VISITORS_COUNT) } >
|
||||
<VisitorsCountLabel />
|
||||
</TouchableOpacity>
|
||||
</>);
|
||||
};
|
||||
|
||||
export default AlwaysOnLabels;
|
||||
621
react/features/conference/components/native/Conference.tsx
Normal file
621
react/features/conference/components/native/Conference.tsx
Normal file
@@ -0,0 +1,621 @@
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
BackHandler,
|
||||
NativeModules,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../../../app/actions.native';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
|
||||
import { isDisplayNameVisible } from '../../../base/config/functions.native';
|
||||
import { FULLSCREEN_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import Container from '../../../base/react/components/native/Container';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import TintedView from '../../../base/react/components/native/TintedView';
|
||||
import {
|
||||
ASPECT_RATIO_NARROW,
|
||||
ASPECT_RATIO_WIDE
|
||||
} from '../../../base/responsive-ui/constants';
|
||||
import { StyleType } from '../../../base/styles/functions.any';
|
||||
import TestConnectionInfo from '../../../base/testing/components/TestConnectionInfo';
|
||||
import { isCalendarEnabled } from '../../../calendar-sync/functions.native';
|
||||
import DisplayNameLabel from '../../../display-name/components/native/DisplayNameLabel';
|
||||
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
|
||||
import Filmstrip from '../../../filmstrip/components/native/Filmstrip';
|
||||
import TileView from '../../../filmstrip/components/native/TileView';
|
||||
import { FILMSTRIP_SIZE } from '../../../filmstrip/constants';
|
||||
import { isFilmstripVisible } from '../../../filmstrip/functions.native';
|
||||
import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeInfoContainer';
|
||||
import LargeVideo from '../../../large-video/components/LargeVideo.native';
|
||||
import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { isPipEnabled, setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
|
||||
import Captions from '../../../subtitles/components/native/Captions';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions.native';
|
||||
import Toolbox from '../../../toolbox/components/native/Toolbox';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
import {
|
||||
AbstractConference,
|
||||
type AbstractProps,
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractConference';
|
||||
import { isConnecting } from '../functions.native';
|
||||
|
||||
import AlwaysOnLabels from './AlwaysOnLabels';
|
||||
import ExpandedLabelPopup from './ExpandedLabelPopup';
|
||||
import LonelyMeetingExperience from './LonelyMeetingExperience';
|
||||
import TitleBar from './TitleBar';
|
||||
import { EXPANDED_LABEL_TIMEOUT } from './constants';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Conference}.
|
||||
*/
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* Application's aspect ratio.
|
||||
*/
|
||||
_aspectRatio: Symbol;
|
||||
|
||||
/**
|
||||
* Whether the audio only is enabled or not.
|
||||
*/
|
||||
_audioOnlyEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Branding styles for conference.
|
||||
*/
|
||||
_brandingStyles: StyleType;
|
||||
|
||||
/**
|
||||
* Whether the calendar feature is enabled or not.
|
||||
*/
|
||||
_calendarEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines that we are still connecting to the
|
||||
* conference which includes establishing the XMPP connection and then
|
||||
* joining the room. If truthy, then an activity/loading indicator will be
|
||||
* rendered.
|
||||
*/
|
||||
_connecting: boolean;
|
||||
|
||||
/**
|
||||
* Set to {@code true} when the filmstrip is currently visible.
|
||||
*/
|
||||
_filmstripVisible: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether fullscreen (immersive) mode is enabled.
|
||||
*/
|
||||
_fullscreenEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines if the display name is visible.
|
||||
*/
|
||||
_isDisplayNameVisible: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines if the participants pane is open.
|
||||
*/
|
||||
_isParticipantsPaneOpen: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the participant currently on stage (if any).
|
||||
*/
|
||||
_largeVideoParticipantId: string;
|
||||
|
||||
/**
|
||||
* Local participant's display name.
|
||||
*/
|
||||
_localParticipantDisplayName: string;
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is enabled.
|
||||
*/
|
||||
_pictureInPictureEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the UI is reduced (to accommodate
|
||||
* smaller display areas).
|
||||
*/
|
||||
_reducedUI: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the lobby screen should be visible.
|
||||
*/
|
||||
_showLobby: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the car mode is enabled.
|
||||
*/
|
||||
_startCarMode: boolean;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the Toolbox is visible.
|
||||
*/
|
||||
_toolboxVisible: boolean;
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Object containing the safe area insets.
|
||||
*/
|
||||
insets: EdgeInsets;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The label that is currently expanded.
|
||||
*/
|
||||
visibleExpandedLabel?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The conference page of the mobile (i.e. React Native) application.
|
||||
*/
|
||||
class Conference extends AbstractConference<IProps, State> {
|
||||
/**
|
||||
* Timeout ref.
|
||||
*/
|
||||
_expandedLabelTimeout: any;
|
||||
|
||||
/**
|
||||
* Initializes hardwareBackPress subscription.
|
||||
*/
|
||||
_hardwareBackPressSubscription: any;
|
||||
|
||||
/**
|
||||
* Initializes a new Conference instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
visibleExpandedLabel: undefined
|
||||
};
|
||||
|
||||
this._expandedLabelTimeout = React.createRef<number>();
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
|
||||
this._setToolboxVisible = this._setToolboxVisible.bind(this);
|
||||
this._createOnPress = this._createOnPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link Component#componentDidMount()}. Invoked immediately
|
||||
* after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
const {
|
||||
_audioOnlyEnabled,
|
||||
_startCarMode,
|
||||
navigation
|
||||
} = this.props;
|
||||
|
||||
this._hardwareBackPressSubscription = BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
|
||||
if (_audioOnlyEnabled && _startCarMode) {
|
||||
navigation.navigate(screen.conference.carmode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidUpdate(prevProps: IProps) {
|
||||
const {
|
||||
_audioOnlyEnabled,
|
||||
_showLobby,
|
||||
_startCarMode
|
||||
} = this.props;
|
||||
|
||||
if (!prevProps._showLobby && _showLobby) {
|
||||
navigate(screen.lobby.root);
|
||||
}
|
||||
|
||||
if (prevProps._showLobby && !_showLobby) {
|
||||
if (_audioOnlyEnabled && _startCarMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(screen.conference.main);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@link Component#componentWillUnmount()}. Invoked immediately
|
||||
* before this component is unmounted and destroyed. Disconnects the
|
||||
* conference described by the redux store/state.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
// Tear handling any hardware button presses for back navigation down.
|
||||
this._hardwareBackPressSubscription?.remove();
|
||||
|
||||
clearTimeout(this._expandedLabelTimeout.current ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_brandingStyles,
|
||||
_fullscreenEnabled
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Container
|
||||
style = { [
|
||||
styles.conference,
|
||||
_brandingStyles
|
||||
] }>
|
||||
<BrandingImageBackground />
|
||||
{
|
||||
Platform.OS === 'android'
|
||||
&& <StatusBar
|
||||
barStyle = 'light-content'
|
||||
hidden = { _fullscreenEnabled }
|
||||
translucent = { _fullscreenEnabled } />
|
||||
}
|
||||
{ this._renderContent() }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the value of the toolboxVisible state, thus allowing us to switch
|
||||
* between Toolbox and Filmstrip and change their visibility.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this._setToolboxVisible(!this.props._toolboxVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
|
||||
* (if supported) or leaves the associated {@code Conference} otherwise.
|
||||
*
|
||||
* @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
|
||||
*/
|
||||
_onHardwareBackPress() {
|
||||
let p;
|
||||
|
||||
if (this.props._pictureInPictureEnabled) {
|
||||
const { PictureInPicture } = NativeModules;
|
||||
|
||||
p = PictureInPicture.enterPictureInPicture();
|
||||
} else {
|
||||
p = Promise.reject(new Error('PiP not enabled'));
|
||||
}
|
||||
|
||||
p.catch(() => {
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function to be invoked when the onPress of the touchables are
|
||||
* triggered.
|
||||
*
|
||||
* @param {string} label - The identifier of the label that's onLayout is
|
||||
* triggered.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_createOnPress(label: string) {
|
||||
return () => {
|
||||
const { visibleExpandedLabel } = this.state;
|
||||
|
||||
const newVisibleExpandedLabel
|
||||
= visibleExpandedLabel === label ? undefined : label;
|
||||
|
||||
clearTimeout(this._expandedLabelTimeout.current);
|
||||
this.setState({
|
||||
visibleExpandedLabel: newVisibleExpandedLabel
|
||||
});
|
||||
|
||||
if (newVisibleExpandedLabel) {
|
||||
this._expandedLabelTimeout.current = setTimeout(() => {
|
||||
this.setState({
|
||||
visibleExpandedLabel: undefined
|
||||
});
|
||||
}, EXPANDED_LABEL_TIMEOUT);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content for the Conference container.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderContent() {
|
||||
const {
|
||||
_aspectRatio,
|
||||
_connecting,
|
||||
_filmstripVisible,
|
||||
_isDisplayNameVisible,
|
||||
_largeVideoParticipantId,
|
||||
_reducedUI,
|
||||
_shouldDisplayTileView,
|
||||
_toolboxVisible
|
||||
} = this.props;
|
||||
|
||||
let alwaysOnTitleBarStyles;
|
||||
|
||||
if (_reducedUI) {
|
||||
return this._renderContentForReducedUi();
|
||||
}
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
alwaysOnTitleBarStyles
|
||||
= !_shouldDisplayTileView && _filmstripVisible
|
||||
? styles.alwaysOnTitleBarWide
|
||||
: styles.alwaysOnTitleBar;
|
||||
} else {
|
||||
alwaysOnTitleBarStyles = styles.alwaysOnTitleBar;
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
* The LargeVideo is the lowermost stacking layer.
|
||||
*/
|
||||
_shouldDisplayTileView
|
||||
? <TileView onClick = { this._onClick } />
|
||||
: <LargeVideo onClick = { this._onClick } />
|
||||
}
|
||||
|
||||
{/*
|
||||
* If there is a ringing call, show the callee's info.
|
||||
*/
|
||||
<CalleeInfoContainer />
|
||||
}
|
||||
|
||||
{/*
|
||||
* The activity/loading indicator goes above everything, except
|
||||
* the toolbox/toolbars and the dialogs.
|
||||
*/
|
||||
_connecting
|
||||
&& <TintedView>
|
||||
<LoadingIndicator />
|
||||
</TintedView>
|
||||
}
|
||||
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.toolboxAndFilmstripContainer as ViewStyle }>
|
||||
|
||||
<Captions onPress = { this._onClick } />
|
||||
|
||||
{
|
||||
_shouldDisplayTileView
|
||||
|| (_isDisplayNameVisible && (
|
||||
<Container style = { styles.displayNameContainer }>
|
||||
<DisplayNameLabel
|
||||
participantId = { _largeVideoParticipantId } />
|
||||
</Container>
|
||||
))
|
||||
}
|
||||
|
||||
{ !_shouldDisplayTileView && <LonelyMeetingExperience /> }
|
||||
|
||||
{
|
||||
_shouldDisplayTileView
|
||||
|| <>
|
||||
<Filmstrip />
|
||||
{ this._renderNotificationsContainer() }
|
||||
<Toolbox />
|
||||
</>
|
||||
}
|
||||
</View>
|
||||
|
||||
<SafeAreaView
|
||||
pointerEvents = 'box-none'
|
||||
style = {
|
||||
(_toolboxVisible
|
||||
? styles.titleBarSafeViewColor
|
||||
: styles.titleBarSafeViewTransparent) as ViewStyle }>
|
||||
<TitleBar _createOnPress = { this._createOnPress } />
|
||||
</SafeAreaView>
|
||||
<SafeAreaView
|
||||
pointerEvents = 'box-none'
|
||||
style = {
|
||||
(_toolboxVisible
|
||||
? [ styles.titleBarSafeViewTransparent, { top: this.props.insets.top + 50 } ]
|
||||
: styles.titleBarSafeViewTransparent) as ViewStyle
|
||||
}>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.expandedLabelWrapper }>
|
||||
<ExpandedLabelPopup visibleExpandedLabel = { this.state.visibleExpandedLabel } />
|
||||
</View>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { alwaysOnTitleBarStyles as ViewStyle }>
|
||||
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||
<AlwaysOnLabels createOnPress = { this._createOnPress } />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
||||
<TestConnectionInfo />
|
||||
|
||||
{
|
||||
_shouldDisplayTileView
|
||||
&& <>
|
||||
{ this._renderNotificationsContainer() }
|
||||
<Toolbox />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content for the Conference container when in "reduced UI" mode.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderContentForReducedUi() {
|
||||
const { _connecting } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LargeVideo onClick = { this._onClick } />
|
||||
|
||||
{
|
||||
_connecting
|
||||
&& <TintedView>
|
||||
<LoadingIndicator />
|
||||
</TintedView>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a container for notifications to be displayed by the
|
||||
* base/notifications feature.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderNotificationsContainer() {
|
||||
const notificationsStyle: ViewStyle = {};
|
||||
|
||||
// In the landscape mode (wide) there's problem with notifications being
|
||||
// shadowed by the filmstrip rendered on the right. This makes the "x"
|
||||
// button not clickable. In order to avoid that a margin of the
|
||||
// filmstrip's size is added to the right.
|
||||
//
|
||||
// Pawel: after many attempts I failed to make notifications adjust to
|
||||
// their contents width because of column and rows being used in the
|
||||
// flex layout. The only option that seemed to limit the notification's
|
||||
// size was explicit 'width' value which is not better than the margin
|
||||
// added here.
|
||||
const { _aspectRatio, _filmstripVisible } = this.props;
|
||||
|
||||
if (_filmstripVisible && _aspectRatio !== ASPECT_RATIO_NARROW) {
|
||||
notificationsStyle.marginRight = FILMSTRIP_SIZE;
|
||||
}
|
||||
|
||||
return super.renderNotificationsContainer(
|
||||
{
|
||||
shouldDisplayTileView: this.props._shouldDisplayTileView,
|
||||
style: notificationsStyle,
|
||||
toolboxVisible: this.props._toolboxVisible
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action changing the visibility of the {@link Toolbox}.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} visible - Pass {@code true} to show the
|
||||
* {@code Toolbox} or {@code false} to hide it.
|
||||
* @returns {void}
|
||||
*/
|
||||
_setToolboxVisible(visible: boolean) {
|
||||
this.props.dispatch(setToolboxVisible(visible));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated {@code Conference}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {any} _ownProps - Component's own props.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { isOpen } = state['features/participants-pane'];
|
||||
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
|
||||
const { backgroundColor } = state['features/dynamic-branding'];
|
||||
const { startCarMode } = state['features/base/settings'];
|
||||
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
|
||||
const brandingStyles = backgroundColor ? {
|
||||
background: backgroundColor
|
||||
} : undefined;
|
||||
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_aspectRatio: aspectRatio,
|
||||
_audioOnlyEnabled: Boolean(audioOnlyEnabled),
|
||||
_brandingStyles: brandingStyles,
|
||||
_calendarEnabled: isCalendarEnabled(state),
|
||||
_connecting: isConnecting(state),
|
||||
_filmstripVisible: isFilmstripVisible(state),
|
||||
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
|
||||
_isDisplayNameVisible: isDisplayNameVisible(state),
|
||||
_isParticipantsPaneOpen: isOpen,
|
||||
_largeVideoParticipantId: state['features/large-video'].participantId,
|
||||
_pictureInPictureEnabled: isPipEnabled(state),
|
||||
_reducedUI: reducedUI,
|
||||
_showLobby: getIsLobbyVisible(state),
|
||||
_startCarMode: startCarMode,
|
||||
_toolboxVisible: isToolboxVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default withSafeAreaInsets(connect(_mapStateToProps)(props => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useFocusEffect(useCallback(() => {
|
||||
dispatch({ type: CONFERENCE_FOCUSED });
|
||||
setPictureInPictureEnabled(true);
|
||||
|
||||
return () => {
|
||||
dispatch({ type: CONFERENCE_BLURRED });
|
||||
setPictureInPictureEnabled(false);
|
||||
};
|
||||
}, []));
|
||||
|
||||
return ( // @ts-ignore
|
||||
<Conference { ...props } />
|
||||
);
|
||||
}));
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { IDisplayProps } from '../ConferenceTimer';
|
||||
|
||||
/**
|
||||
* Returns native element to be rendered.
|
||||
*
|
||||
* @param {Object} props - Component props.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function ConferenceTimerDisplay({ timerValue, textStyle }: IDisplayProps) {
|
||||
return (
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { textStyle }>
|
||||
{ timerValue }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||
|
||||
import { EXPANDED_LABELS } from './constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The selected label to show details.
|
||||
*/
|
||||
visibleExpandedLabel?: string;
|
||||
}
|
||||
|
||||
const ExpandedLabelPopup = ({ visibleExpandedLabel }: IProps) => {
|
||||
if (visibleExpandedLabel) {
|
||||
const expandedLabel = EXPANDED_LABELS[visibleExpandedLabel as keyof typeof EXPANDED_LABELS];
|
||||
|
||||
if (expandedLabel) {
|
||||
const LabelComponent = expandedLabel.component;
|
||||
|
||||
const { props, alwaysOn } = expandedLabel;
|
||||
const style = {
|
||||
top: alwaysOn ? BaseTheme.spacing[6] : BaseTheme.spacing[1]
|
||||
};
|
||||
|
||||
return (<LabelComponent
|
||||
{ ...props }
|
||||
style = { style } />);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ExpandedLabelPopup;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import ExpandedLabel, { IProps as AbstractProps } from '../../../base/label/components/native/ExpandedLabel';
|
||||
import getUnsafeRoomText from '../../../base/util/getUnsafeRoomText.native';
|
||||
|
||||
import { INSECURE_ROOM_NAME_LABEL_COLOR } from './styles';
|
||||
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
getUnsafeRoomTextFn: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* A react {@code Component} that implements an expanded label as tooltip-like
|
||||
* component to explain the meaning of the {@code InsecureRoomNameExpandedLabel}.
|
||||
*/
|
||||
class InsecureRoomNameExpandedLabel extends ExpandedLabel<IProps> {
|
||||
/**
|
||||
* Returns the color this expanded label should be rendered with.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getColor() {
|
||||
return INSECURE_ROOM_NAME_LABEL_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label specific text of this {@code ExpandedLabel}.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLabel() {
|
||||
return this.props.getUnsafeRoomTextFn(this.props.t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
getUnsafeRoomTextFn: (t: Function) => getUnsafeRoomText(state, t, 'meeting')
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(InsecureRoomNameExpandedLabel));
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconWarning } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/native/Label';
|
||||
import AbstractInsecureRoomNameLabel, { _mapStateToProps } from '../AbstractInsecureRoomNameLabel';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Renders a label indicating that we are in a room with an insecure name.
|
||||
*/
|
||||
class InsecureRoomNameLabel extends AbstractInsecureRoomNameLabel {
|
||||
/**
|
||||
* Renders the platform dependent content.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_render() {
|
||||
return (
|
||||
<Label
|
||||
icon = { IconWarning }
|
||||
style = { styles.insecureRoomNameLabel } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(InsecureRoomNameLabel));
|
||||
54
react/features/conference/components/native/Labels.tsx
Normal file
54
react/features/conference/components/native/Labels.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import { TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||
|
||||
import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.native';
|
||||
|
||||
import InsecureRoomNameLabel from './InsecureRoomNameLabel';
|
||||
import { LABEL_ID_INSECURE_ROOM_NAME, LABEL_ID_QUALITY, LabelHitSlop } from './constants';
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Creates a function to be invoked when the onPress of the touchables are
|
||||
* triggered.
|
||||
*/
|
||||
createOnPress: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* A container that renders the conference indicators, if any.
|
||||
*/
|
||||
class Labels extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Implements React {@code Component}'s render.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<View pointerEvents = 'box-none'>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.indicatorContainer as ViewStyle }>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = {
|
||||
this.props.createOnPress(LABEL_ID_INSECURE_ROOM_NAME)
|
||||
} >
|
||||
<InsecureRoomNameLabel />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = {
|
||||
this.props.createOnPress(LABEL_ID_QUALITY) } >
|
||||
<VideoQualityLabel />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Labels;
|
||||
@@ -0,0 +1,159 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { INVITE_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconAddUser } from '../../../base/icons/svg';
|
||||
import {
|
||||
addPeopleFeatureControl,
|
||||
getParticipantCountWithFake,
|
||||
setShareDialogVisiblity
|
||||
} from '../../../base/participants/functions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { doInvitePeople } from '../../../invite/actions.native';
|
||||
import { getInviteOthersControl } from '../../../share-room/functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Props type of the component.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Control for invite other button.
|
||||
*/
|
||||
_inviteOthersControl: any;
|
||||
|
||||
/**
|
||||
* Checks if add-people feature is enabled.
|
||||
*/
|
||||
_isAddPeopleFeatureEnabled: boolean;
|
||||
|
||||
/**
|
||||
* True if currently in a breakout room.
|
||||
*/
|
||||
_isInBreakoutRoom: boolean;
|
||||
|
||||
/**
|
||||
* True if the invite functions (dial out, invite, share...etc) are disabled.
|
||||
*/
|
||||
_isInviteFunctionsDisabled: boolean;
|
||||
|
||||
/**
|
||||
* True if it's a lonely meeting (participant count excluding fakes is 1).
|
||||
*/
|
||||
_isLonelyMeeting: boolean;
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the UI elements to be displayed in the lonely meeting experience.
|
||||
*/
|
||||
class LonelyMeetingExperience extends PureComponent<IProps> {
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onPress = this._onPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_inviteOthersControl,
|
||||
_isAddPeopleFeatureEnabled,
|
||||
_isInBreakoutRoom,
|
||||
_isInviteFunctionsDisabled,
|
||||
_isLonelyMeeting,
|
||||
t
|
||||
} = this.props;
|
||||
const { color, shareDialogVisible } = _inviteOthersControl;
|
||||
|
||||
if (!_isLonelyMeeting || !_isAddPeopleFeatureEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { styles.lonelyMeetingContainer as ViewStyle }>
|
||||
<Text style = { styles.lonelyMessage }>
|
||||
{ t('lonelyMeetingExperience.youAreAlone') }
|
||||
</Text>
|
||||
{ !_isInviteFunctionsDisabled && !_isInBreakoutRoom && (
|
||||
<Button
|
||||
accessibilityLabel = 'lonelyMeetingExperience.button'
|
||||
disabled = { shareDialogVisible }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
icon = { () => (
|
||||
<Icon
|
||||
color = { color }
|
||||
size = { 20 }
|
||||
src = { IconAddUser } />
|
||||
) }
|
||||
labelKey = 'lonelyMeetingExperience.button'
|
||||
onClick = { this._onPress }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
) }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the onPress function of the button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress() {
|
||||
const { _isAddPeopleFeatureEnabled, dispatch } = this.props;
|
||||
|
||||
setShareDialogVisiblity(_isAddPeopleFeatureEnabled, dispatch);
|
||||
|
||||
dispatch(doInvitePeople());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps parts of the Redux state to the props of this Component.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { disableInviteFunctions } = state['features/base/config'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const _inviteOthersControl = getInviteOthersControl(state);
|
||||
const flag = getFeatureFlag(state, INVITE_ENABLED, true);
|
||||
const _isAddPeopleFeatureEnabled = addPeopleFeatureControl(state);
|
||||
const _isInBreakoutRoom = isInBreakoutRoom(state);
|
||||
|
||||
return {
|
||||
_isAddPeopleFeatureEnabled,
|
||||
_inviteOthersControl,
|
||||
_isInBreakoutRoom,
|
||||
_isInviteFunctionsDisabled: Boolean(!flag || disableInviteFunctions),
|
||||
_isLonelyMeeting: Boolean(conference && getParticipantCountWithFake(state) === 1)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(translate(LonelyMeetingExperience));
|
||||
@@ -0,0 +1,24 @@
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import ExpandedLabel, { IProps as AbstractProps } from '../../../base/label/components/native/ExpandedLabel';
|
||||
|
||||
type Props = AbstractProps & WithTranslation;
|
||||
|
||||
/**
|
||||
* A react {@code Component} that implements an expanded label as tooltip-like
|
||||
* component to explain the meaning of the {@code RaisedHandsCountExpandedLabel}.
|
||||
*/
|
||||
class RaisedHandsCountExpandedLabel extends ExpandedLabel<Props> {
|
||||
|
||||
/**
|
||||
* Returns the label specific text of this {@code ExpandedLabel}.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLabel() {
|
||||
return this.props.t('raisedHandsLabel');
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(RaisedHandsCountExpandedLabel);
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/native/Label';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const RaisedHandsCountLabel = () => {
|
||||
const raisedHandsCount = useSelector((state: IReduxState) =>
|
||||
(state['features/base/participants'].raisedHandsQueue || []).length);
|
||||
|
||||
return raisedHandsCount > 0 ? (
|
||||
<Label
|
||||
icon = { IconRaiseHand }
|
||||
iconColor = { BaseTheme.palette.uiBackground }
|
||||
style = { styles.raisedHandsCountLabel }
|
||||
text = { `${raisedHandsCount}` }
|
||||
textStyle = { styles.raisedHandsCountLabelText } />
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default RaisedHandsCountLabel;
|
||||
157
react/features/conference/components/native/TitleBar.tsx
Normal file
157
react/features/conference/components/native/TitleBar.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React from 'react';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getConferenceName, getConferenceTimestamp } from '../../../base/conference/functions';
|
||||
import {
|
||||
AUDIO_DEVICE_BUTTON_ENABLED,
|
||||
CONFERENCE_TIMER_ENABLED,
|
||||
TOGGLE_CAMERA_BUTTON_ENABLED
|
||||
} from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import AudioDeviceToggleButton from '../../../mobile/audio-mode/components/AudioDeviceToggleButton';
|
||||
import PictureInPictureButton from '../../../mobile/picture-in-picture/components/PictureInPictureButton';
|
||||
import ParticipantsPaneButton from '../../../participants-pane/components/native/ParticipantsPaneButton';
|
||||
import { isParticipantsPaneEnabled } from '../../../participants-pane/functions';
|
||||
import { isRoomNameEnabled } from '../../../prejoin/functions.native';
|
||||
import ToggleCameraButton from '../../../toolbox/components/native/ToggleCameraButton';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
import ConferenceTimer from '../ConferenceTimer';
|
||||
|
||||
import Labels from './Labels';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether the audio device button should be displayed.
|
||||
*/
|
||||
_audioDeviceButtonEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether displaying the current conference timer is enabled or not.
|
||||
*/
|
||||
_conferenceTimerEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Creates a function to be invoked when the onPress of the touchables are
|
||||
* triggered.
|
||||
*/
|
||||
_createOnPress: Function;
|
||||
|
||||
/**
|
||||
* Whether participants feature is enabled or not.
|
||||
*/
|
||||
_isParticipantsPaneEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Name of the meeting we're currently in.
|
||||
*/
|
||||
_meetingName: string;
|
||||
|
||||
/**
|
||||
* Whether displaying the current room name is enabled or not.
|
||||
*/
|
||||
_roomNameEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the toggle camera button should be displayed.
|
||||
*/
|
||||
_toggleCameraButtonEnabled: boolean;
|
||||
|
||||
/**
|
||||
* True if the navigation bar should be visible.
|
||||
*/
|
||||
_visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a navigation bar component that is rendered on top of the
|
||||
* conference screen.
|
||||
*
|
||||
* @param {IProps} props - The React props passed to this component.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const TitleBar = (props: IProps) => {
|
||||
const { _isParticipantsPaneEnabled, _visible } = props;
|
||||
|
||||
if (!_visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { styles.titleBarWrapper as ViewStyle }>
|
||||
<View style = { styles.pipButtonContainer as ViewStyle }>
|
||||
<PictureInPictureButton styles = { styles.pipButton } />
|
||||
</View>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.roomNameWrapper as ViewStyle }>
|
||||
{
|
||||
props._conferenceTimerEnabled
|
||||
&& <View style = { styles.roomTimerView as ViewStyle }>
|
||||
<ConferenceTimer textStyle = { styles.roomTimer } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
props._roomNameEnabled
|
||||
&& <View style = { styles.roomNameView as ViewStyle }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.roomName }>
|
||||
{ props._meetingName }
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||
<Labels createOnPress = { props._createOnPress } />
|
||||
</View>
|
||||
{
|
||||
props._toggleCameraButtonEnabled
|
||||
&& <View style = { styles.titleBarButtonContainer }>
|
||||
<ToggleCameraButton styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
props._audioDeviceButtonEnabled
|
||||
&& <View style = { styles.titleBarButtonContainer }>
|
||||
<AudioDeviceToggleButton styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
}
|
||||
{
|
||||
_isParticipantsPaneEnabled
|
||||
&& <View style = { styles.titleBarButtonContainer }>
|
||||
<ParticipantsPaneButton
|
||||
styles = { styles.titleBarButton } />
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { hideConferenceTimer } = state['features/base/config'];
|
||||
const startTimestamp = getConferenceTimestamp(state);
|
||||
|
||||
return {
|
||||
_audioDeviceButtonEnabled: getFeatureFlag(state, AUDIO_DEVICE_BUTTON_ENABLED, true),
|
||||
_conferenceTimerEnabled:
|
||||
Boolean(getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer && startTimestamp),
|
||||
_isParticipantsPaneEnabled: isParticipantsPaneEnabled(state),
|
||||
_meetingName: getConferenceName(state),
|
||||
_roomNameEnabled: isRoomNameEnabled(state),
|
||||
_toggleCameraButtonEnabled: getFeatureFlag(state, TOGGLE_CAMERA_BUTTON_ENABLED, true),
|
||||
_visible: isToolboxVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(TitleBar);
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconVolumeUp } from '../../../../base/icons/svg';
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* React component for Audio icon.
|
||||
*
|
||||
* @returns {JSX.Element} - The Audio icon.
|
||||
*/
|
||||
const AudioIcon = (): JSX.Element => (<Icon
|
||||
color = { BaseTheme.palette.ui02 }
|
||||
size = { 20 }
|
||||
src = { IconVolumeUp } />);
|
||||
|
||||
export default AudioIcon;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import Orientation from 'react-native-orientation-locker';
|
||||
import { withSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../../base/react/components/native/LoadingIndicator';
|
||||
import TintedView from '../../../../base/react/components/native/TintedView';
|
||||
import { isLocalVideoTrackDesktop } from '../../../../base/tracks/functions.native';
|
||||
import { setPictureInPictureEnabled } from '../../../../mobile/picture-in-picture/functions';
|
||||
import { setIsCarmode } from '../../../../video-layout/actions';
|
||||
import ConferenceTimer from '../../ConferenceTimer';
|
||||
import { isConnecting } from '../../functions';
|
||||
|
||||
import CarModeFooter from './CarModeFooter';
|
||||
import MicrophoneButton from './MicrophoneButton';
|
||||
import TitleBar from './TitleBar';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Implements the carmode component.
|
||||
*
|
||||
* @returns { JSX.Element} - The carmode component.
|
||||
*/
|
||||
const CarMode = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const connecting = useSelector(isConnecting);
|
||||
const isSharing = useSelector(isLocalVideoTrackDesktop);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setIsCarmode(true));
|
||||
setPictureInPictureEnabled(false);
|
||||
Orientation.lockToPortrait();
|
||||
|
||||
return () => {
|
||||
Orientation.unlockAllOrientations();
|
||||
dispatch(setIsCarmode(false));
|
||||
if (!isSharing) {
|
||||
setPictureInPictureEnabled(true);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
footerComponent = { CarModeFooter }
|
||||
style = { styles.conference }>
|
||||
{/*
|
||||
* The activity/loading indicator goes above everything, except
|
||||
* the toolbox/toolbars and the dialogs.
|
||||
*/
|
||||
connecting
|
||||
&& <TintedView>
|
||||
<LoadingIndicator />
|
||||
</TintedView>
|
||||
}
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.titleBarSafeViewColor as ViewStyle }>
|
||||
<View
|
||||
style = { styles.titleBar as ViewStyle }>
|
||||
<TitleBar />
|
||||
</View>
|
||||
<ConferenceTimer textStyle = { styles.roomTimer } />
|
||||
</View>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.microphoneContainer as ViewStyle }>
|
||||
<MicrophoneButton />
|
||||
</View>
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default withSafeAreaInsets(CarMode);
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
|
||||
import EndMeetingButton from './EndMeetingButton';
|
||||
import SoundDeviceButton from './SoundDeviceButton';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Implements the car mode footer component.
|
||||
*
|
||||
* @returns { JSX.Element} - The car mode footer component.
|
||||
*/
|
||||
const CarModeFooter = (): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.bottomContainer as ViewStyle }>
|
||||
<Text style = { styles.videoStoppedLabel }>
|
||||
{ t('carmode.labels.videoStopped') }
|
||||
</Text>
|
||||
<SoundDeviceButton />
|
||||
<EndMeetingButton />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CarModeFooter;
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../../analytics/functions';
|
||||
import { appNavigate } from '../../../../app/actions.native';
|
||||
import Button from '../../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../../base/ui/constants.native';
|
||||
|
||||
import EndMeetingIcon from './EndMeetingIcon';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Button for ending meeting from carmode.
|
||||
*
|
||||
* @returns {JSX.Element} - The end meeting button.
|
||||
*/
|
||||
const EndMeetingButton = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSelect = useCallback(() => {
|
||||
sendAnalytics(createToolbarEvent('hangup'));
|
||||
|
||||
dispatch(appNavigate(undefined));
|
||||
}, [ dispatch ]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.leaveConference'
|
||||
icon = { EndMeetingIcon }
|
||||
labelKey = 'toolbar.leaveConference'
|
||||
onClick = { onSelect }
|
||||
style = { styles.endMeetingButton }
|
||||
type = { BUTTON_TYPES.DESTRUCTIVE } />
|
||||
);
|
||||
};
|
||||
|
||||
export default EndMeetingButton;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconHangup } from '../../../../base/icons/svg';
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* Implements an end meeting icon.
|
||||
*
|
||||
* @returns {JSX.Element} - The end meeting icon.
|
||||
*/
|
||||
const EndMeetingIcon = (): JSX.Element => (<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 20 }
|
||||
src = { IconHangup } />);
|
||||
|
||||
export default EndMeetingIcon;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { TouchableOpacity, View, ViewStyle } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
ACTION_SHORTCUT_PRESSED as PRESSED,
|
||||
ACTION_SHORTCUT_RELEASED as RELEASED,
|
||||
createShortcutEvent
|
||||
} from '../../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../../analytics/functions';
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { AUDIO_MUTE_BUTTON_ENABLED } from '../../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../../base/flags/functions';
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconMic, IconMicSlash } from '../../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../../base/media/constants';
|
||||
import { isLocalTrackMuted } from '../../../../base/tracks/functions';
|
||||
import { isAudioMuteButtonDisabled } from '../../../../toolbox/functions.any';
|
||||
import { muteLocal } from '../../../../video-menu/actions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const LONG_PRESS = 'long.press';
|
||||
|
||||
/**
|
||||
* Implements a round audio mute/unmute button of a custom size.
|
||||
*
|
||||
* @returns {JSX.Element} - The audio mute round button.
|
||||
*/
|
||||
const MicrophoneButton = (): JSX.Element | null => {
|
||||
const dispatch = useDispatch();
|
||||
const audioMuted = useSelector((state: IReduxState) => isLocalTrackMuted(state['features/base/tracks'],
|
||||
MEDIA_TYPE.AUDIO));
|
||||
const disabled = useSelector(isAudioMuteButtonDisabled);
|
||||
const enabledFlag = useSelector((state: IReduxState) => getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true));
|
||||
const [ longPress, setLongPress ] = useState(false);
|
||||
|
||||
if (!enabledFlag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onPressIn = useCallback(() => {
|
||||
!disabled && dispatch(muteLocal(!audioMuted, MEDIA_TYPE.AUDIO));
|
||||
}, [ audioMuted, disabled ]);
|
||||
|
||||
const onLongPress = useCallback(() => {
|
||||
if (!disabled && !audioMuted) {
|
||||
sendAnalytics(createShortcutEvent(
|
||||
'push.to.talk',
|
||||
PRESSED,
|
||||
{},
|
||||
LONG_PRESS));
|
||||
setLongPress(true);
|
||||
}
|
||||
}, [ audioMuted, disabled, setLongPress ]);
|
||||
|
||||
const onPressOut = useCallback(() => {
|
||||
if (longPress) {
|
||||
setLongPress(false);
|
||||
sendAnalytics(createShortcutEvent(
|
||||
'push.to.talk',
|
||||
RELEASED,
|
||||
{},
|
||||
LONG_PRESS
|
||||
));
|
||||
dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
|
||||
}
|
||||
}, [ longPress, setLongPress ]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onLongPress = { onLongPress }
|
||||
onPressIn = { onPressIn }
|
||||
onPressOut = { onPressOut } >
|
||||
<View
|
||||
style = { [
|
||||
styles.microphoneStyles.container,
|
||||
!audioMuted && styles.microphoneStyles.unmuted
|
||||
] as ViewStyle[] }>
|
||||
<View
|
||||
style = { styles.microphoneStyles.iconContainer as ViewStyle }>
|
||||
<Icon
|
||||
src = { audioMuted ? IconMicSlash : IconMic }
|
||||
style = { styles.microphoneStyles.icon } />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default MicrophoneButton;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { openSheet } from '../../../../base/dialog/actions';
|
||||
import Button from '../../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../../base/ui/constants.native';
|
||||
import AudioRoutePickerDialog from '../../../../mobile/audio-mode/components/AudioRoutePickerDialog';
|
||||
|
||||
import AudioIcon from './AudioIcon';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Button for selecting sound device in carmode.
|
||||
*
|
||||
* @returns {JSX.Element} - The sound device button.
|
||||
*/
|
||||
const SelectSoundDevice = (): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSelect = useCallback(() =>
|
||||
dispatch(openSheet(AudioRoutePickerDialog))
|
||||
, [ dispatch ]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
accessibilityLabel = 'carmode.actions.selectSoundDevice'
|
||||
icon = { AudioIcon }
|
||||
labelKey = 'carmode.actions.selectSoundDevice'
|
||||
onClick = { onSelect }
|
||||
style = { styles.soundDeviceButton }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectSoundDevice;
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { StyleProp, Text, View, ViewStyle } from 'react-native';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { getConferenceName } from '../../../../base/conference/functions';
|
||||
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant } from '../../../../base/participants/functions';
|
||||
import ConnectionIndicator from '../../../../connection-indicator/components/native/ConnectionIndicator';
|
||||
import { isRoomNameEnabled } from '../../../../prejoin/functions';
|
||||
import RecordingLabel from '../../../../recording/components/native/RecordingLabel';
|
||||
import VideoQualityLabel from '../../../../video-quality/components/VideoQualityLabel.native';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Name of the meeting we're currently in.
|
||||
*/
|
||||
_meetingName: string;
|
||||
|
||||
/**
|
||||
* Whether displaying the current meeting name is enabled or not.
|
||||
*/
|
||||
_meetingNameEnabled: boolean;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a navigation bar component that is rendered on top of the
|
||||
* carmode screen.
|
||||
*
|
||||
* @param {IProps} props - The React props passed to this component.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const TitleBar = (props: IProps): JSX.Element => {
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const localParticipantId = localParticipant?.id;
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { styles.titleBarWrapper as StyleProp<ViewStyle> }>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.roomNameWrapper as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.qualityLabelContainer as StyleProp<ViewStyle> }>
|
||||
<VideoQualityLabel />
|
||||
</View>
|
||||
<ConnectionIndicator
|
||||
iconStyle = { styles.connectionIndicatorIcon }
|
||||
participantId = { localParticipantId } />
|
||||
<View style = { styles.headerLabels as StyleProp<ViewStyle> }>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</View>
|
||||
{
|
||||
props._meetingNameEnabled
|
||||
&& <View style = { styles.roomNameView as StyleProp<ViewStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.roomName }>
|
||||
{ props._meetingName }
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_meetingName: getConferenceName(state),
|
||||
_meetingNameEnabled: isRoomNameEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(TitleBar);
|
||||
173
react/features/conference/components/native/carmode/styles.ts
Normal file
173
react/features/conference/components/native/carmode/styles.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* The size of the microphone icon.
|
||||
*/
|
||||
const MICROPHONE_SIZE = 180;
|
||||
|
||||
/**
|
||||
* The styles of the safe area view that contains the title bar.
|
||||
*/
|
||||
const titleBarSafeView = {
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the native components of Carmode.
|
||||
*/
|
||||
export default {
|
||||
|
||||
bottomContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bottom: BaseTheme.spacing[8]
|
||||
},
|
||||
|
||||
/**
|
||||
* {@code Conference} Style.
|
||||
*/
|
||||
conference: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
microphoneStyles: {
|
||||
container: {
|
||||
borderRadius: MICROPHONE_SIZE / 2,
|
||||
height: MICROPHONE_SIZE,
|
||||
maxHeight: MICROPHONE_SIZE,
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
width: MICROPHONE_SIZE,
|
||||
maxWidth: MICROPHONE_SIZE,
|
||||
flex: 1,
|
||||
zIndex: 1,
|
||||
elevation: 1
|
||||
},
|
||||
|
||||
icon: {
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: MICROPHONE_SIZE * 0.45,
|
||||
fontWeight: '100'
|
||||
},
|
||||
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui03
|
||||
},
|
||||
|
||||
unmuted: {
|
||||
borderWidth: 4,
|
||||
borderColor: BaseTheme.palette.success01
|
||||
}
|
||||
},
|
||||
|
||||
qualityLabelContainer: {
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
flexShrink: 1,
|
||||
paddingHorizontal: 2,
|
||||
justifyContent: 'center',
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
roomTimer: {
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
color: BaseTheme.palette.text01,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
titleView: {
|
||||
width: 152,
|
||||
height: 28,
|
||||
backgroundColor: BaseTheme.palette.ui02,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'center'
|
||||
},
|
||||
|
||||
title: {
|
||||
margin: 'auto',
|
||||
textAlign: 'center',
|
||||
paddingVertical: BaseTheme.spacing[1],
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
color: BaseTheme.palette.text02
|
||||
},
|
||||
|
||||
soundDeviceButton: {
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
width: 240
|
||||
},
|
||||
|
||||
endMeetingButton: {
|
||||
width: 240
|
||||
},
|
||||
|
||||
headerLabels: {
|
||||
borderBottomLeftRadius: 3,
|
||||
borderTopLeftRadius: 3,
|
||||
flexShrink: 1,
|
||||
paddingHorizontal: 2,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
titleBarSafeViewColor: {
|
||||
...titleBarSafeView,
|
||||
backgroundColor: BaseTheme.palette.uiBackground
|
||||
},
|
||||
|
||||
microphoneContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
titleBarWrapper: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
roomNameWrapper: {
|
||||
flexDirection: 'row',
|
||||
marginRight: BaseTheme.spacing[2],
|
||||
flexShrink: 1,
|
||||
flexGrow: 1
|
||||
},
|
||||
|
||||
roomNameView: {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
flexShrink: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
roomName: {
|
||||
color: BaseTheme.palette.text01,
|
||||
...BaseTheme.typography.bodyShortBold
|
||||
},
|
||||
|
||||
titleBar: {
|
||||
alignSelf: 'center',
|
||||
marginTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
videoStoppedLabel: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
textAlign: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
connectionIndicatorIcon: {
|
||||
fontSize: 20
|
||||
}
|
||||
};
|
||||
66
react/features/conference/components/native/constants.ts
Normal file
66
react/features/conference/components/native/constants.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import RecordingExpandedLabel from '../../../recording/components/native/RecordingExpandedLabel';
|
||||
import VideoQualityExpandedLabel from '../../../video-quality/components/VideoQualityExpandedLabel.native';
|
||||
|
||||
import InsecureRoomNameExpandedLabel from './InsecureRoomNameExpandedLabel';
|
||||
import RaisedHandsCountExpandedLabel from './RaisedHandsCountExpandedLabel';
|
||||
|
||||
export const LabelHitSlop = {
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
left: 0,
|
||||
right: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Timeout to hide the {@ExpandedLabel}.
|
||||
*/
|
||||
export const EXPANDED_LABEL_TIMEOUT = 5000;
|
||||
|
||||
export const LABEL_ID_QUALITY = 'quality';
|
||||
export const LABEL_ID_RECORDING = 'recording';
|
||||
export const LABEL_ID_STREAMING = 'streaming';
|
||||
export const LABEL_ID_INSECURE_ROOM_NAME = 'insecure-room-name';
|
||||
export const LABEL_ID_RAISED_HANDS_COUNT = 'raised-hands-count';
|
||||
export const LABEL_ID_VISITORS_COUNT = 'visitors-count';
|
||||
|
||||
interface IExpandedLabel {
|
||||
alwaysOn?: boolean;
|
||||
component: React.ComponentType<any>;
|
||||
props?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@code ExpandedLabel} components to be rendered for the individual
|
||||
* {@code Label}s.
|
||||
*/
|
||||
export const EXPANDED_LABELS: {
|
||||
[key: string]: IExpandedLabel;
|
||||
} = {
|
||||
[LABEL_ID_QUALITY]: {
|
||||
component: VideoQualityExpandedLabel
|
||||
},
|
||||
[LABEL_ID_RECORDING]: {
|
||||
component: RecordingExpandedLabel,
|
||||
props: {
|
||||
mode: JitsiRecordingConstants.mode.FILE
|
||||
},
|
||||
alwaysOn: true
|
||||
},
|
||||
[LABEL_ID_STREAMING]: {
|
||||
component: RecordingExpandedLabel,
|
||||
props: {
|
||||
mode: JitsiRecordingConstants.mode.STREAM
|
||||
},
|
||||
alwaysOn: true
|
||||
},
|
||||
[LABEL_ID_INSECURE_ROOM_NAME]: {
|
||||
component: InsecureRoomNameExpandedLabel
|
||||
},
|
||||
[LABEL_ID_RAISED_HANDS_COUNT]: {
|
||||
component: RaisedHandsCountExpandedLabel,
|
||||
alwaysOn: true
|
||||
}
|
||||
};
|
||||
212
react/features/conference/components/native/styles.ts
Normal file
212
react/features/conference/components/native/styles.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const INSECURE_ROOM_NAME_LABEL_COLOR = BaseTheme.palette.actionDanger;
|
||||
|
||||
const TITLE_BAR_BUTTON_SIZE = 24;
|
||||
|
||||
|
||||
/**
|
||||
* The styles of the safe area view that contains the title bar.
|
||||
*/
|
||||
const titleBarSafeView = {
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0
|
||||
};
|
||||
|
||||
const alwaysOnTitleBar = {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-end',
|
||||
backgroundColor: 'rgba(0, 0, 0, .5)',
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: BaseTheme.spacing[3],
|
||||
paddingRight: BaseTheme.spacing[0],
|
||||
'&:not(:empty)': {
|
||||
padding: BaseTheme.spacing[1]
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the feature conference.
|
||||
*/
|
||||
export default {
|
||||
|
||||
/**
|
||||
* {@code Conference} Style.
|
||||
*/
|
||||
conference: {
|
||||
alignSelf: 'stretch',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
displayNameContainer: {
|
||||
margin: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* View that contains the indicators.
|
||||
*/
|
||||
indicatorContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
titleBarButtonContainer: {
|
||||
borderRadius: 3,
|
||||
height: BaseTheme.spacing[7],
|
||||
marginTop: BaseTheme.spacing[1],
|
||||
marginRight: BaseTheme.spacing[1],
|
||||
zIndex: 1,
|
||||
width: BaseTheme.spacing[7]
|
||||
},
|
||||
|
||||
titleBarButton: {
|
||||
iconStyle: {
|
||||
color: BaseTheme.palette.icon01,
|
||||
padding: 12,
|
||||
fontSize: TITLE_BAR_BUTTON_SIZE
|
||||
},
|
||||
underlayColor: 'transparent'
|
||||
},
|
||||
|
||||
lonelyMeetingContainer: {
|
||||
alignSelf: 'stretch',
|
||||
alignItems: 'center',
|
||||
padding: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
lonelyMessage: {
|
||||
color: BaseTheme.palette.text01,
|
||||
paddingVertical: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
pipButtonContainer: {
|
||||
'&:not(:empty)': {
|
||||
borderRadius: 3,
|
||||
height: BaseTheme.spacing[7],
|
||||
marginTop: BaseTheme.spacing[1],
|
||||
marginLeft: BaseTheme.spacing[1],
|
||||
zIndex: 1,
|
||||
width: BaseTheme.spacing[7]
|
||||
}
|
||||
},
|
||||
|
||||
pipButton: {
|
||||
iconStyle: {
|
||||
color: BaseTheme.palette.icon01,
|
||||
padding: 12,
|
||||
fontSize: TITLE_BAR_BUTTON_SIZE
|
||||
},
|
||||
underlayColor: 'transparent'
|
||||
},
|
||||
|
||||
titleBarSafeViewColor: {
|
||||
...titleBarSafeView,
|
||||
backgroundColor: BaseTheme.palette.uiBackground
|
||||
},
|
||||
|
||||
titleBarSafeViewTransparent: {
|
||||
...titleBarSafeView
|
||||
},
|
||||
|
||||
titleBarWrapper: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[8],
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
alwaysOnTitleBar: {
|
||||
...alwaysOnTitleBar,
|
||||
marginRight: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
alwaysOnTitleBarWide: {
|
||||
...alwaysOnTitleBar,
|
||||
marginRight: BaseTheme.spacing[12]
|
||||
},
|
||||
|
||||
expandedLabelWrapper: {
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
roomTimer: {
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
color: BaseTheme.palette.text01,
|
||||
lineHeight: 14,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
roomTimerView: {
|
||||
backgroundColor: BaseTheme.palette.ui03,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
height: 32,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: BaseTheme.spacing[2],
|
||||
paddingVertical: BaseTheme.spacing[1],
|
||||
minWidth: 50
|
||||
},
|
||||
|
||||
roomName: {
|
||||
color: BaseTheme.palette.text01,
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
paddingVertical: 6
|
||||
},
|
||||
|
||||
roomNameView: {
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
borderBottomLeftRadius: 3,
|
||||
borderTopLeftRadius: 3,
|
||||
flexShrink: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10
|
||||
},
|
||||
|
||||
roomNameWrapper: {
|
||||
flexDirection: 'row',
|
||||
marginRight: 10,
|
||||
marginLeft: 8,
|
||||
flexShrink: 1,
|
||||
flexGrow: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the {@link View} which expands over the whole
|
||||
* {@link Conference} area and splits it between the {@link Filmstrip} and
|
||||
* the {@link Toolbox}.
|
||||
*/
|
||||
toolboxAndFilmstripContainer: {
|
||||
bottom: 0,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
|
||||
insecureRoomNameLabel: {
|
||||
backgroundColor: INSECURE_ROOM_NAME_LABEL_COLOR,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
height: 32
|
||||
},
|
||||
|
||||
raisedHandsCountLabel: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.warning02,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
flexDirection: 'row',
|
||||
marginBottom: BaseTheme.spacing[0],
|
||||
marginLeft: BaseTheme.spacing[0]
|
||||
},
|
||||
|
||||
raisedHandsCountLabelText: {
|
||||
color: BaseTheme.palette.uiBackground,
|
||||
paddingLeft: BaseTheme.spacing[2]
|
||||
}
|
||||
};
|
||||
497
react/features/conference/components/web/Conference.tsx
Normal file
497
react/features/conference/components/web/Conference.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
import { throttle } from 'lodash-es';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect as reactReduxConnect, useDispatch, useSelector, useStore } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getConferenceNameForTitle } from '../../../base/conference/functions';
|
||||
import { hangup } from '../../../base/connection/actions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { setColorAlpha } from '../../../base/util/helpers';
|
||||
import { openChat, setFocusedTab } from '../../../chat/actions.web';
|
||||
import Chat from '../../../chat/components/web/Chat';
|
||||
import { ChatTabs } from '../../../chat/constants';
|
||||
import { isFileUploadingEnabled, processFiles } from '../../../file-sharing/functions.any';
|
||||
import MainFilmstrip from '../../../filmstrip/components/web/MainFilmstrip';
|
||||
import ScreenshareFilmstrip from '../../../filmstrip/components/web/ScreenshareFilmstrip';
|
||||
import StageFilmstrip from '../../../filmstrip/components/web/StageFilmstrip';
|
||||
import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeInfoContainer';
|
||||
import LargeVideo from '../../../large-video/components/LargeVideo.web';
|
||||
import LobbyScreen from '../../../lobby/components/web/LobbyScreen';
|
||||
import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||
import { getOverlayToRender } from '../../../overlay/functions.web';
|
||||
import ParticipantsPane from '../../../participants-pane/components/web/ParticipantsPane';
|
||||
import Prejoin from '../../../prejoin/components/web/Prejoin';
|
||||
import { isPrejoinPageVisible } from '../../../prejoin/functions.web';
|
||||
import ReactionAnimations from '../../../reactions/components/web/ReactionsAnimations';
|
||||
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
|
||||
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
|
||||
import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
|
||||
import Toolbox from '../../../toolbox/components/web/Toolbox';
|
||||
import { LAYOUT_CLASSNAMES } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.any';
|
||||
import VisitorsQueue from '../../../visitors/components/web/VisitorsQueue';
|
||||
import { showVisitorsQueue } from '../../../visitors/functions';
|
||||
import { init } from '../../actions.web';
|
||||
import { maybeShowSuboptimalExperienceNotification } from '../../functions.web';
|
||||
import {
|
||||
AbstractConference,
|
||||
type AbstractProps,
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractConference';
|
||||
|
||||
import ConferenceInfo from './ConferenceInfo';
|
||||
import { default as Notice } from './Notice';
|
||||
|
||||
/**
|
||||
* DOM events for when full screen mode has changed. Different browsers need
|
||||
* different vendor prefixes.
|
||||
*
|
||||
* @private
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const FULL_SCREEN_EVENTS = [
|
||||
'webkitfullscreenchange',
|
||||
'mozfullscreenchange',
|
||||
'fullscreenchange'
|
||||
];
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Conference}.
|
||||
*/
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* The alpha(opacity) of the background.
|
||||
*/
|
||||
_backgroundAlpha?: number;
|
||||
|
||||
/**
|
||||
* Are any overlays visible?
|
||||
*/
|
||||
_isAnyOverlayVisible: boolean;
|
||||
|
||||
/**
|
||||
* The CSS class to apply to the root of {@link Conference} to modify the
|
||||
* application layout.
|
||||
*/
|
||||
_layoutClassName: string;
|
||||
|
||||
/**
|
||||
* The config specified interval for triggering mouseMoved iframe api events.
|
||||
*/
|
||||
_mouseMoveCallbackInterval?: number;
|
||||
|
||||
/**
|
||||
*Whether or not the notifications should be displayed in the overflow drawer.
|
||||
*/
|
||||
_overflowDrawer: boolean;
|
||||
|
||||
/**
|
||||
* Name for this conference room.
|
||||
*/
|
||||
_roomName: string;
|
||||
|
||||
/**
|
||||
* If lobby page is visible or not.
|
||||
*/
|
||||
_showLobby: boolean;
|
||||
|
||||
/**
|
||||
* If prejoin page is visible or not.
|
||||
*/
|
||||
_showPrejoin: boolean;
|
||||
|
||||
/**
|
||||
* If visitors queue page is visible or not.
|
||||
* NOTE: This should be set to true once we received an error on connect. Before the first connect this will always
|
||||
* be false.
|
||||
*/
|
||||
_showVisitorsQueue: boolean;
|
||||
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the prejoin screen should be displayed and false otherwise.
|
||||
*
|
||||
* @param {IProps} props - The props object.
|
||||
* @returns {boolean} - True if the prejoin screen should be displayed and false otherwise.
|
||||
*/
|
||||
function shouldShowPrejoin({ _showLobby, _showPrejoin, _showVisitorsQueue }: IProps) {
|
||||
return _showPrejoin && !_showVisitorsQueue && !_showLobby;
|
||||
}
|
||||
|
||||
/**
|
||||
* The conference page of the Web application.
|
||||
*/
|
||||
class Conference extends AbstractConference<IProps, any> {
|
||||
_originalOnMouseMove: Function;
|
||||
_originalOnShowToolbar: Function;
|
||||
|
||||
/**
|
||||
* Initializes a new Conference instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const { _mouseMoveCallbackInterval } = props;
|
||||
|
||||
// Throttle and bind this component's mousemove handler to prevent it
|
||||
// from firing too often.
|
||||
this._originalOnShowToolbar = this._onShowToolbar;
|
||||
this._originalOnMouseMove = this._onMouseMove;
|
||||
|
||||
this._onShowToolbar = throttle(
|
||||
() => this._originalOnShowToolbar(),
|
||||
100,
|
||||
{
|
||||
leading: true,
|
||||
trailing: false
|
||||
});
|
||||
|
||||
this._onMouseMove = throttle(
|
||||
event => this._originalOnMouseMove(event),
|
||||
_mouseMoveCallbackInterval,
|
||||
{
|
||||
leading: true,
|
||||
trailing: false
|
||||
});
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onFullScreenChange = this._onFullScreenChange.bind(this);
|
||||
this._onVideospaceTouchStart = this._onVideospaceTouchStart.bind(this);
|
||||
this._setBackground = this._setBackground.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the connection and get the UI ready for the conference.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
document.title = `${this.props._roomName} | ${interfaceConfig.APP_NAME}`;
|
||||
this._start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls into legacy UI to update the application layout, if necessary.
|
||||
*
|
||||
* @inheritdoc
|
||||
* returns {void}
|
||||
*/
|
||||
override componentDidUpdate(prevProps: IProps) {
|
||||
if (this.props._shouldDisplayTileView
|
||||
=== prevProps._shouldDisplayTileView) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: For now VideoLayout is being called as LargeVideo and Filmstrip
|
||||
// sizing logic is still handled outside of React. Once all components
|
||||
// are in react they should calculate size on their own as much as
|
||||
// possible and pass down sizings.
|
||||
VideoLayout.refreshLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the conference when component will be
|
||||
* unmounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
APP.UI.unbindEvents();
|
||||
|
||||
FULL_SCREEN_EVENTS.forEach(name =>
|
||||
document.removeEventListener(name, this._onFullScreenChange));
|
||||
|
||||
APP.conference.isJoined() && this.props.dispatch(hangup());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_isAnyOverlayVisible,
|
||||
_layoutClassName,
|
||||
_notificationsVisible,
|
||||
_overflowDrawer,
|
||||
_showLobby,
|
||||
_showPrejoin,
|
||||
_showVisitorsQueue,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
id = 'layout_wrapper'
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }
|
||||
onMouseMove = { this._onMouseMove }
|
||||
ref = { this._setBackground }>
|
||||
<Chat />
|
||||
<div
|
||||
className = { _layoutClassName }
|
||||
id = 'videoconference_page'
|
||||
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
|
||||
{ _showPrejoin || _showLobby || <ConferenceInfo /> }
|
||||
<Notice />
|
||||
<div
|
||||
id = 'videospace'
|
||||
onTouchStart = { this._onVideospaceTouchStart }>
|
||||
<LargeVideo />
|
||||
{
|
||||
_showPrejoin || _showLobby || (<>
|
||||
<StageFilmstrip />
|
||||
<ScreenshareFilmstrip />
|
||||
<MainFilmstrip />
|
||||
</>)
|
||||
}
|
||||
</div>
|
||||
|
||||
{ _showPrejoin || _showLobby || (
|
||||
<>
|
||||
<span
|
||||
aria-level = { 1 }
|
||||
className = 'sr-only'
|
||||
role = 'heading'>
|
||||
{ t('toolbar.accessibilityLabel.heading') }
|
||||
</span>
|
||||
<Toolbox />
|
||||
</>
|
||||
)}
|
||||
|
||||
{_notificationsVisible && !_isAnyOverlayVisible && (_overflowDrawer
|
||||
? <JitsiPortal className = 'notification-portal'>
|
||||
{this.renderNotificationsContainer({ portal: true })}
|
||||
</JitsiPortal>
|
||||
: this.renderNotificationsContainer())
|
||||
}
|
||||
|
||||
<CalleeInfoContainer />
|
||||
|
||||
{ shouldShowPrejoin(this.props) && <Prejoin />}
|
||||
{ (_showLobby && !_showVisitorsQueue) && <LobbyScreen />}
|
||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||
</div>
|
||||
<ParticipantsPane />
|
||||
<ReactionAnimations />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets custom background opacity based on config. It also applies the
|
||||
* opacity on parent element, as the parent element is not accessible directly,
|
||||
* only though it's child.
|
||||
*
|
||||
* @param {Object} element - The DOM element for which to apply opacity.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setBackground(element: HTMLDivElement) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props._backgroundAlpha !== undefined) {
|
||||
const elemColor = element.style.background;
|
||||
const alphaElemColor = setColorAlpha(elemColor, this.props._backgroundAlpha);
|
||||
|
||||
element.style.background = alphaElemColor;
|
||||
if (element.parentElement) {
|
||||
const parentColor = element.parentElement.style.background;
|
||||
const alphaParentColor = setColorAlpha(parentColor, this.props._backgroundAlpha);
|
||||
|
||||
element.parentElement.style.background = alphaParentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler used for touch start on Video container.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onVideospaceTouchStart() {
|
||||
this.props.dispatch(toggleToolboxVisible());
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Redux state when full screen mode has been enabled or
|
||||
* disabled.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFullScreenChange() {
|
||||
this.props.dispatch(fullScreenChanged(APP.UI.isFullScreen()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers iframe API mouseEnter event.
|
||||
*
|
||||
* @param {MouseEvent} event - The mouse event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseEnter(event: React.MouseEvent) {
|
||||
APP.API.notifyMouseEnter(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers iframe API mouseLeave event.
|
||||
*
|
||||
* @param {MouseEvent} event - The mouse event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseLeave(event: React.MouseEvent) {
|
||||
APP.API.notifyMouseLeave(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers iframe API mouseMove event.
|
||||
*
|
||||
* @param {MouseEvent} event - The mouse event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseMove(event: React.MouseEvent) {
|
||||
APP.API.notifyMouseMove(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the toolbar.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShowToolbar() {
|
||||
this.props.dispatch(showToolbox());
|
||||
}
|
||||
|
||||
/**
|
||||
* Until we don't rewrite UI using react components
|
||||
* we use UI.start from old app. Also method translates
|
||||
* component right after it has been mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_start() {
|
||||
APP.UI.start();
|
||||
APP.UI.bindEvents();
|
||||
|
||||
FULL_SCREEN_EVENTS.forEach(name =>
|
||||
document.addEventListener(name, this._onFullScreenChange));
|
||||
|
||||
const { dispatch, t } = this.props;
|
||||
|
||||
// if we will be showing prejoin we don't want to call connect from init.
|
||||
// Connect will be dispatched from prejoin screen.
|
||||
dispatch(init(!shouldShowPrejoin(this.props)));
|
||||
|
||||
maybeShowSuboptimalExperienceNotification(dispatch, t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code Conference} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config'];
|
||||
const { overflowDrawer } = state['features/toolbox'];
|
||||
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_backgroundAlpha: backgroundAlpha,
|
||||
_isAnyOverlayVisible: Boolean(getOverlayToRender(state)),
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state) ?? ''],
|
||||
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_roomName: getConferenceNameForTitle(state),
|
||||
_showLobby: getIsLobbyVisible(state),
|
||||
_showPrejoin: isPrejoinPageVisible(state),
|
||||
_showVisitorsQueue: showVisitorsQueue(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default reactReduxConnect(_mapStateToProps)(translate(props => {
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore();
|
||||
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
|
||||
const { isOpen: isChatOpen } = useSelector((state: IReduxState) => state['features/chat']);
|
||||
const isFileUploadEnabled = useSelector(isFileUploadingEnabled);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isFileUploadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
if (!isChatOpen) {
|
||||
dispatch(openChat());
|
||||
}
|
||||
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
|
||||
}
|
||||
}, [ isChatOpen, isDragging, isFileUploadEnabled ]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (!isFileUploadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.dataTransfer.files?.length > 0) {
|
||||
processFiles(e.dataTransfer.files, store);
|
||||
}
|
||||
}, [ isFileUploadEnabled, processFiles ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragEnter = { handleDragEnter }
|
||||
onDragLeave = { handleDragLeave }
|
||||
onDragOver = { handleDragOver }
|
||||
onDrop = { handleDrop }>
|
||||
{/* @ts-ignore */}
|
||||
<Conference { ...props } />
|
||||
</div>
|
||||
);
|
||||
}));
|
||||
226
react/features/conference/components/web/ConferenceInfo.tsx
Normal file
226
react/features/conference/components/web/ConferenceInfo.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import E2EELabel from '../../../e2ee/components/E2EELabel';
|
||||
import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/web/RecordingLabel';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
|
||||
import VisitorsCountLabel from '../../../visitors/components/web/VisitorsCountLabel';
|
||||
import ConferenceTimer from '../ConferenceTimer';
|
||||
import { getConferenceInfo } from '../functions.web';
|
||||
|
||||
import ConferenceInfoContainer from './ConferenceInfoContainer';
|
||||
import InsecureRoomNameLabel from './InsecureRoomNameLabel';
|
||||
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
|
||||
import SpeakerStatsLabel from './SpeakerStatsLabel';
|
||||
import SubjectText from './SubjectText';
|
||||
import ToggleTopPanelLabel from './ToggleTopPanelLabel';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Subject}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The conference info labels to be shown in the conference header.
|
||||
*/
|
||||
_conferenceInfo: {
|
||||
alwaysVisible?: string[];
|
||||
autoHide?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates whether the component should be visible or not.
|
||||
*/
|
||||
_visible: boolean;
|
||||
|
||||
/**
|
||||
* Invoked to active other features of the app.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
const COMPONENTS: Array<{
|
||||
Component: React.ComponentType<any>;
|
||||
id: string;
|
||||
}> = [
|
||||
{
|
||||
Component: HighlightButton,
|
||||
id: 'highlight-moment'
|
||||
},
|
||||
{
|
||||
Component: SubjectText,
|
||||
id: 'subject'
|
||||
},
|
||||
{
|
||||
Component: ConferenceTimer,
|
||||
id: 'conference-timer'
|
||||
},
|
||||
{
|
||||
Component: SpeakerStatsLabel,
|
||||
id: 'participants-count'
|
||||
},
|
||||
{
|
||||
Component: E2EELabel,
|
||||
id: 'e2ee'
|
||||
},
|
||||
{
|
||||
Component: () => (
|
||||
<>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</>
|
||||
),
|
||||
id: 'recording'
|
||||
},
|
||||
{
|
||||
Component: RaisedHandsCountLabel,
|
||||
id: 'raised-hands-count'
|
||||
},
|
||||
{
|
||||
Component: VideoQualityLabel,
|
||||
id: 'video-quality'
|
||||
},
|
||||
{
|
||||
Component: VisitorsCountLabel,
|
||||
id: 'visitors-count'
|
||||
},
|
||||
{
|
||||
Component: InsecureRoomNameLabel,
|
||||
id: 'insecure-room'
|
||||
},
|
||||
{
|
||||
Component: ToggleTopPanelLabel,
|
||||
id: 'top-panel-toggle'
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* The upper band of the meeing containing the conference name, timer and labels.
|
||||
*
|
||||
* @param {Object} props - The props of the component.
|
||||
* @returns {React$None}
|
||||
*/
|
||||
class ConferenceInfo extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code ConferenceInfo} instance.
|
||||
*
|
||||
* @param {IProps} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._renderAutoHide = this._renderAutoHide.bind(this);
|
||||
this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
|
||||
this._onTabIn = this._onTabIn.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the component is focused to show the conference
|
||||
* info if necessary.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTabIn() {
|
||||
if (this.props._conferenceInfo.autoHide?.length && !this.props._visible) {
|
||||
this.props.dispatch(showToolbox());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders auto-hidden info header labels.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderAutoHide() {
|
||||
const { autoHide } = this.props._conferenceInfo;
|
||||
|
||||
if (!autoHide?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer
|
||||
id = 'autoHide'
|
||||
visible = { this.props._visible }>
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => autoHide.includes(comp.id))
|
||||
.map(c =>
|
||||
<c.Component key = { c.id } />
|
||||
)
|
||||
}
|
||||
</ConferenceInfoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the always visible info header labels.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_renderAlwaysVisible() {
|
||||
const { alwaysVisible } = this.props._conferenceInfo;
|
||||
|
||||
if (!alwaysVisible?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConferenceInfoContainer
|
||||
id = 'alwaysVisible'
|
||||
visible = { true } >
|
||||
{
|
||||
COMPONENTS
|
||||
.filter(comp => alwaysVisible.includes(comp.id))
|
||||
.map(c =>
|
||||
<c.Component key = { c.id } />
|
||||
)
|
||||
}
|
||||
</ConferenceInfoContainer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<div
|
||||
className = 'details-container'
|
||||
onFocus = { this._onTabIn }>
|
||||
{ this._renderAlwaysVisible() }
|
||||
{ this._renderAutoHide() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code Subject}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _visible: boolean,
|
||||
* _conferenceInfo: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_visible: isToolboxVisible(state),
|
||||
_conferenceInfo: getConferenceInfo(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(ConferenceInfo);
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import { isAlwaysOnTitleBarEmpty } from '../functions.web';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The children components.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Id of the component.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Whether this conference info container should be visible or not.
|
||||
*/
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export default ({ visible, children, id }: IProps) => (
|
||||
<div
|
||||
className = { `subject${isAlwaysOnTitleBarEmpty() ? '' : ' with-always-on'}${visible ? ' visible' : ''}` }
|
||||
id = { id }>
|
||||
<div className = { 'subject-info-container' }>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IDisplayProps } from '../ConferenceTimer';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
timer: {
|
||||
...theme.typography.labelRegular,
|
||||
color: theme.palette.text01,
|
||||
padding: '6px 8px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
boxSizing: 'border-box',
|
||||
height: '28px',
|
||||
borderRadius: `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`,
|
||||
marginRight: '2px',
|
||||
|
||||
'@media (max-width: 300px)': {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns web element to be rendered.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function ConferenceTimerDisplay({ timerValue, textStyle: _textStyle }: IDisplayProps) {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<span className = { classes.timer }>{ timerValue }</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconExclamationTriangle } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../../base/label/constants';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import getUnsafeRoomText from '../../../base/util/getUnsafeRoomText.web';
|
||||
import AbstractInsecureRoomNameLabel, { _mapStateToProps } from '../AbstractInsecureRoomNameLabel';
|
||||
|
||||
/**
|
||||
* Renders a label indicating that we are in a room with an insecure name.
|
||||
*/
|
||||
class InsecureRoomNameLabel extends AbstractInsecureRoomNameLabel {
|
||||
/**
|
||||
* Renders the platform dependent content.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _render() {
|
||||
return (
|
||||
<Tooltip
|
||||
content = { getUnsafeRoomText(this.props.t, 'meeting') }
|
||||
position = 'bottom'>
|
||||
<Label
|
||||
color = { COLORS.red }
|
||||
icon = { IconExclamationTriangle } />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(InsecureRoomNameLabel));
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialog: {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
text: {
|
||||
fontSize: '1.25rem'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link LeaveReasonDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code LeaveReasonDialog} is unmounted.
|
||||
*/
|
||||
onClose: () => void;
|
||||
|
||||
/**
|
||||
* The title to display in the dialog.
|
||||
*/
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A React {@code Component} for displaying a dialog with a reason that ended the conference.
|
||||
*
|
||||
* @param {IProps} props - Component's props.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
const LeaveReasonDialog = ({ onClose, title }: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => () => {
|
||||
onClose?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
onSubmit = { onClose }
|
||||
size = 'medium'
|
||||
testId = 'dialog.leaveReason'>
|
||||
<div className = { classes.dialog }>
|
||||
{title ? <div className = { classes.text }>{t(title)}</div> : null}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default LeaveReasonDialog;
|
||||
43
react/features/conference/components/web/Notice.tsx
Normal file
43
react/features/conference/components/web/Notice.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
notice: {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
zIndex: 3,
|
||||
marginTop: theme.spacing(2),
|
||||
transform: 'translateX(-50%)'
|
||||
},
|
||||
|
||||
message: {
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
color: theme.palette.text01,
|
||||
padding: '3px',
|
||||
borderRadius: '5px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Notice = () => {
|
||||
const message = useSelector((state: IReduxState) => state['features/base/config'].noticeMessage);
|
||||
const { classes } = useStyles();
|
||||
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.notice }>
|
||||
<span className = { classes.message } >
|
||||
{message}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notice;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { open as openParticipantsPane } from '../../../participants-pane/actions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
label: {
|
||||
backgroundColor: theme.palette.warning02,
|
||||
color: theme.palette.uiBackground
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const RaisedHandsCountLabel = () => {
|
||||
const { classes: styles, theme } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const raisedHandsCount = useSelector((state: IReduxState) =>
|
||||
(state['features/base/participants'].raisedHandsQueue || []).length);
|
||||
const { t } = useTranslation();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(openParticipantsPane());
|
||||
}, []);
|
||||
|
||||
return raisedHandsCount > 0 ? (<Tooltip
|
||||
content = { t('raisedHandsLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
accessibilityText = { t('raisedHandsLabel') }
|
||||
className = { styles.label }
|
||||
icon = { IconRaiseHand }
|
||||
iconColor = { theme.palette.icon04 }
|
||||
id = 'raisedHandsCountLabel'
|
||||
onClick = { onClick }
|
||||
text = { `${raisedHandsCount}` } />
|
||||
</Tooltip>) : null;
|
||||
};
|
||||
|
||||
export default RaisedHandsCountLabel;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../../base/label/constants';
|
||||
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import SpeakerStats from '../../../speaker-stats/components/web/SpeakerStats';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
|
||||
/**
|
||||
* ParticipantsCount react component.
|
||||
* Displays the number of participants and opens Speaker stats on click.
|
||||
*
|
||||
* @class ParticipantsCount
|
||||
*/
|
||||
function SpeakerStatsLabel() {
|
||||
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
|
||||
const count = useSelector(getParticipantCountForDisplay);
|
||||
const _isSpeakerStatsDisabled = useSelector(isSpeakerStatsDisabled);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(openDialog(SpeakerStats, { conference }));
|
||||
};
|
||||
|
||||
if (count <= 2 || _isSpeakerStatsDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { t('speakerStats.labelTooltip', { count }) }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
color = { COLORS.white }
|
||||
icon = { IconUsers }
|
||||
iconColor = '#fff'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { onClick }
|
||||
text = { `${count}` } />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default SpeakerStatsLabel;
|
||||
54
react/features/conference/components/web/SubjectText.tsx
Normal file
54
react/features/conference/components/web/SubjectText.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { getConferenceName } from '../../../base/conference/functions';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
...theme.typography.bodyLongRegular,
|
||||
color: theme.palette.text01,
|
||||
padding: '2px 16px',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
maxWidth: '324px',
|
||||
boxSizing: 'border-box',
|
||||
height: '28px',
|
||||
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
|
||||
marginLeft: '2px',
|
||||
|
||||
'@media (max-width: 300px)': {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Label for the conference name.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const SubjectText = () => {
|
||||
const subject = useSelector(getConferenceName);
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { subject }
|
||||
position = 'bottom'>
|
||||
<div className = { classes.container }>
|
||||
<div className = { clsx('subject-text--content', classes.content) }>{subject}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubjectText;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IconArrowDown } from '../../../base/icons/svg/index';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { setTopPanelVisible } from '../../../filmstrip/actions.web';
|
||||
|
||||
const ToggleTopPanelLabel = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const topPanelHidden = !useSelector((state: IReduxState) => state['features/filmstrip'].topPanelVisible);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setTopPanelVisible(true));
|
||||
}, []);
|
||||
|
||||
return topPanelHidden ? (<Tooltip
|
||||
content = { t('toggleTopPanelLabel') }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
icon = { IconArrowDown }
|
||||
onClick = { onClick } />
|
||||
</Tooltip>) : null;
|
||||
};
|
||||
|
||||
export default ToggleTopPanelLabel;
|
||||
12
react/features/conference/constants.ts
Normal file
12
react/features/conference/constants.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IFRAME_EMBED_ALLOWED_LOCATIONS as ADDITIONAL_LOCATIONS } from './extraConstants';
|
||||
|
||||
/**
|
||||
* Timeout of the conference when iframe is disabled in minutes.
|
||||
*/
|
||||
export const IFRAME_DISABLED_TIMEOUT_MINUTES = 5;
|
||||
|
||||
/**
|
||||
* A list of allowed location to embed iframe.
|
||||
*/
|
||||
/* eslint-disable-next-line no-extra-parens */
|
||||
export const IFRAME_EMBED_ALLOWED_LOCATIONS = ([] as string[]).concat(ADDITIONAL_LOCATIONS);
|
||||
5
react/features/conference/extraConstants.ts
Normal file
5
react/features/conference/extraConstants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Deploy-specific configuration constants.
|
||||
*/
|
||||
|
||||
export const IFRAME_EMBED_ALLOWED_LOCATIONS = [];
|
||||
34
react/features/conference/functions.any.ts
Normal file
34
react/features/conference/functions.any.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
|
||||
/**
|
||||
* Tells whether or not the notifications should be displayed within
|
||||
* the conference feature based on the current Redux state.
|
||||
*
|
||||
* @param {Object|Function} stateful - The redux store state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function shouldDisplayNotifications(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { calleeInfoVisible } = state['features/invite'];
|
||||
|
||||
return !calleeInfoVisible;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Returns true if polls feature is disabled.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/config.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function arePollsDisabled(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return state['features/base/config']?.disablePolls || iAmVisitor(state);
|
||||
}
|
||||
1
react/features/conference/functions.native.ts
Normal file
1
react/features/conference/functions.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './functions.any';
|
||||
33
react/features/conference/functions.web.ts
Normal file
33
react/features/conference/functions.web.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { isSuboptimalBrowser } from '../base/environment/environment';
|
||||
import { translateToHTML } from '../base/i18n/functions';
|
||||
import { showWarningNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Shows the suboptimal experience notification if needed.
|
||||
*
|
||||
* @param {Function} dispatch - The dispatch method.
|
||||
* @param {Function} t - The translation function.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function maybeShowSuboptimalExperienceNotification(dispatch: IStore['dispatch'], t: Function) {
|
||||
if (isSuboptimalBrowser()) {
|
||||
dispatch(
|
||||
showWarningNotification(
|
||||
{
|
||||
titleKey: 'notify.suboptimalExperienceTitle',
|
||||
description: translateToHTML(
|
||||
t,
|
||||
'notify.suboptimalBrowserWarning',
|
||||
{
|
||||
recommendedBrowserPageLink: `${window.location.origin}/static/recommendedBrowsers.html`
|
||||
}
|
||||
)
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
3
react/features/conference/logger.ts
Normal file
3
react/features/conference/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/conference');
|
||||
350
react/features/conference/middleware.any.ts
Normal file
350
react/features/conference/middleware.any.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import i18n from 'i18next';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import { API_ID } from '../../../modules/API/constants';
|
||||
import { appNavigate } from '../app/actions';
|
||||
import { redirectToStaticPage } from '../app/actions.any';
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getDisableLowerHandByModerator } from '../base/config/functions.any';
|
||||
import { hangup } from '../base/connection/actions';
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
|
||||
import { hideDialog } from '../base/dialog/actions';
|
||||
import { isDialogOpen } from '../base/dialog/functions';
|
||||
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
import { translateToHTML } from '../base/i18n/functions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
import { pinParticipant, raiseHand, raiseHandClear } from '../base/participants/actions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
|
||||
import { LOWER_HAND_MESSAGE } from '../base/tracks/constants';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
import { isEmbedded } from '../base/util/embedUtils';
|
||||
import { isCalendarEnabled } from '../calendar-sync/functions';
|
||||
import FeedbackDialog from '../feedback/components/FeedbackDialog';
|
||||
import { setFilmstripEnabled } from '../filmstrip/actions.any';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
import { hideNotification, showNotification, showWarningNotification } from '../notifications/actions';
|
||||
import {
|
||||
CALENDAR_NOTIFICATION_ID,
|
||||
NOTIFICATION_ICON,
|
||||
NOTIFICATION_TIMEOUT_TYPE
|
||||
} from '../notifications/constants';
|
||||
import { showStartRecordingNotification } from '../recording/actions';
|
||||
import { showSalesforceNotification } from '../salesforce/actions';
|
||||
import { setToolboxEnabled } from '../toolbox/actions.any';
|
||||
|
||||
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
|
||||
import { dismissCalendarNotification } from './actions';
|
||||
import { IFRAME_DISABLED_TIMEOUT_MINUTES, IFRAME_EMBED_ALLOWED_LOCATIONS } from './constants';
|
||||
|
||||
|
||||
let intervalID: any;
|
||||
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
_conferenceJoined(store);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_REDUCED_UI: {
|
||||
_setReducedUI(store);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case DISMISS_CALENDAR_NOTIFICATION:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_FAILED: {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
if (data.name === LOWER_HAND_MESSAGE
|
||||
&& participant.isModerator()
|
||||
&& !getDisableLowerHandByModerator(getState())) {
|
||||
dispatch(raiseHand(false));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, close all dialogs and unpin any pinned participants.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch, getState }, prevConference) => {
|
||||
const { authRequired, membersOnly, passwordRequired }
|
||||
= getState()['features/base/conference'];
|
||||
|
||||
if (conference !== prevConference) {
|
||||
// Unpin participant, in order to avoid the local participant
|
||||
// remaining pinned, since it's not destroyed across runs.
|
||||
dispatch(pinParticipant(null));
|
||||
|
||||
// Clear raised hands.
|
||||
dispatch(raiseHandClear());
|
||||
|
||||
// XXX I wonder if there is a better way to do this. At this stage
|
||||
// we do know what dialogs we want to keep but the list of those
|
||||
// we want to hide is a lot longer. Thus we take a bit of a shortcut
|
||||
// and explicitly check.
|
||||
if (typeof authRequired === 'undefined'
|
||||
&& typeof passwordRequired === 'undefined'
|
||||
&& typeof membersOnly === 'undefined'
|
||||
&& !isDialogOpen(getState(), FeedbackDialog)) {
|
||||
// Conference changed, left or failed... and there is no
|
||||
// pending authentication, nor feedback request, so close any
|
||||
// dialog we might have open.
|
||||
dispatch(hideDialog());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Configures the UI. In reduced UI mode some components will
|
||||
* be hidden if there is no space to render them.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setReducedUI({ dispatch, getState }: IStore) {
|
||||
const { reducedUI } = getState()['features/base/responsive-ui'];
|
||||
|
||||
dispatch(setToolboxEnabled(!reducedUI));
|
||||
dispatch(setFilmstripEnabled(!reducedUI));
|
||||
}
|
||||
|
||||
/**
|
||||
* Does extra sync up on properties that may need to be updated after the
|
||||
* conference was joined.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _conferenceJoined({ dispatch, getState }: IStore) {
|
||||
_setReducedUI({
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
|
||||
if (!intervalID) {
|
||||
intervalID = setInterval(() =>
|
||||
_maybeDisplayCalendarNotification({
|
||||
dispatch,
|
||||
getState
|
||||
}), 10 * 1000);
|
||||
}
|
||||
|
||||
dispatch(showSalesforceNotification());
|
||||
dispatch(showStartRecordingNotification());
|
||||
|
||||
_checkIframe(getState(), dispatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional checks for embedding in iframe.
|
||||
*
|
||||
* @param {IReduxState} state - The current state of the app.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _checkIframe(state: IReduxState, dispatch: IStore['dispatch']) {
|
||||
let allowIframe = false;
|
||||
|
||||
if (document.referrer === '' && browser.isElectron()) {
|
||||
// no iframe
|
||||
allowIframe = true;
|
||||
} else {
|
||||
try {
|
||||
allowIframe = IFRAME_EMBED_ALLOWED_LOCATIONS.includes(new URL(document.referrer).hostname);
|
||||
} catch (e) {
|
||||
// wrong URL in referrer
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmbedded() && state['features/base/config'].disableIframeAPI && !isVpaasMeeting(state) && !allowIframe) {
|
||||
// show sticky notification and redirect in 5 minutes
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
let translationKey = 'notify.disabledIframe';
|
||||
const hostname = locationURL?.hostname ?? '';
|
||||
let domain = '';
|
||||
|
||||
const mapping: Record<string, string> = {
|
||||
'8x8.vc': 'https://jaas.8x8.vc',
|
||||
'meet.jit.si': 'https://jitsi.org/jaas'
|
||||
};
|
||||
|
||||
const jaasDomain = mapping[hostname];
|
||||
|
||||
if (jaasDomain) {
|
||||
translationKey = `notify.disabledIframeSecondary${browser.isReactNative() ? 'Native' : 'Web'}`;
|
||||
domain = hostname;
|
||||
}
|
||||
|
||||
if (browser.isReactNative()) {
|
||||
dispatch(showWarningNotification({
|
||||
description: i18next.t(translationKey,
|
||||
{
|
||||
domain,
|
||||
timeout: IFRAME_DISABLED_TIMEOUT_MINUTES
|
||||
}
|
||||
)
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(hangup());
|
||||
}, IFRAME_DISABLED_TIMEOUT_MINUTES * 60 * 1000);
|
||||
} else {
|
||||
dispatch(showWarningNotification({
|
||||
description: translateToHTML(
|
||||
i18next.t.bind(i18next),
|
||||
translationKey,
|
||||
{
|
||||
domain,
|
||||
jaasDomain,
|
||||
timeout: IFRAME_DISABLED_TIMEOUT_MINUTES
|
||||
}
|
||||
)
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
setTimeout(() => {
|
||||
// redirect to the promotional page
|
||||
dispatch(redirectToStaticPage('static/close3.html', `#jitsi_meet_external_api_id=${API_ID}`));
|
||||
}, IFRAME_DISABLED_TIMEOUT_MINUTES * 60 * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically checks if there is an event in the calendar for which we
|
||||
* need to show a notification.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybeDisplayCalendarNotification({ dispatch, getState }: IStore) {
|
||||
const state = getState();
|
||||
|
||||
const calendarEnabled = isCalendarEnabled(state);
|
||||
const { events: eventList } = state['features/calendar-sync'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const { reducedUI } = state['features/base/responsive-ui'];
|
||||
|
||||
const currentConferenceURL
|
||||
= locationURL ? getURLWithoutParamsNormalized(locationURL) : '';
|
||||
const ALERT_MILLISECONDS = 5 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
let eventToShow;
|
||||
|
||||
if (!calendarEnabled && reducedUI) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventList?.length) {
|
||||
|
||||
for (const event of eventList) {
|
||||
const eventURL
|
||||
= event?.url && getURLWithoutParamsNormalized(new URL(event.url));
|
||||
|
||||
if (eventURL && eventURL !== currentConferenceURL) {
|
||||
// @ts-ignore
|
||||
if ((!eventToShow && event.startDate > now && event.startDate < now + ALERT_MILLISECONDS)
|
||||
|
||||
// @ts-ignore
|
||||
|| (event.startDate < now && event.endDate > now)) {
|
||||
eventToShow = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_calendarNotification(
|
||||
{
|
||||
dispatch,
|
||||
getState
|
||||
}, eventToShow
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar notification.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {eventToShow} eventToShow - Next or ongoing event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _calendarNotification({ dispatch, getState }: IStore, eventToShow: any) {
|
||||
const state = getState();
|
||||
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
const currentConferenceURL
|
||||
= locationURL ? getURLWithoutParamsNormalized(locationURL) : '';
|
||||
const now = Date.now();
|
||||
|
||||
if (!eventToShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customActionNameKey = [ 'notify.joinMeeting', 'notify.dontRemindMe' ];
|
||||
const customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
|
||||
const customActionHandler = [ () => batch(() => {
|
||||
dispatch(hideNotification(CALENDAR_NOTIFICATION_ID));
|
||||
if (eventToShow?.url && (eventToShow.url !== currentConferenceURL)) {
|
||||
dispatch(appNavigate(eventToShow.url));
|
||||
}
|
||||
}), () => dispatch(dismissCalendarNotification()) ];
|
||||
const description
|
||||
= getLocalizedDateFormatter(eventToShow.startDate).fromNow();
|
||||
const icon = NOTIFICATION_ICON.WARNING;
|
||||
const title = (eventToShow.startDate < now) && (eventToShow.endDate > now)
|
||||
? `${i18n.t('calendarSync.ongoingMeeting')}: \n${eventToShow.title}`
|
||||
: `${i18n.t('calendarSync.nextMeeting')}: \n${eventToShow.title}`;
|
||||
const uid = CALENDAR_NOTIFICATION_ID;
|
||||
|
||||
dispatch(showNotification({
|
||||
customActionHandler,
|
||||
customActionNameKey,
|
||||
customActionType,
|
||||
description,
|
||||
icon,
|
||||
maxLines: 1,
|
||||
title,
|
||||
uid
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
28
react/features/conference/middleware.native.ts
Normal file
28
react/features/conference/middleware.native.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { appNavigate } from '../app/actions.native';
|
||||
import { KICKED_OUT } from '../base/conference/actionTypes';
|
||||
import { conferenceLeft } from '../base/conference/actions.native';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { notifyKickedOut } from './actions.native';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case KICKED_OUT: {
|
||||
const { dispatch } = store;
|
||||
|
||||
dispatch(notifyKickedOut(
|
||||
action.participant,
|
||||
() => {
|
||||
dispatch(conferenceLeft(action.conference));
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
46
react/features/conference/middleware.web.ts
Normal file
46
react/features/conference/middleware.web.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import i18next from 'i18next';
|
||||
|
||||
import { ENDPOINT_MESSAGE_RECEIVED, KICKED_OUT } from '../base/conference/actionTypes';
|
||||
import { hangup } from '../base/connection/actions.web';
|
||||
import { getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { openAllowToggleCameraDialog, setCameraFacingMode } from '../base/tracks/actions.web';
|
||||
import { CAMERA_FACING_MODE_MESSAGE } from '../base/tracks/constants';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
|
||||
if (data?.name === CAMERA_FACING_MODE_MESSAGE) {
|
||||
APP.store.dispatch(openAllowToggleCameraDialog(
|
||||
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(data.facingMode)),
|
||||
/* initiatorId */ participant.getId()
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KICKED_OUT: {
|
||||
const { dispatch } = store;
|
||||
const { participant } = action;
|
||||
|
||||
// we need to first finish dispatching or the notification can be cleared out
|
||||
const result = next(action);
|
||||
|
||||
const participantDisplayName
|
||||
= participant && getParticipantDisplayName(store.getState, participant.getId());
|
||||
|
||||
dispatch(hangup(true,
|
||||
participantDisplayName ? i18next.t('dialog.kickTitle', { participantDisplayName })
|
||||
: i18next.t('dialog.kickSystemTitle'),
|
||||
true));
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
Reference in New Issue
Block a user