Compare commits

...

6 Commits

Author SHA1 Message Date
Horatiu Muresan 6e6ed1b9a2 Address review 2022-05-06 12:39:32 +03:00
Horatiu Muresan 0a07770c20 Align rec and audio only labels 2022-05-05 18:25:04 +03:00
Horatiu Muresan 1829a9b678 Update car icon 2022-05-05 17:02:44 +03:00
Horatiu Muresan 6948fb98cc fix 2022-05-05 13:57:02 +03:00
Horatiu Muresan 698ab32a62 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
2022-05-05 13:00:36 +03:00
Horatiu Muresan ac965b86bc feat(carmode) Add carmode screen 2022-05-03 13:56:40 +03:00
33 changed files with 788 additions and 49 deletions

View File

@ -77,6 +77,17 @@
"refresh": "Refresh calendar", "refresh": "Refresh calendar",
"today": "Today" "today": "Today"
}, },
"carmode": {
"actions": {
"leaveMeeting": " Leave meeting",
"selectSoundDevice": "Select sound device"
},
"labels": {
"buttonLabel": "Car mode",
"title": "Safe driving mode",
"videoStopped": "Your video is stopped"
}
},
"chat": { "chat": {
"enter": "Enter room", "enter": "Enter room",
"error": "Error: your message was not sent. Reason: {{error}}", "error": "Error: your message was not sent. Reason: {{error}}",
@ -1010,6 +1021,7 @@
"boo": "Boo", "boo": "Boo",
"breakoutRoom": "Join/leave breakout room", "breakoutRoom": "Join/leave breakout room",
"callQuality": "Manage video quality", "callQuality": "Manage video quality",
"carmode": "Carmode",
"cc": "Toggle subtitles", "cc": "Toggle subtitles",
"chat": "Open / Close chat", "chat": "Open / Close chat",
"clap": "Clap", "clap": "Clap",

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

@ -38,6 +38,12 @@ export const CALENDAR_ENABLED = 'calendar.enabled';
*/ */
export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled'; export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
/**
* Flag indicating if car mode should be enabled.
* Default: enabled (true).
*/
export const CAR_MODE_ENABLED = 'car-mode.enabled';
/** /**
* Flag indicating if close captions should be enabled. * Flag indicating if close captions should be enabled.
* Default: enabled (true). * Default: enabled (true).

View File

@ -1,3 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.44701 5H15.553C16.427 5 17.1997 5.56747 17.4613 6.40136L18.2765 9H18H6H5.7235L6.5387 6.40136C6.8003 5.56747 7.57305 5 8.44701 5ZM3.29779 10.0507L4.6304 5.80272C5.15358 4.13493 6.69908 3 8.44701 3H15.553C17.3009 3 18.8464 4.13494 19.3696 5.80272L20.7022 10.0507C21.4999 10.782 22 11.8326 22 13V17V18V21H20V18H4V21H2V18V17V13C2 11.8326 2.50012 10.782 3.29779 10.0507ZM6 11C4.89543 11 4 11.8954 4 13V16H20V13C20 11.8954 19.1046 11 18 11H6ZM9 13.5C9 14.3284 8.32843 15 7.5 15C6.67157 15 6 14.3284 6 13.5C6 12.6716 6.67157 12 7.5 12C8.32843 12 9 12.6716 9 13.5ZM16.5 15C17.3284 15 18 14.3284 18 13.5C18 12.6716 17.3284 12 16.5 12C15.6716 12 15 12.6716 15 13.5C15 14.3284 15.6716 15 16.5 15Z"/> <path d="M7.50001 11.25C7.50001 11.9404 6.94036 12.5 6.25001 12.5C5.55965 12.5 5.00001 11.9404 5.00001 11.25C5.00001 10.5596 5.55965 10 6.25001 10C6.94036 10 7.50001 10.5596 7.50001 11.25Z" />
<path d="M13.75 12.5C14.4404 12.5 15 11.9404 15 11.25C15 10.5596 14.4404 10 13.75 10C13.0596 10 12.5 10.5596 12.5 11.25C12.5 11.9404 13.0596 12.5 13.75 12.5Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.74816 8.3756L3.85867 4.8356C4.29466 3.44578 5.58258 2.5 7.03918 2.5H12.9608C14.4174 2.5 15.7054 3.44578 16.1413 4.8356L17.2518 8.3756C17.9166 8.98497 18.3333 9.86048 18.3333 10.8333V17.5H16.6667V15H3.33334V17.5H1.66667V10.8333C1.66667 9.86048 2.08344 8.98497 2.74816 8.3756ZM7.03918 4.16667H12.9608C13.6891 4.16667 14.3331 4.63956 14.5511 5.33447L15.2304 7.5H4.76959L5.44892 5.33447C5.66692 4.63956 6.31088 4.16667 7.03918 4.16667ZM5.00001 9.16667C4.07953 9.16667 3.33334 9.91286 3.33334 10.8333V13.3333H16.6667V10.8333C16.6667 9.91286 15.9205 9.16667 15 9.16667H5.00001Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 1.0 KiB

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 IconChatUnread } from './chat-unread.svg';
export { default as IconCheck } from './check.svg'; export { default as IconCheck } from './check.svg';
export { default as IconCheckSolid } from './check-solid.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 IconClose } from './close.svg';
export { default as IconCloseCircle } from './close-circle.svg'; export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseSolid } from './close-solid.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 { APP_STATE_CHANGED } from '../../mobile/background/actionTypes';
import { import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_CAR_MODE,
SET_TILE_VIEW, SET_TILE_VIEW,
VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED
} from '../../video-layout/actionTypes'; } from '../../video-layout/actionTypes';
@ -51,6 +52,7 @@ const _updateLastN = debounce(({ dispatch, getState }) => {
const config = state['features/base/config']; const config = state['features/base/config'];
const { lastNLimits } = state['features/base/lastn']; const { lastNLimits } = state['features/base/lastn'];
const participantCount = getParticipantCount(state); const participantCount = getParticipantCount(state);
const { carMode } = state['features/video-layout'];
// Select the (initial) lastN value based on the following preference order. // Select the (initial) lastN value based on the following preference order.
// 1. The last-n value from 'startLastN' if it is specified in config.js // 1. The last-n value from 'startLastN' if it is specified in config.js
@ -67,6 +69,8 @@ const _updateLastN = debounce(({ dispatch, getState }) => {
if (typeof appState !== 'undefined' && appState !== 'active') { if (typeof appState !== 'undefined' && appState !== 'active') {
lastNSelected = isLocalVideoTrackDesktop(state) ? 1 : 0; lastNSelected = isLocalVideoTrackDesktop(state) ? 1 : 0;
} else if (carMode) {
lastNSelected = 0;
} else if (audioOnly) { } else if (audioOnly) {
const { remoteScreenShares, tileViewEnabled } = state['features/video-layout']; const { remoteScreenShares, tileViewEnabled } = state['features/video-layout'];
const largeVideoParticipantId = state['features/large-video'].participantId; const largeVideoParticipantId = state['features/large-video'].participantId;
@ -101,6 +105,7 @@ MiddlewareRegistry.register(store => next => action => {
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case SELECT_LARGE_VIDEO_PARTICIPANT: case SELECT_LARGE_VIDEO_PARTICIPANT:
case SET_AUDIO_ONLY: case SET_AUDIO_ONLY:
case SET_CAR_MODE:
case SET_FILMSTRIP_ENABLED: case SET_FILMSTRIP_ENABLED:
case SET_TILE_VIEW: case SET_TILE_VIEW:
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED: case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED:

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

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

View File

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

View File

@ -1 +1,30 @@
export * from './functions.any'; 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 abstractMapStateToProps
} from '../AbstractConference'; } from '../AbstractConference';
import type { AbstractProps } from '../AbstractConference'; import type { AbstractProps } from '../AbstractConference';
import { isConnecting } from '../functions';
import AlwaysOnLabels from './AlwaysOnLabels'; import AlwaysOnLabels from './AlwaysOnLabels';
import ExpandedLabelPopup from './ExpandedLabelPopup'; import ExpandedLabelPopup from './ExpandedLabelPopup';
@ -496,34 +497,15 @@ class Conference extends AbstractConference<Props, State> {
* @returns {Props} * @returns {Props}
*/ */
function _mapStateToProps(state) { 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 { isOpen } = state['features/participants-pane'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui']; const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
const participantCount = getParticipantCount(state); 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 { return {
...abstractMapStateToProps(state), ...abstractMapStateToProps(state),
_aspectRatio: aspectRatio, _aspectRatio: aspectRatio,
_calendarEnabled: isCalendarEnabled(state), _calendarEnabled: isCalendarEnabled(state),
_connecting: Boolean(connecting_), _connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state), _filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true), _fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isOneToOneConference: Boolean(participantCount === 2), _isOneToOneConference: Boolean(participantCount === 2),

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,82 @@
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 { useDispatch, useSelector } from 'react-redux';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import { LoadingIndicator, TintedView } from '../../../../base/react';
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';
import { isLocalVideoTrackDesktop } from '../../../../base/tracks';
import { setPictureInPictureDisabled } from '../../../../mobile/picture-in-picture/functions';
/**
* Implements the carmode tab.
*
* @returns { JSX.Element} - The carmode tab.
*/
const CarmodeTab = (): JSX.Element => {
const dispatch = useDispatch();
const { t } = useTranslation();
const connecting = useSelector(isConnecting);
const isSharing = useSelector(isLocalVideoTrackDesktop);
useEffect(() => {
dispatch(setIsCarmode(true));
setPictureInPictureDisabled(true);
return () => {
dispatch(setIsCarmode(false));
if (!isSharing) {
setPictureInPictureDisabled(false);
}
}
}, []);
return (
<JitsiScreen 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 }>
<View
style = { styles.titleBar }>
<TitleBar />
</View>
<ConferenceTimer textStyle = { styles.roomTimer } />
</View>
<View
pointerEvents = 'box-none'
style = { styles.microphoneContainer }>
<MicrophoneButton />
</View>
<View
pointerEvents = 'box-none'
style = { styles.bottomContainer }>
<Text style = { styles.videoStoppedLabel }>
{t('carmode.labels.videoStopped')}
</Text>
<SoundDeviceButton />
<EndMeetingButton />
</View>
</JitsiScreen>
);
};
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,88 @@
import React, { useCallback } from 'react';
import { useState } from 'react';
import { View, TouchableOpacity } from 'react-native';
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 { 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';
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 => {
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));
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
onPressIn = { onPressIn }
onLongPress={ onLongPress }
onPressOut={ onPressOut } >
<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,81 @@
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 { 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.qualityLabelContainer }>
<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,207 @@
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
/**
* The size of the microphone icon.
*/
const MICROPHONE_SIZE = 180;
/**
* 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: {
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: {
borderBottomLeftRadius: 3,
borderTopLeftRadius: 3,
flexShrink: 1,
paddingHorizontal: 2,
justifyContent: 'center',
marginTop: 8
},
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,
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: 10,
flexShrink: 1,
flexGrow: 1
},
roomNameView: {
backgroundColor: 'rgba(0,0,0,0.6)',
flexShrink: 1,
justifyContent: 'center',
paddingHorizontal: 5
},
roomName: {
color: BaseTheme.palette.text01,
...BaseTheme.typography.bodyShortBold
},
titleBar: {
alignSelf: 'center'
},
videoStoppedLabel: {
color: BaseTheme.palette.text01,
marginBottom: 32,
...BaseTheme.typography.bodyShortRegularLarge
}
};

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

@ -4,8 +4,10 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Chat } from '../../../../../chat'; import { Chat, ChatAndPolls } from '../../../../../chat';
import Conference from '../../../../../conference/components/native/Conference'; import Conference from '../../../../../conference/components/native/Conference';
import CarmodeTab from '../../../../../conference/components/native/carmode/Conference';
import { getDisablePolls } from '../../../../../conference/functions'; import { getDisablePolls } from '../../../../../conference/functions';
import { SharedDocument } from '../../../../../etherpad'; import { SharedDocument } from '../../../../../etherpad';
import { GifsMenu } from '../../../../../gifs/components'; import { GifsMenu } from '../../../../../gifs/components';
@ -23,6 +25,7 @@ import SpeakerStats
from '../../../../../speaker-stats/components/native/SpeakerStats'; from '../../../../../speaker-stats/components/native/SpeakerStats';
import { screen } from '../../../routes'; import { screen } from '../../../routes';
import { import {
carmodeScreenOptions,
chatScreenOptions, chatScreenOptions,
conferenceScreenOptions, conferenceScreenOptions,
gifsMenuOptions, gifsMenuOptions,
@ -36,8 +39,6 @@ import {
sharedDocumentScreenOptions, sharedDocumentScreenOptions,
speakerStatsScreenOptions speakerStatsScreenOptions
} from '../../../screenOptions'; } from '../../../screenOptions';
import ChatAndPollsNavigationContainer
from '../../chat/components/ChatAndPollsNavigationContainer';
import LobbyNavigationContainer import LobbyNavigationContainer
from '../../lobby/components/LobbyNavigationContainer'; from '../../lobby/components/LobbyNavigationContainer';
import { import {
@ -46,7 +47,6 @@ import {
const ConferenceStack = createStackNavigator(); const ConferenceStack = createStackNavigator();
const ConferenceNavigationContainer = () => { const ConferenceNavigationContainer = () => {
const isPollsDisabled = useSelector(getDisablePolls); const isPollsDisabled = useSelector(getDisablePolls);
let ChatScreen; let ChatScreen;
@ -58,7 +58,7 @@ const ConferenceNavigationContainer = () => {
chatScreenName = screen.conference.chat; chatScreenName = screen.conference.chat;
chatTitleString = 'chat.title'; chatTitleString = 'chat.title';
} else { } else {
ChatScreen = ChatAndPollsNavigationContainer; ChatScreen = ChatAndPolls;
chatScreenName = screen.conference.chatandpolls.main; chatScreenName = screen.conference.chatandpolls.main;
chatTitleString = 'chat.titleWithPolls'; chatTitleString = 'chat.titleWithPolls';
} }
@ -152,9 +152,16 @@ const ConferenceNavigationContainer = () => {
...sharedDocumentScreenOptions, ...sharedDocumentScreenOptions,
title: t('documentSharing.title') title: t('documentSharing.title')
}} /> }} />
<ConferenceStack.Screen
component = { CarmodeTab }
name = { screen.conference.carmode }
options = {{
...carmodeScreenOptions,
title: t('carmode.labels.title')
}}/>
</ConferenceStack.Navigator> </ConferenceStack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
}; };
export default ConferenceNavigationContainer; export default ConferenceNavigationContainer;

View File

@ -16,6 +16,7 @@ export const screen = {
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',
@ -24,6 +25,7 @@ export const screen = {
polls: 'Polls' polls: 'Polls'
} }
}, },
container: 'Conference container',
security: 'Security Options', security: 'Security Options',
recording: 'Recording', recording: 'Recording',
liveStream: 'Live stream', liveStream: 'Live stream',

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,46 @@
import { CAR_MODE_ENABLED, getFeatureFlag } from '../../../base/flags';
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);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {AbstractButtonProps} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: Object, ownProps: AbstractButtonProps): Object {
const enabled = getFeatureFlag(state, CAR_MODE_ENABLED, true);
const { visible = enabled } = ownProps;
return {
visible
};
}
export default translate(connect(_mapStateToProps)(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

@ -11,14 +11,14 @@ export const SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED
= 'SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED'; = 'SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED';
/** /**
* The type of the action which sets the list of known remote virtual screen share participant IDs. * The type of the action which tells whether we are in carmode.
* *
* @returns {{ * @returns {{
* type: VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED, * type: SET_CAR_MODE,
* participantIds: Array<string> * enabled: boolean
* }} * }}
*/ */
export const VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED = 'VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED'; export const SET_CAR_MODE = ' SET_CAR_MODE';
/** /**
* The type of the action which enables or disables the feature for showing * The type of the action which enables or disables the feature for showing
@ -30,3 +30,13 @@ export const VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED = 'VIRTUAL_SCREENSH
* }} * }}
*/ */
export const SET_TILE_VIEW = 'SET_TILE_VIEW'; export const SET_TILE_VIEW = 'SET_TILE_VIEW';
/**
* The type of the action which sets the list of known remote virtual screen share participant IDs.
*
* @returns {{
* type: VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED,
* participantIds: Array<string>
* }}
*/
export const VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED = 'VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED';

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

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

View File

@ -4,11 +4,20 @@ import { ReducerRegistry } from '../base/redux';
import { import {
SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED, SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED,
SET_CAR_MODE,
SET_TILE_VIEW, SET_TILE_VIEW,
VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED
} from './actionTypes'; } from './actionTypes';
const DEFAULT_STATE = { const DEFAULT_STATE = {
/**
* Whether we are in carmode.
*
* @public
* @type {boolean}
*/
carMode: false,
remoteScreenShares: [], remoteScreenShares: [],
/** /**
@ -29,12 +38,17 @@ const STORE_NAME = 'features/video-layout';
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED: case SCREEN_SHARE_REMOTE_PARTICIPANTS_UPDATED:
case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED: { case VIRTUAL_SCREENSHARE_REMOTE_PARTICIPANTS_UPDATED:
return { return {
...state, ...state,
remoteScreenShares: action.participantIds remoteScreenShares: action.participantIds
}; };
}
case SET_CAR_MODE:
return {
...state,
carMode: action.enabled
};
case SET_TILE_VIEW: case SET_TILE_VIEW:
return { return {