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

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

View File

@@ -0,0 +1,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;

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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