feat(carmode) Add carmode screen
- opens as a modal - lastn is 0, mutes local video while open - long press to talk - and more
This commit is contained in:
parent
e628d99544
commit
61abf0d882
|
@ -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",
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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';
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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;
|
|
@ -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';
|
||||||
|
|
|
@ -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))))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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',
|
||||||
|
|
|
@ -183,6 +183,11 @@ export const presentationScreenOptions = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Screen options for car mode.
|
||||||
|
*/
|
||||||
|
export const carmodeScreenOptions = presentationScreenOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen options for chat.
|
* Screen options for chat.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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));
|
|
@ -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';
|
import ScreenSharingButton from './ScreenSharingButton';
|
||||||
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 } />
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './actions.any';
|
|
@ -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;
|
||||||
|
});
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue