This commit is contained in:
@@ -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]
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user