feat(carmode) Add carmode screen

This commit is contained in:
Horatiu Muresan 2022-05-03 13:56:40 +03:00
parent 8e67c8e74f
commit ac965b86bc
27 changed files with 972 additions and 193 deletions

View File

@ -77,6 +77,16 @@
"refresh": "Refresh calendar",
"today": "Today"
},
"carmode": {
"actions": {
"leaveMeeting": " Leave meeting",
"selectSoundDevice": "Select sound device"
},
"labels": {
"title": "Safe driving mode",
"videoStopped": "Your video is stopped"
}
},
"chat": {
"enter": "Enter room",
"error": "Error: your message was not sent. Reason: {{error}}",

View File

@ -0,0 +1,3 @@
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="12"/>
</svg>

After

Width:  |  Height:  |  Size: 124 B

View File

@ -28,6 +28,7 @@ export { default as IconChatSend } from './send.svg';
export { default as IconChatUnread } from './chat-unread.svg';
export { default as IconCheck } from './check.svg';
export { default as IconCheckSolid } from './check-solid.svg';
export { default as IconCircle } from './circle.svg';
export { default as IconClose } from './close.svg';
export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseSolid } from './close-solid.svg';

View File

@ -7,6 +7,7 @@ import { SELECT_LARGE_VIDEO_PARTICIPANT } from '../../large-video/actionTypes';
import { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_CAR_MODE,
SET_TILE_VIEW,
VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED
} from '../../video-layout/actionTypes';
@ -68,7 +69,7 @@ const _updateLastN = debounce(({ dispatch, getState }) => {
if (typeof appState !== 'undefined' && appState !== 'active') {
lastNSelected = isLocalVideoTrackDesktop(state) ? 1 : 0;
} else if (audioOnly) {
const { remoteScreenShares, tileViewEnabled } = state['features/video-layout'];
const { remoteScreenShares, tileViewEnabled, carMode } = state['features/video-layout'];
const largeVideoParticipantId = state['features/large-video'].participantId;
const largeVideoParticipant
= largeVideoParticipantId ? getParticipantById(state, largeVideoParticipantId) : undefined;
@ -76,7 +77,7 @@ const _updateLastN = debounce(({ dispatch, getState }) => {
// Use tileViewEnabled state from redux here instead of determining if client should be in tile
// view since we make an exception only for screenshare when in audio-only mode. If the user unpins
// the screenshare, lastN will be set to 0 here. It will be set to 1 if screenshare has been auto pinned.
if (!tileViewEnabled && largeVideoParticipant && !largeVideoParticipant.local) {
if (!carMode && !tileViewEnabled && largeVideoParticipant && !largeVideoParticipant.local) {
lastNSelected = (remoteScreenShares || []).includes(largeVideoParticipantId) ? 1 : 0;
} else {
lastNSelected = 0;
@ -101,6 +102,7 @@ MiddlewareRegistry.register(store => next => action => {
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case SELECT_LARGE_VIDEO_PARTICIPANT:
case SET_AUDIO_ONLY:
case SET_CAR_MODE:
case SET_FILMSTRIP_ENABLED:
case SET_TILE_VIEW:
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED:

View File

@ -4,20 +4,20 @@ import { createMaterialTopTabNavigator } from '@react-navigation/material-top-ta
import React from 'react';
import { useSelector } from 'react-redux';
import { Chat } from '../..';
import {
getClientHeight,
getClientWidth
} from '../../../../../base/modal/components/functions.native';
import BaseTheme from '../../../../../base/ui/components/BaseTheme.native';
import { Chat } from '../../../../../chat';
import { PollsPane } from '../../../../../polls/components';
import { screen } from '../../../routes';
import { chatTabBarOptions } from '../../../screenOptions';
} from '../../../base/modal/components/functions.native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import { screen } from '../../../mobile/navigation/routes';
import { chatTabBarOptions } from '../../../mobile/navigation/screenOptions';
import { PollsPane } from '../../../polls/components';
const ChatTab = createMaterialTopTabNavigator();
const ChatAndPollsNavigationContainer = () => {
const ChatAndPolls = () => {
const clientHeight = useSelector(getClientHeight);
const clientWidth = useSelector(getClientWidth);
@ -44,4 +44,4 @@ const ChatAndPollsNavigationContainer = () => {
);
};
export default ChatAndPollsNavigationContainer;
export default ChatAndPolls;

View File

@ -1,5 +1,6 @@
// @flow
export { default as Chat } from './Chat';
export { default as ChatAndPolls } from './ChatAndPolls';
export { default as ChatButton } from './ChatButton';
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

View File

@ -1 +1,30 @@
export * from './functions.any';
/**
* Returns whether the conference is in connecting state.
*
* @param {Object} state - The redux state.
* @returns {boolean} Whether conference is connecting.
*/
export const isConnecting = (state: Object) => {
const { connecting, connection } = state['features/base/connection'];
const {
conference,
joining,
membersOnly,
leaving
} = state['features/base/conference'];
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
// which the app does not appear to be doing anything according to the redux
// state. In order to not toggle the _connecting props during the window of
// time in question, define _connecting as follows:
// - the XMPP connection is connecting, or
// - the XMPP connection is connected and the conference is joining, or
// - the XMPP connection is connected and we have no conference yet, nor we
// are leaving one.
return Boolean(
connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))))
);
};

View File

@ -35,6 +35,7 @@ import {
abstractMapStateToProps
} from '../AbstractConference';
import type { AbstractProps } from '../AbstractConference';
import { isConnecting } from '../functions';
import AlwaysOnLabels from './AlwaysOnLabels';
import ExpandedLabelPopup from './ExpandedLabelPopup';
@ -496,34 +497,15 @@ class Conference extends AbstractConference<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { connecting, connection } = state['features/base/connection'];
const {
conference,
joining,
membersOnly,
leaving
} = state['features/base/conference'];
const { isOpen } = state['features/participants-pane'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
const participantCount = getParticipantCount(state);
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
// which the app does not appear to be doing anything according to the redux
// state. In order to not toggle the _connecting props during the window of
// time in question, define _connecting as follows:
// - the XMPP connection is connecting, or
// - the XMPP connection is connected and the conference is joining, or
// - the XMPP connection is connected and we have no conference yet, nor we
// are leaving one.
const connecting_
= connecting || (connection && (!membersOnly && (joining || (!conference && !leaving))));
return {
...abstractMapStateToProps(state),
_aspectRatio: aspectRatio,
_calendarEnabled: isCalendarEnabled(state),
_connecting: Boolean(connecting_),
_connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isOneToOneConference: Boolean(participantCount === 2),

View File

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

@ -0,0 +1,17 @@
import React from 'react';
import { Icon, IconVolumeEmpty } from '../../../../base/icons';
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.text06 }
size = { 20 }
src = { IconVolumeEmpty } />);
export default AudioIcon;

View File

@ -0,0 +1,93 @@
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, SafeAreaView, View } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { batch, useDispatch, useSelector } from 'react-redux';
import { setAudioOnly } from '../../../../base/audio-only';
import { Container, LoadingIndicator, TintedView } from '../../../../base/react';
import { screen } from '../../../../mobile/navigation/routes';
import { setIsCarmode } from '../../../../video-layout/actions';
import ConferenceTimer from '../../ConferenceTimer';
import { isConnecting } from '../../functions';
import EndMeetingButton from './EndMeetingButton';
import MicrophoneButton from './MicrophoneButton';
import SoundDeviceButton from './SoundDeviceButton';
import TitleBar from './TitleBar';
import styles from './styles';
type Props = {
/**
* Callback on component focused.
* Passes the route name to the embedder.
*/
onFocused: Function
}
/**
* Implements the carmode tab.
*
* @param { Props } - - The component's props.
* @returns { JSX.Element} - The carmode tab.
*/
const CarmodeTab = ({ onFocused }: Props): JSX.Element => {
const isFocused = useIsFocused();
const dispatch = useDispatch();
const { t } = useTranslation();
const connecting = useSelector(isConnecting);
useEffect(() => {
if (isFocused) {
batch(() => {
dispatch(setAudioOnly(true));
dispatch(setIsCarmode(true));
});
onFocused(screen.car);
}
}, [ isFocused ]);
return (
<Container style = { styles.conference }>
{/*
* The activity/loading indicator goes above everything, except
* the toolbox/toolbars and the dialogs.
*/
connecting
&& <TintedView>
<LoadingIndicator />
</TintedView>
}
<SafeAreaView
pointerEvents = 'box-none'
style = { styles.titleBarSafeViewColor }>
<View style = { styles.titleView }>
<Text style = { styles.title }>
{t('carmode.labels.title')}
</Text>
</View>
<View
style = { styles.titleBar }>
<TitleBar />
</View>
<ConferenceTimer textStyle = { styles.roomTimer } />
</SafeAreaView>
<MicrophoneButton />
<SafeAreaView
pointerEvents = 'box-none'
style = { styles.bottomContainer }>
<Text style = { styles.videoStoppedLabel }>
{t('carmode.labels.videoStopped')}
</Text>
<SoundDeviceButton />
<EndMeetingButton />
</SafeAreaView>
</Container>
);
};
export default withSafeAreaInsets(CarmodeTab);

View File

@ -0,0 +1,39 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../../../analytics';
import { appNavigate } from '../../../../app/actions';
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 { t } = useTranslation();
const dispatch = useDispatch();
const onSelect = useCallback(() => {
sendAnalytics(createToolbarEvent('hangup'));
dispatch(appNavigate(undefined));
}, [ dispatch ]);
return (
<Button
accessibilityLabel = { t('carmode.actions.leaveMeeting') }
children = { t('carmode.actions.leaveMeeting') }
icon = { EndMeetingIcon }
labelStyle = { styles.endMeetingButtonLabel }
mode = 'contained'
onPress = { onSelect }
style = { styles.endMeetingButton } />
);
};
export default EndMeetingButton;

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Icon, IconHangup } from '../../../../base/icons';
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,52 @@
import React, { useCallback } from 'react';
import { View, TouchableOpacity } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { getFeatureFlag, AUDIO_MUTE_BUTTON_ENABLED } from '../../../../base/flags';
import { Icon, IconMicrophone, IconMicrophoneEmptySlash } from '../../../../base/icons';
import { MEDIA_TYPE } from '../../../../base/media';
import { isLocalTrackMuted } from '../../../../base/tracks';
import { isAudioMuteButtonDisabled } from '../../../../toolbox/functions.any';
import { muteLocal } from '../../../../video-menu/actions';
import styles from './styles';
/**
* Implements a round audio mute/unmute button of a custom size.
*
* @returns {JSX.Element} - The audio mute round button.
*/
const MicrophoneButton = () : JSX.Element => {
const dispatch = useDispatch();
const audioMuted = useSelector(state => isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO));
const disabled = useSelector(isAudioMuteButtonDisabled);
const enabledFlag = useSelector(state => getFeatureFlag(state, AUDIO_MUTE_BUTTON_ENABLED, true));
if (!enabledFlag) {
return null;
}
const toggleMute = useCallback(() => {
!disabled && dispatch(muteLocal(!audioMuted, MEDIA_TYPE.AUDIO));
}, [ audioMuted, disabled ]);
return (
<TouchableOpacity
onPress = { toggleMute }>
<View
style = { [
styles.microphoneStyles.container,
!audioMuted && styles.microphoneStyles.unmuted
] }>
<View
style = { styles.microphoneStyles.iconContainer }>
<Icon
src = { audioMuted ? IconMicrophoneEmptySlash : IconMicrophone }
style = { styles.microphoneStyles.icon } />
</View>
</View>
</TouchableOpacity>
);
};
export default MicrophoneButton;

View File

@ -0,0 +1,37 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import { openDialog } from '../../../../base/dialog/actions';
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 { t } = useTranslation();
const dispatch = useDispatch();
const onSelect = useCallback(() =>
dispatch(openDialog(AudioRoutePickerDialog))
, [ dispatch ]);
return (
<Button
accessibilityLabel = { t('carmode.actions.selectSoundDevice') }
children = { t('carmode.actions.selectSoundDevice') }
icon = { AudioIcon }
labelStyle = { styles.soundDeviceButtonLabel }
mode = 'contained'
onPress = { onSelect }
style = { styles.soundDeviceButton } />
);
};
export default SelectSoundDevice;

View File

@ -0,0 +1,85 @@
import React from 'react';
import { Text, View } from 'react-native';
import { getConferenceName } from '../../../../base/conference/functions';
import { getFeatureFlag, MEETING_NAME_ENABLED } from '../../../../base/flags';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import { PictureInPictureButton } from '../../../../mobile/picture-in-picture';
import { RecordingLabel } from '../../../../recording';
import { VideoQualityLabel } from '../../../../video-quality';
import styles from './styles';
type Props = {
/**
* 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 {Props} props - The React props passed to this component.
* @returns {JSX.Element}
*/
const TitleBar = (props: Props) : JSX.Element => (<>
<View
pointerEvents = 'box-none'
style = { styles.titleBarWrapper }>
<View
pointerEvents = 'box-none'
style = { styles.roomNameWrapper }>
<View style = { styles.headerLabels }>
<PictureInPictureButton styles = { styles.pipButton } />
</View>
<View style = { styles.headerLabels }>
<VideoQualityLabel />
</View>
<View style = { styles.headerLabels }>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
</View>
{
props._meetingNameEnabled
&& <View style = { styles.roomNameView }>
<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 {Props}
*/
function _mapStateToProps(state: Object) {
const { hideConferenceSubject } = state['features/base/config'];
return {
_meetingName: getConferenceName(state),
_meetingNameEnabled:
getFeatureFlag(state, MEETING_NAME_ENABLED, true) && !hideConferenceSubject
};
}
export default connect(_mapStateToProps)(TitleBar);

View File

@ -0,0 +1,204 @@
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
/**
* The size of the microphone icon.
*/
const MICROPHONE_SIZE = 180;
const TITLE_BAR_BUTTON_SIZE = 24;
/**
* Base button style.
*/
const baseButton = {
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
marginTop: BaseTheme.spacing[3],
marginLeft: BaseTheme.spacing[10],
marginRight: BaseTheme.spacing[10],
display: 'flex',
justifyContent: 'space-around',
width: 300
};
/**
* Base label style.
*/
const baseLabel = {
display: 'flex',
fontSize: 16,
textTransform: 'capitalize'
};
/**
* 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: 0,
left: 0,
right: 0,
position: 'absolute'
},
/**
* {@code Conference} Style.
*/
conference: {
alignSelf: 'stretch',
backgroundColor: BaseTheme.palette.uiBackground,
flex: 1,
justifyContent: 'center',
alignItems: '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
}
},
pipButton: {
iconStyle: {
color: BaseTheme.palette.icon01,
fontSize: TITLE_BAR_BUTTON_SIZE
},
underlayColor: BaseTheme.spacing.underlay01
},
roomTimer: {
color: BaseTheme.palette.text01,
...BaseTheme.typography.bodyShortBold,
paddingHorizontal: 8,
paddingVertical: 6,
textAlign: 'center'
},
titleView: {
width: 152,
height: 28,
backgroundColor: BaseTheme.palette.ui02,
borderRadius: 12,
alignSelf: 'center'
},
title: {
margin: 'auto',
textAlign: 'center',
paddingVertical: 4,
paddingHorizontal: 16,
color: BaseTheme.palette.text02
},
soundDeviceButtonLabel: {
...baseLabel,
color: BaseTheme.palette.text06
},
soundDeviceButton: {
...baseButton,
backgroundColor: BaseTheme.palette.section01
},
endMeetingButton: {
...baseButton,
backgroundColor: BaseTheme.palette.actionDanger,
marginBottom: 60
},
endMeetingButtonLabel: {
...baseLabel,
color: BaseTheme.palette.text01
},
headerLabels: {
borderBottomLeftRadius: 3,
borderTopLeftRadius: 3,
flexShrink: 1,
paddingHorizontal: 2
},
titleBarSafeViewColor: {
...titleBarSafeView,
backgroundColor: BaseTheme.palette.uiBackground
},
titleBarWrapper: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
roomNameWrapper: {
flexDirection: 'row',
marginRight: 10,
flexShrink: 1,
flexGrow: 1
},
roomNameView: {
backgroundColor: 'rgba(0,0,0,0.6)',
flexShrink: 1,
justifyContent: 'center',
paddingHorizontal: 5,
marginBottom: 8
},
roomName: {
color: BaseTheme.palette.text01,
...BaseTheme.typography.bodyShortBold
},
titleBar: {
alignSelf: 'center'
},
videoStoppedLabel: {
color: BaseTheme.palette.text01,
marginBottom: 32,
...BaseTheme.typography.bodyShortRegularLarge
}
};

View File

@ -1,160 +0,0 @@
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Chat } from '../../../../../chat';
import Conference from '../../../../../conference/components/native/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 {
chatScreenOptions,
conferenceScreenOptions,
gifsMenuOptions,
inviteScreenOptions,
liveStreamScreenOptions,
navigationContainerTheme,
participantsScreenOptions,
recordingScreenOptions,
salesforceScreenOptions,
securityScreenOptions,
sharedDocumentScreenOptions,
speakerStatsScreenOptions
} from '../../../screenOptions';
import ChatAndPollsNavigationContainer
from '../../chat/components/ChatAndPollsNavigationContainer';
import LobbyNavigationContainer
from '../../lobby/components/LobbyNavigationContainer';
import {
conferenceNavigationRef
} from '../ConferenceNavigationContainerRef';
const ConferenceStack = createStackNavigator();
const ConferenceNavigationContainer = () => {
const isPollsDisabled = useSelector(getDisablePolls);
let ChatScreen;
let chatScreenName;
let chatTitleString;
if (isPollsDisabled) {
ChatScreen = Chat;
chatScreenName = screen.conference.chat;
chatTitleString = 'chat.title';
} else {
ChatScreen = ChatAndPollsNavigationContainer;
chatScreenName = screen.conference.chatandpolls.main;
chatTitleString = 'chat.titleWithPolls';
}
const { t } = useTranslation();
return (
<NavigationContainer
independent = { true }
ref = { conferenceNavigationRef }
theme = { navigationContainerTheme }>
<ConferenceStack.Navigator
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>
</NavigationContainer>
);
};
export default ConferenceNavigationContainer;

View File

@ -0,0 +1,69 @@
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import { NavigationContainer, TypedNavigator } from '@react-navigation/native';
import React, { useCallback, useState } from 'react';
import ConferenceTab from '../../../../../conference/components/native/ConferenceTab';
import CarmodeTab from '../../../../../conference/components/native/carmode/Conference';
import { screen } from '../../../routes';
import { navigationContainerTheme } from '../../../screenOptions';
import {
conferenceNavigationRef
} from '../ConferenceNavigationContainerRef';
import NavigationThumb, { THUMBS } from './NavigationThumb';
import styles from './styles';
const ConferenceTabs: TypedNavigator = createMaterialTopTabNavigator();
/**
* Navigation container component for the conference.
*
* @returns {JSX.Element} - the container.
*/
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 {
setSelectedThumb(THUMBS.CONFERENCE_VIEW);
}
}, []);
const Carmode = useCallback(() => (
<CarmodeTab
onFocused = { onFocused } />
), []);
const Conference = useCallback(() => (
<ConferenceTab
onFocused = { onFocused } />
), []);
return (
<NavigationContainer
independent = { true }
ref = { conferenceNavigationRef }
theme = { navigationContainerTheme }>
<ConferenceTabs.Navigator
backBehavior = 'none'
screenOptions = { styles.tabBarOptions }>
<ConferenceTabs.Screen
component = { Conference }
name = { screen.conference.container } />
<ConferenceTabs.Screen
component = { Carmode }
headerShown = { false }
name = { screen.car } />
</ConferenceTabs.Navigator>
<NavigationThumb selectedThumb = { selectedThumb } />
</NavigationContainer>
);
};
export default ConferenceNavigationContainer;

View File

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

@ -0,0 +1,31 @@
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,6 +11,7 @@ export const screen = {
privacy: 'Privacy',
help: 'Help'
},
car: 'Car Mode',
dialInSummary: 'Dial-In Info',
connecting: 'Connecting',
conference: {
@ -24,6 +25,7 @@ export const screen = {
polls: 'Polls'
}
},
container: 'Conference container',
security: 'Security Options',
recording: 'Recording',
liveStream: 'Live stream',

View File

@ -30,3 +30,13 @@ export const VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED = 'VIRTUAL_SCREENSH
* }}
*/
export const SET_TILE_VIEW = 'SET_TILE_VIEW';
/**
* The type of the action which tells whether we are in carmode.
*
* @returns {{
* type: SET_CAR_MODE,
* enabled: boolean
* }}
*/
export const SET_CAR_MODE = ' SET_CAR_MODE';

View File

@ -0,0 +1,19 @@
import { SET_CAR_MODE } from './actionTypes';
export * from './actions.any';
/**
* Creates a (redux) action which tells whether we are in carmode.
*
* @param {boolean} enabled - Whether we are in carmode.
* @returns {{
* type: SET_CAR_MODE,
* enabled: boolean
* }}
*/
export function setIsCarmode(enabled) {
return {
type: SET_CAR_MODE,
enabled
};
}

View File

@ -0,0 +1 @@
export * from './actions.any';

View File

@ -4,6 +4,7 @@ import { ReducerRegistry } from '../base/redux';
import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_CAR_MODE,
SET_TILE_VIEW,
VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED
} from './actionTypes';
@ -21,7 +22,15 @@ const DEFAULT_STATE = {
* @public
* @type {boolean}
*/
tileViewEnabled: undefined
tileViewEnabled: undefined,
/**
* Whether we are in carmode.
*
* @public
* @type {boolean}
*/
carMode: false
};
const STORE_NAME = 'features/video-layout';
@ -29,18 +38,23 @@ const STORE_NAME = 'features/video-layout';
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED: {
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED:
return {
...state,
remoteScreenShares: action.participantIds
};
}
case SET_TILE_VIEW:
return {
...state,
tileViewEnabled: action.enabled
};
case SET_CAR_MODE:
return {
...state,
carMode: action.enabled
};
}
return state;