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