Refactor carmnode

- open as a modal
- add long press to talk
- add long press analytics
- video mute keeps the previous state after carmode closes
- and more
This commit is contained in:
Horatiu Muresan 2022-05-05 13:00:36 +03:00
parent ac965b86bc
commit 698ab32a62
19 changed files with 289 additions and 367 deletions

View File

@ -83,6 +83,7 @@
"selectSoundDevice": "Select sound device" "selectSoundDevice": "Select sound device"
}, },
"labels": { "labels": {
"buttonLabel": "Car mode",
"title": "Safe driving mode", "title": "Safe driving mode",
"videoStopped": "Your video is stopped" "videoStopped": "Your video is stopped"
} }
@ -1050,6 +1051,7 @@
"muteEveryoneElse": "Mute everyone else", "muteEveryoneElse": "Mute everyone else",
"muteEveryoneElsesVideoStream": "Stop everyone else's video", "muteEveryoneElsesVideoStream": "Stop everyone else's video",
"muteEveryonesVideoStream": "Stop everyone's video", "muteEveryonesVideoStream": "Stop everyone's video",
"carmode": "Carmode",
"participants": "Participants", "participants": "Participants",
"pip": "Toggle Picture-in-Picture mode", "pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message", "privateMessage": "Send private message",

View File

@ -641,18 +641,20 @@ export function createSharedVideoEvent(action, attributes = {}) {
* of ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED * of ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED
* or ACTION_SHORTCUT_TRIGGERED). * or ACTION_SHORTCUT_TRIGGERED).
* @param {Object} attributes - Attributes to attach to the event. * @param {Object} attributes - Attributes to attach to the event.
* @param {string} source - The event's source.
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.
*/ */
export function createShortcutEvent( export function createShortcutEvent(
shortcut, shortcut,
action = ACTION_SHORTCUT_TRIGGERED, action = ACTION_SHORTCUT_TRIGGERED,
attributes = {}) { attributes = {},
source = 'keyboard.shortcut') {
return { return {
action, action,
actionSubjectId: shortcut, actionSubjectId: shortcut,
attributes, attributes,
source: 'keyboard.shortcut', source,
type: TYPE_UI type: TYPE_UI
}; };
} }
@ -901,7 +903,7 @@ export function createBreakoutRoomsEvent(actionSubject) {
} }
/** /**
* Creates and event which indicates a GIF was sent. * Creates an event which indicates a GIF was sent.
* *
* @returns {Object} The event in a format suitable for sending via * @returns {Object} The event in a format suitable for sending via
* sendAnalytics. * sendAnalytics.

View File

@ -166,7 +166,7 @@ export function setVideoAvailable(available: boolean) {
*/ */
export function setVideoMuted( export function setVideoMuted(
muted: boolean, muted: boolean,
mediaType: MediaType = MEDIA_TYPE.VIDEO, mediaType: string = MEDIA_TYPE.VIDEO,
authority: number = VIDEO_MUTISM_AUTHORITY.USER, authority: number = VIDEO_MUTISM_AUTHORITY.USER,
ensureTrack: boolean = false) { ensureTrack: boolean = false) {
return (dispatch: Dispatch<any>, getState: Function) => { return (dispatch: Dispatch<any>, getState: Function) => {

View File

@ -45,7 +45,8 @@ export const SCREENSHARE_MUTISM_AUTHORITY = {
export const VIDEO_MUTISM_AUTHORITY = { export const VIDEO_MUTISM_AUTHORITY = {
AUDIO_ONLY: 1 << 0, AUDIO_ONLY: 1 << 0,
BACKGROUND: 1 << 1, BACKGROUND: 1 << 1,
USER: 1 << 2 USER: 1 << 2,
CAR_MODE: 1 << 3
}; };
/* eslint-enable no-bitwise */ /* eslint-enable no-bitwise */

View File

@ -16,7 +16,6 @@ import { PollsPane } from '../../../polls/components';
const ChatTab = createMaterialTopTabNavigator(); const ChatTab = createMaterialTopTabNavigator();
const ChatAndPolls = () => { const ChatAndPolls = () => {
const clientHeight = useSelector(getClientHeight); const clientHeight = useSelector(getClientHeight);
const clientWidth = useSelector(getClientWidth); const clientWidth = useSelector(getClientWidth);

View File

@ -1,176 +0,0 @@
import { TypedNavigator, useIsFocused } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { Chat, ChatAndPolls } from '../../../chat';
import { SharedDocument } from '../../../etherpad';
import { GifsMenu } from '../../../gifs/components';
import AddPeopleDialog
from '../../../invite/components/add-people-dialog/native/AddPeopleDialog';
import LobbyNavigationContainer
from '../../../mobile/navigation/components/lobby/components/LobbyNavigationContainer';
import { screen } from '../../../mobile/navigation/routes';
import {
chatScreenOptions,
conferenceScreenOptions,
gifsMenuOptions,
inviteScreenOptions,
liveStreamScreenOptions,
participantsScreenOptions,
recordingScreenOptions,
salesforceScreenOptions,
securityScreenOptions,
sharedDocumentScreenOptions,
speakerStatsScreenOptions
} from '../../../mobile/navigation/screenOptions';
import { ParticipantsPane } from '../../../participants-pane/components/native';
import { StartLiveStreamDialog } from '../../../recording';
import { StartRecordingDialog }
from '../../../recording/components/Recording/native';
import SalesforceLinkDialog
from '../../../salesforce/components/native/SalesforceLinkDialog';
import SecurityDialog
from '../../../security/components/security-dialog/native/SecurityDialog';
import SpeakerStats
from '../../../speaker-stats/components/native/SpeakerStats';
import { setIsCarmode } from '../../../video-layout/actions';
import { getDisablePolls } from '../../functions';
import Conference from './Conference';
const ConferenceStack : TypedNavigator = createStackNavigator();
type Props = {
/**
* Callback on component focused.
* Passes the route name to the embedder.
*/
onFocused: Function
}
/**
* The main conference screen navigator.
*
* @param {Props} props - The React props passed to this component.
* @returns {JSX.Element} - The conference tab.
*/
const ConferenceTab = ({ onFocused }: Props) : JSX.Element => {
const isFocused = useIsFocused();
const dispatch = useDispatch();
const isPollsDisabled = useSelector(getDisablePolls);
let ChatScreen;
let chatScreenName;
let chatTitleString;
if (isPollsDisabled) {
ChatScreen = Chat;
chatScreenName = screen.conference.chat;
chatTitleString = 'chat.title';
} else {
ChatScreen = ChatAndPolls;
chatScreenName = screen.conference.chatandpolls.main;
chatTitleString = 'chat.titleWithPolls';
}
const { t } = useTranslation();
useEffect(() => {
if (isFocused) {
dispatch(setIsCarmode(false));
onFocused(screen.conference.container);
}
}, [ isFocused ]);
return (
<ConferenceStack.Navigator
initialRouteName = { screen.conference.main }
screenOptions = {{
presentation: 'modal'
}}>
<ConferenceStack.Screen
component = { Conference }
name = { screen.conference.main }
options = { conferenceScreenOptions } />
<ConferenceStack.Screen
component = { ChatScreen }
name = { chatScreenName }
options = {{
...chatScreenOptions,
title: t(chatTitleString)
}} />
<ConferenceStack.Screen
component = { ParticipantsPane }
name = { screen.conference.participants }
options = {{
...participantsScreenOptions,
title: t('participantsPane.header')
}} />
<ConferenceStack.Screen
component = { SecurityDialog }
name = { screen.conference.security }
options = {{
...securityScreenOptions,
title: t('security.header')
}} />
<ConferenceStack.Screen
component = { StartRecordingDialog }
name = { screen.conference.recording }
options = {{
...recordingScreenOptions
}} />
<ConferenceStack.Screen
component = { StartLiveStreamDialog }
name = { screen.conference.liveStream }
options = {{
...liveStreamScreenOptions
}} />
<ConferenceStack.Screen
component = { SpeakerStats }
name = { screen.conference.speakerStats }
options = {{
...speakerStatsScreenOptions,
title: t('speakerStats.speakerStats')
}} />
<ConferenceStack.Screen
component = { SalesforceLinkDialog }
name = { screen.conference.salesforce }
options = {{
...salesforceScreenOptions,
title: t('notify.linkToSalesforce')
}} />
<ConferenceStack.Screen
component = { GifsMenu }
name = { screen.conference.gifsMenu }
options = {{
...gifsMenuOptions,
title: t('notify.gifsMenu')
}} />
<ConferenceStack.Screen
component = { LobbyNavigationContainer }
name = { screen.lobby.root }
options = {{
gestureEnabled: false,
headerShown: false
}} />
<ConferenceStack.Screen
component = { AddPeopleDialog }
name = { screen.conference.invite }
options = {{
...inviteScreenOptions,
title: t('addPeople.add')
}} />
<ConferenceStack.Screen
component = { SharedDocument }
name = { screen.conference.sharedDocument }
options = {{
...sharedDocumentScreenOptions,
title: t('documentSharing.title')
}} />
</ConferenceStack.Navigator>
);
};
export default ConferenceTab;

View File

@ -1,13 +1,11 @@
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Text, SafeAreaView, View } from 'react-native'; import { Text, SafeAreaView, View } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context'; import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { batch, useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import { setAudioOnly } from '../../../../base/audio-only'; import { LoadingIndicator, TintedView } from '../../../../base/react';
import { Container, LoadingIndicator, TintedView } from '../../../../base/react';
import { screen } from '../../../../mobile/navigation/routes';
import { setIsCarmode } from '../../../../video-layout/actions'; import { setIsCarmode } from '../../../../video-layout/actions';
import ConferenceTimer from '../../ConferenceTimer'; import ConferenceTimer from '../../ConferenceTimer';
import { isConnecting } from '../../functions'; import { isConnecting } from '../../functions';
@ -17,42 +15,34 @@ import MicrophoneButton from './MicrophoneButton';
import SoundDeviceButton from './SoundDeviceButton'; import SoundDeviceButton from './SoundDeviceButton';
import TitleBar from './TitleBar'; import TitleBar from './TitleBar';
import styles from './styles'; import styles from './styles';
import { isLocalVideoTrackDesktop } from '../../../../base/tracks';
type Props = { import { setPictureInPictureDisabled } from '../../../../mobile/picture-in-picture/functions';
/**
* Callback on component focused.
* Passes the route name to the embedder.
*/
onFocused: Function
}
/** /**
* Implements the carmode tab. * Implements the carmode tab.
* *
* @param { Props } - - The component's props.
* @returns { JSX.Element} - The carmode tab. * @returns { JSX.Element} - The carmode tab.
*/ */
const CarmodeTab = ({ onFocused }: Props): JSX.Element => { const CarmodeTab = (): JSX.Element => {
const isFocused = useIsFocused();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const connecting = useSelector(isConnecting); const connecting = useSelector(isConnecting);
const isSharing = useSelector(isLocalVideoTrackDesktop);
useEffect(() => { useEffect(() => {
if (isFocused) {
batch(() => {
dispatch(setAudioOnly(true));
dispatch(setIsCarmode(true)); dispatch(setIsCarmode(true));
}); setPictureInPictureDisabled(true);
onFocused(screen.car); return () => {
dispatch(setIsCarmode(false));
if (!isSharing) {
setPictureInPictureDisabled(false);
} }
}, [ isFocused ]); }
}, []);
return ( return (
<Container style = { styles.conference }> <JitsiScreen style = { styles.conference }>
{/* {/*
* The activity/loading indicator goes above everything, except * The activity/loading indicator goes above everything, except
* the toolbox/toolbars and the dialogs. * the toolbox/toolbars and the dialogs.
@ -62,22 +52,21 @@ const CarmodeTab = ({ onFocused }: Props): JSX.Element => {
<LoadingIndicator /> <LoadingIndicator />
</TintedView> </TintedView>
} }
<SafeAreaView <View
pointerEvents = 'box-none' pointerEvents = 'box-none'
style = { styles.titleBarSafeViewColor }> style = { styles.titleBarSafeViewColor }>
<View style = { styles.titleView }>
<Text style = { styles.title }>
{t('carmode.labels.title')}
</Text>
</View>
<View <View
style = { styles.titleBar }> style = { styles.titleBar }>
<TitleBar /> <TitleBar />
</View> </View>
<ConferenceTimer textStyle = { styles.roomTimer } /> <ConferenceTimer textStyle = { styles.roomTimer } />
</SafeAreaView> </View>
<View
pointerEvents = 'box-none'
style = { styles.microphoneContainer }>
<MicrophoneButton /> <MicrophoneButton />
<SafeAreaView </View>
<View
pointerEvents = 'box-none' pointerEvents = 'box-none'
style = { styles.bottomContainer }> style = { styles.bottomContainer }>
<Text style = { styles.videoStoppedLabel }> <Text style = { styles.videoStoppedLabel }>
@ -85,8 +74,8 @@ const CarmodeTab = ({ onFocused }: Props): JSX.Element => {
</Text> </Text>
<SoundDeviceButton /> <SoundDeviceButton />
<EndMeetingButton /> <EndMeetingButton />
</SafeAreaView> </View>
</Container> </JitsiScreen>
); );
}; };

View File

@ -1,6 +1,13 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useState } from 'react';
import { View, TouchableOpacity } from 'react-native'; import { View, TouchableOpacity } from 'react-native';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import {
createShortcutEvent,
sendAnalytics,
ACTION_SHORTCUT_PRESSED as PRESSED,
ACTION_SHORTCUT_RELEASED as RELEASED
} from '../../../../analytics';
import { getFeatureFlag, AUDIO_MUTE_BUTTON_ENABLED } from '../../../../base/flags'; import { getFeatureFlag, AUDIO_MUTE_BUTTON_ENABLED } from '../../../../base/flags';
import { Icon, IconMicrophone, IconMicrophoneEmptySlash } from '../../../../base/icons'; import { Icon, IconMicrophone, IconMicrophoneEmptySlash } from '../../../../base/icons';
@ -11,6 +18,8 @@ import { muteLocal } from '../../../../video-menu/actions';
import styles from './styles'; import styles from './styles';
const LONG_PRESS = 'long.press';
/** /**
* Implements a round audio mute/unmute button of a custom size. * Implements a round audio mute/unmute button of a custom size.
* *
@ -21,18 +30,45 @@ const MicrophoneButton = () : JSX.Element => {
const audioMuted = useSelector(state => isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO)); const audioMuted = useSelector(state => isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO));
const disabled = useSelector(isAudioMuteButtonDisabled); const disabled = useSelector(isAudioMuteButtonDisabled);
const enabledFlag = useSelector(state => getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true)); const enabledFlag = useSelector(state => getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true));
const [ longPress, setLongPress ] = useState(false);
if (!enabledFlag) { if (!enabledFlag) {
return null; return null;
} }
const toggleMute = useCallback(() => { const onPressIn = useCallback(() => {
!disabled && dispatch(muteLocal(!audioMuted, MEDIA_TYPE.AUDIO)); !disabled && dispatch(muteLocal(!audioMuted, MEDIA_TYPE.AUDIO));
}, [ audioMuted, disabled ]); }, [ 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 ( return (
<TouchableOpacity <TouchableOpacity
onPress = { toggleMute }> onPressIn = { onPressIn }
onLongPress={ onLongPress }
onPressOut={ onPressOut } >
<View <View
style = { [ style = { [
styles.microphoneStyles.container, styles.microphoneStyles.container,

View File

@ -4,8 +4,7 @@ import { Text, View } from 'react-native';
import { getConferenceName } from '../../../../base/conference/functions'; import { getConferenceName } from '../../../../base/conference/functions';
import { getFeatureFlag, MEETING_NAME_ENABLED } from '../../../../base/flags'; import { getFeatureFlag, MEETING_NAME_ENABLED } from '../../../../base/flags';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet'; import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux'; import { connect } from '../../../../base/redux';;
import { PictureInPictureButton } from '../../../../mobile/picture-in-picture';
import { RecordingLabel } from '../../../../recording'; import { RecordingLabel } from '../../../../recording';
import { VideoQualityLabel } from '../../../../video-quality'; import { VideoQualityLabel } from '../../../../video-quality';
@ -40,9 +39,6 @@ const TitleBar = (props: Props) : JSX.Element => (<>
<View <View
pointerEvents = 'box-none' pointerEvents = 'box-none'
style = { styles.roomNameWrapper }> style = { styles.roomNameWrapper }>
<View style = { styles.headerLabels }>
<PictureInPictureButton styles = { styles.pipButton } />
</View>
<View style = { styles.headerLabels }> <View style = { styles.headerLabels }>
<VideoQualityLabel /> <VideoQualityLabel />
</View> </View>

View File

@ -5,8 +5,6 @@ import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
*/ */
const MICROPHONE_SIZE = 180; const MICROPHONE_SIZE = 180;
const TITLE_BAR_BUTTON_SIZE = 24;
/** /**
* Base button style. * Base button style.
*/ */
@ -59,11 +57,9 @@ export default {
* {@code Conference} Style. * {@code Conference} Style.
*/ */
conference: { conference: {
alignSelf: 'stretch',
backgroundColor: BaseTheme.palette.uiBackground, backgroundColor: BaseTheme.palette.uiBackground,
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center'
alignItems: 'center'
}, },
microphoneStyles: { microphoneStyles: {
@ -100,14 +96,6 @@ export default {
} }
}, },
pipButton: {
iconStyle: {
color: BaseTheme.palette.icon01,
fontSize: TITLE_BAR_BUTTON_SIZE
},
underlayColor: BaseTheme.spacing.underlay01
},
roomTimer: { roomTimer: {
color: BaseTheme.palette.text01, color: BaseTheme.palette.text01,
...BaseTheme.typography.bodyShortBold, ...BaseTheme.typography.bodyShortBold,
@ -165,6 +153,12 @@ export default {
backgroundColor: BaseTheme.palette.uiBackground backgroundColor: BaseTheme.palette.uiBackground
}, },
microphoneContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
titleBarWrapper: { titleBarWrapper: {
alignItems: 'center', alignItems: 'center',
flex: 1, flex: 1,

View File

@ -9,7 +9,7 @@ export const conferenceNavigationRef = React.createRef();
* @param {Object} params - Params to pass to the destination route. * @param {Object} params - Params to pass to the destination route.
* @returns {Function} * @returns {Function}
*/ */
export function navigate(name: string, params: Object) { export function navigate(name: string, params?: Object) {
return conferenceNavigationRef.current?.navigate(name, params); return conferenceNavigationRef.current?.navigate(name, params);
} }

View File

@ -1,67 +1,165 @@
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; import { NavigationContainer } from '@react-navigation/native';
import { NavigationContainer, TypedNavigator } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack';
import React, { useCallback, useState } from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import ConferenceTab from '../../../../../conference/components/native/ConferenceTab'; import { Chat, ChatAndPolls } from '../../../../../chat';
import Conference from '../../../../../conference/components/native/Conference';
import CarmodeTab from '../../../../../conference/components/native/carmode/Conference'; import CarmodeTab from '../../../../../conference/components/native/carmode/Conference';
import { getDisablePolls } from '../../../../../conference/functions';
import { SharedDocument } from '../../../../../etherpad';
import { GifsMenu } from '../../../../../gifs/components';
import AddPeopleDialog
from '../../../../../invite/components/add-people-dialog/native/AddPeopleDialog';
import { ParticipantsPane } from '../../../../../participants-pane/components/native';
import { StartLiveStreamDialog } from '../../../../../recording';
import { StartRecordingDialog }
from '../../../../../recording/components/Recording/native';
import SalesforceLinkDialog
from '../../../../../salesforce/components/native/SalesforceLinkDialog';
import SecurityDialog
from '../../../../../security/components/security-dialog/native/SecurityDialog';
import SpeakerStats
from '../../../../../speaker-stats/components/native/SpeakerStats';
import { screen } from '../../../routes'; import { screen } from '../../../routes';
import { navigationContainerTheme } from '../../../screenOptions'; import {
carmodeScreenOptions,
chatScreenOptions,
conferenceScreenOptions,
gifsMenuOptions,
inviteScreenOptions,
liveStreamScreenOptions,
navigationContainerTheme,
participantsScreenOptions,
recordingScreenOptions,
salesforceScreenOptions,
securityScreenOptions,
sharedDocumentScreenOptions,
speakerStatsScreenOptions
} from '../../../screenOptions';
import LobbyNavigationContainer
from '../../lobby/components/LobbyNavigationContainer';
import { import {
conferenceNavigationRef conferenceNavigationRef
} from '../ConferenceNavigationContainerRef'; } from '../ConferenceNavigationContainerRef';
import NavigationThumb, { THUMBS } from './NavigationThumb'; const ConferenceStack = createStackNavigator();
import styles from './styles';
const ConferenceTabs: TypedNavigator = createMaterialTopTabNavigator(); const ConferenceNavigationContainer = () => {
const isPollsDisabled = useSelector(getDisablePolls);
let ChatScreen;
let chatScreenName;
let chatTitleString;
/** if (isPollsDisabled) {
* Navigation container component for the conference. ChatScreen = Chat;
* chatScreenName = screen.conference.chat;
* @returns {JSX.Element} - the container. chatTitleString = 'chat.title';
*/
const ConferenceNavigationContainer = () : JSX.Element => {
const [ selectedThumb , setSelectedThumb ] = useState(THUMBS.CONFERENCE_VIEW);
/**
* Lights up the correct bottom navigation circle
* in regards with the focused screen.
*/
const onFocused = useCallback(selected => {
if (selected === screen.car) {
setSelectedThumb(THUMBS.CAR_VIEW);
} else { } else {
setSelectedThumb(THUMBS.CONFERENCE_VIEW); ChatScreen = ChatAndPolls;
chatScreenName = screen.conference.chatandpolls.main;
chatTitleString = 'chat.titleWithPolls';
} }
}, []); const { t } = useTranslation();
const Carmode = useCallback(() => (
<CarmodeTab
onFocused = { onFocused } />
), []);
const Conference = useCallback(() => (
<ConferenceTab
onFocused = { onFocused } />
), []);
return ( return (
<NavigationContainer <NavigationContainer
independent = { true } independent = { true }
ref = { conferenceNavigationRef } ref = { conferenceNavigationRef }
theme = { navigationContainerTheme }> theme = { navigationContainerTheme }>
<ConferenceTabs.Navigator <ConferenceStack.Navigator
backBehavior = 'none' screenOptions = {{
screenOptions = { styles.tabBarOptions }> presentation: 'modal'
<ConferenceTabs.Screen }}>
<ConferenceStack.Screen
component = { Conference } component = { Conference }
name = { screen.conference.container } /> name = { screen.conference.main }
<ConferenceTabs.Screen options = { conferenceScreenOptions } />
component = { Carmode } <ConferenceStack.Screen
headerShown = { false } component = { ChatScreen }
name = { screen.car } /> name = { chatScreenName }
</ConferenceTabs.Navigator> options = {{
<NavigationThumb selectedThumb = { selectedThumb } /> ...chatScreenOptions,
title: t(chatTitleString)
}} />
<ConferenceStack.Screen
component = { ParticipantsPane }
name = { screen.conference.participants }
options = {{
...participantsScreenOptions,
title: t('participantsPane.header')
}} />
<ConferenceStack.Screen
component = { SecurityDialog }
name = { screen.conference.security }
options = {{
...securityScreenOptions,
title: t('security.header')
}} />
<ConferenceStack.Screen
component = { StartRecordingDialog }
name = { screen.conference.recording }
options = {{
...recordingScreenOptions
}} />
<ConferenceStack.Screen
component = { StartLiveStreamDialog }
name = { screen.conference.liveStream }
options = {{
...liveStreamScreenOptions
}} />
<ConferenceStack.Screen
component = { SpeakerStats }
name = { screen.conference.speakerStats }
options = {{
...speakerStatsScreenOptions,
title: t('speakerStats.speakerStats')
}} />
<ConferenceStack.Screen
component = { SalesforceLinkDialog }
name = { screen.conference.salesforce }
options = {{
...salesforceScreenOptions,
title: t('notify.linkToSalesforce')
}} />
<ConferenceStack.Screen
component = { GifsMenu }
name = { screen.conference.gifsMenu }
options = {{
...gifsMenuOptions,
title: t('notify.gifsMenu')
}} />
<ConferenceStack.Screen
component = { LobbyNavigationContainer }
name = { screen.lobby.root }
options = {{
gestureEnabled: false,
headerShown: false
}} />
<ConferenceStack.Screen
component = { AddPeopleDialog }
name = { screen.conference.invite }
options = {{
...inviteScreenOptions,
title: t('addPeople.add')
}} />
<ConferenceStack.Screen
component = { SharedDocument }
name = { screen.conference.sharedDocument }
options = {{
...sharedDocumentScreenOptions,
title: t('documentSharing.title')
}} />
<ConferenceStack.Screen
component = { CarmodeTab }
name = { screen.conference.carmode }
options = {{
...carmodeScreenOptions,
title: t('carmode.labels.title')
}}/>
</ConferenceStack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
}; };

View File

@ -1,46 +0,0 @@
// @flow
import React from 'react';
import { SafeAreaView } from 'react-native';
import { Icon, IconCircle } from '../../../../../base/icons';
import styles, { ICON_ACTIVE_COLOR, ICON_INACTIVE_COLOR } from './styles';
export const enum THUMBS {
CONFERENCE_VIEW = 'CONFERENCE_VIEW ',
CAR_VIEW = 'CAR_VIEW'
}
type Props = {
/**
* Which thumb is selected.
*/
selectedThumb: THUMBS
}
/**
* Bottom tab navigation screen indicator.
*
* @returns {JSX.Element} - The tab navigation indicator.
*/
const NavigationThumb = ({ selectedThumb }: Props): JSX.Element => (
<SafeAreaView
style = { styles.navigationThumbContainer }>
{
Object.values(THUMBS)
.map(value =>
(<Icon
key = { `thumb-${value.toLowerCase()}` }
size = { 8 }
color = { value === selectedThumb ? ICON_ACTIVE_COLOR : ICON_INACTIVE_COLOR}
src = { IconCircle }
style = { styles.navigationThumbIcon } />)
)
}
</SafeAreaView>
);
export default NavigationThumb;

View File

@ -1,31 +0,0 @@
import BaseTheme from '../../../../../base/ui/components/BaseTheme';
export const ICON_ACTIVE_COLOR = BaseTheme.palette.icon01;
export const ICON_INACTIVE_COLOR = BaseTheme.palette.icon03;
/**
* The styles navigation components.
*/
export default {
navigationThumbContainer: {
alignSelf: 'center',
flexDirection: 'row',
position: 'absolute',
bottom: 13,
height: 8,
flex: 1
},
navigationThumbIcon: {
marginRight: 10
},
tabBarOptions: {
tabBarStyle: {
display: 'none'
}
}
};

View File

@ -11,12 +11,12 @@ export const screen = {
privacy: 'Privacy', privacy: 'Privacy',
help: 'Help' help: 'Help'
}, },
car: 'Car Mode',
dialInSummary: 'Dial-In Info', dialInSummary: 'Dial-In Info',
connecting: 'Connecting', connecting: 'Connecting',
conference: { conference: {
root: 'Conference root', root: 'Conference root',
main: 'Conference', main: 'Conference',
carmode: 'Car Mode',
chat: 'Chat', chat: 'Chat',
chatandpolls: { chatandpolls: {
main: 'Chat and Polls', main: 'Chat and Polls',

View File

@ -198,6 +198,11 @@ export const presentationScreenOptions = {
} }
}; };
/**
* Screen options for car mode.
*/
export const carmodeScreenOptions = presentationScreenOptions;
/** /**
* Screen options for chat. * Screen options for chat.
*/ */

View File

@ -0,0 +1,28 @@
import { translate } from '../../../base/i18n';
import { IconCar } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* Implements an {@link AbstractButton} to open the carmode.
*/
class OpenCarmodeButton extends AbstractButton<AbstractButtonProps, any, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.carmode';
icon = IconCar;
label = 'carmode.labels.buttonLabel';
/**
* Handles clicking / pressing the button, and opens the carmode mode.
*
* @private
* @returns {void}
*/
_handleClick() {
return navigate(screen.conference.carmode);
}
}
export default translate(connect()(OpenCarmodeButton));

View File

@ -24,6 +24,7 @@ import HelpButton from '../HelpButton';
import AudioOnlyButton from './AudioOnlyButton'; import AudioOnlyButton from './AudioOnlyButton';
import LinkToSalesforceButton from './LinkToSalesforceButton'; import LinkToSalesforceButton from './LinkToSalesforceButton';
import OpenCarmodeButton from './OpenCarmodeButton';
import RaiseHandButton from './RaiseHandButton'; import RaiseHandButton from './RaiseHandButton';
import ScreenSharingButton from './ScreenSharingButton.js'; import ScreenSharingButton from './ScreenSharingButton.js';
import ToggleCameraButton from './ToggleCameraButton'; import ToggleCameraButton from './ToggleCameraButton';
@ -143,6 +144,7 @@ class OverflowMenu extends PureComponent<Props, State> {
? this._renderReactionMenu ? this._renderReactionMenu
: null }> : null }>
<ParticipantsPaneButton { ...topButtonProps } /> <ParticipantsPaneButton { ...topButtonProps } />
<OpenCarmodeButton { ...topButtonProps } />
<AudioOnlyButton { ...buttonProps } /> <AudioOnlyButton { ...buttonProps } />
{!_reactionsEnabled && !toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />} {!_reactionsEnabled && !toolbarButtons.has('raisehand') && <RaiseHandButton { ...buttonProps } />}
<Divider style = { styles.divider } /> <Divider style = { styles.divider } />

View File

@ -1 +1,24 @@
import { MEDIA_TYPE, setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../base/media';
import { MiddlewareRegistry } from '../base/redux';
import { SET_CAR_MODE } from './actionTypes';
import './middleware.any'; import './middleware.any';
/**
* Middleware which intercepts actions and updates the legacy component.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { dispatch } = store;
switch (action.type) {
case SET_CAR_MODE:
dispatch(setVideoMuted(action.enabled, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.CAR_MODE));
break;
}
return result;
});