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