feat(prejoin) native prejoin screen and other navigation updates

* feat(prejoin) created native Prejoin screen

* feat(prejoin) fixed useState callback and updates warnings

* feat(prejoin) created styles file

* feat(prejoin) moved nav from middleware to appNavigate, created native DeviceStatus

* feat(prejoin) updated styles

* feat(prejoin) review remarks pt. 1

* feat(prejoin) removed unused styles

* feat(prejoin) review remarks pt. 2

* feat(prejoin) comment fix

* feat(prejoin) added header title

* feat(prejoin) review remarks

* feat(lobby) updated styles

* feat(prejoin) updated lobby screen header button functionality

* feat(prejoin) review remarks pt 3

* feat(welcome) removed VideoSwitch component

* feat(mobile/navigation) fixed linter

* feat(welcome) moved isWelcomePageEnabled to functions.ts

* feat(mobile/navigation) screen options and order updates

* feat(app) review remark

* feat(welcome) added translation for screen header title and fixed build

* feat(mobile/navigation) added screen title translation and created screen option

* feat(mobile/navigation) fixed screenOptions import

* feat(mobile/navigation) added DialInSummary title translation, fixed animation and close button

* feat(welcome) fixed build

* feat(welcome) removed extra check

* feat(prejoin) review remarks pt 4

* feat(prejoin) added Join in low bandwidth mode btn

* feat(welcome) changed welcome screen header title

* fixup lobby close
This commit is contained in:
Calinteodor 2022-06-16 12:49:07 +03:00 committed by GitHub
parent d31eb3b248
commit dbf7bf4750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 487 additions and 329 deletions

View File

@ -208,6 +208,9 @@
"selectADevice": "Select a device", "selectADevice": "Select a device",
"testAudio": "Play a test sound" "testAudio": "Play a test sound"
}, },
"dialIn": {
"screenTitle": "Dial-in summary"
},
"dialOut": { "dialOut": {
"statusMessage": "is now {{status}}" "statusMessage": "is now {{status}}"
}, },
@ -815,6 +818,7 @@
"initiated": "Call initiated", "initiated": "Call initiated",
"joinAudioByPhone": "Join with phone audio", "joinAudioByPhone": "Join with phone audio",
"joinMeeting": "Join meeting", "joinMeeting": "Join meeting",
"joinMeetingInLowBandwidthMode": "Join in low bandwidth mode",
"joinWithoutAudio": "Join without audio", "joinWithoutAudio": "Join without audio",
"keyboardShortcuts": "Enable Keyboard shortcuts", "keyboardShortcuts": "Enable Keyboard shortcuts",
"linkCopied": "Link copied to clipboard", "linkCopied": "Link copied to clipboard",
@ -949,6 +953,7 @@
"playSounds": "Play sound on", "playSounds": "Play sound on",
"reactions": "Meeting reactions", "reactions": "Meeting reactions",
"sameAsSystem": "Same as system ({{label}})", "sameAsSystem": "Same as system ({{label}})",
"screenTitle": "Settings",
"selectAudioOutput": "Audio output", "selectAudioOutput": "Audio output",
"selectCamera": "Camera", "selectCamera": "Camera",
"selectMic": "Microphone", "selectMic": "Microphone",

View File

@ -19,7 +19,11 @@ import {
parseURIString, parseURIString,
toURLString toURLString
} from '../base/util'; } from '../base/util';
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef'; import { isPrejoinPageEnabled } from '../mobile/navigation/functions';
import {
goBackToRoot,
navigateRoot
} from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes'; import { screen } from '../mobile/navigation/routes';
import { setFatalError } from '../overlay'; import { setFatalError } from '../overlay';
@ -128,7 +132,15 @@ export function appNavigate(uri: ?string) {
if (room) { if (room) {
dispatch(createDesiredLocalTracks()); dispatch(createDesiredLocalTracks());
dispatch(connect());
if (isPrejoinPageEnabled(getState())) {
navigateRoot(screen.preJoin);
} else {
dispatch(connect());
navigateRoot(screen.conference.root);
}
} else {
goBackToRoot(getState(), dispatch);
} }
}; };
} }

View File

@ -7,7 +7,8 @@ import { toState } from '../base/redux';
import { Conference } from '../conference'; import { Conference } from '../conference';
import { getDeepLinkingPage } from '../deep-linking'; import { getDeepLinkingPage } from '../deep-linking';
import { UnsupportedDesktopBrowser } from '../unsupported-browser'; import { UnsupportedDesktopBrowser } from '../unsupported-browser';
import { BlankPage, isWelcomePageUserEnabled, WelcomePage } from '../welcome'; import { BlankPage, WelcomePage } from '../welcome';
import { isWelcomePageEnabled } from '../welcome/functions';
/** /**
* Determines which route is to be rendered in order to depict a specific Redux * Determines which route is to be rendered in order to depict a specific Redux
@ -72,7 +73,7 @@ function _getWebConferenceRoute(state) {
function _getWebWelcomePageRoute(state) { function _getWebWelcomePageRoute(state) {
const route = _getEmptyRoute(); const route = _getEmptyRoute();
if (isWelcomePageUserEnabled(state)) { if (isWelcomePageEnabled(state)) {
if (isSupportedBrowser()) { if (isSupportedBrowser()) {
route.component = WelcomePage; route.component = WelcomePage;
} else { } else {

View File

@ -160,6 +160,12 @@ export const OVERFLOW_MENU_ENABLED = 'overflow-menu.enabled';
*/ */
export const PIP_ENABLED = 'pip.enabled'; export const PIP_ENABLED = 'pip.enabled';
/**
* Flag indicating if the prejoin page should be enabled.
* Default: enabled (true).
*/
export const PREJOIN_PAGE_ENABLED = 'prejoinpage.enabled';
/** /**
* Flag indicating if raise hand feature should be enabled. * Flag indicating if raise hand feature should be enabled.
* Default: enabled. * Default: enabled.

View File

@ -1,23 +1,21 @@
// @flow // @flow
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Linking, Platform, View } from 'react-native'; import { Linking, View } from 'react-native';
import { WebView } from 'react-native-webview'; import { WebView } from 'react-native-webview';
import { type Dispatch } from 'redux'; import { type Dispatch } from 'redux';
import { openDialog } from '../../../../base/dialog'; import { openDialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n'; import { translate } from '../../../../base/i18n';
import { IconClose } from '../../../../base/icons';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen'; import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import { LoadingIndicator } from '../../../../base/react'; import { LoadingIndicator } from '../../../../base/react';
import { connect } from '../../../../base/redux'; import { connect } from '../../../../base/redux';
import HeaderNavigationButton
from '../../../../mobile/navigation/components/HeaderNavigationButton';
import { getDialInfoPageURLForURIString } from '../../../functions'; import { getDialInfoPageURLForURIString } from '../../../functions';
import DialInSummaryErrorDialog from './DialInSummaryErrorDialog'; import DialInSummaryErrorDialog from './DialInSummaryErrorDialog';
import styles, { INDICATOR_COLOR } from './styles'; import styles, { INDICATOR_COLOR } from './styles';
type Props = { type Props = {
dispatch: Dispatch<any>, dispatch: Dispatch<any>,
@ -65,28 +63,9 @@ class DialInSummary extends PureComponent<Props> {
*/ */
componentDidMount() { componentDidMount() {
const { navigation, t } = this.props; const { navigation, t } = this.props;
const onNavigationClose = () => {
navigation.goBack();
};
navigation.setOptions({ navigation.setOptions({
headerLeft: () => { headerTitle: t('dialIn.screenTitle')
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
// eslint-disable-next-line react/jsx-no-bind
onPress = { onNavigationClose } />
);
}
return (
<HeaderNavigationButton
// eslint-disable-next-line react/jsx-no-bind
onPress = { onNavigationClose }
src = { IconClose } />
);
}
}); });
} }

View File

@ -15,12 +15,10 @@ export default {
buttonStylesBorderless: { buttonStylesBorderless: {
iconStyle: { iconStyle: {
backgroundColor: 'transparent',
color: BaseTheme.palette.icon01, color: BaseTheme.palette.icon01,
fontSize: 24 fontSize: 24
}, },
style: { style: {
backgroundColor: 'transparent',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[3], marginHorizontal: BaseTheme.spacing[3],
@ -70,25 +68,25 @@ export default {
}, },
largeVideoContainerWide: { largeVideoContainerWide: {
position: 'absolute',
marginRight: 'auto',
height: '100%', height: '100%',
marginRight: 'auto',
position: 'absolute',
width: '50%' width: '50%'
}, },
contentContainer: { contentContainer: {
display: 'flex',
alignItems: 'center', alignItems: 'center',
display: 'flex',
justifyContent: 'center', justifyContent: 'center',
minHeight: '50%' minHeight: '50%'
}, },
contentContainerWide: { contentContainerWide: {
height: '100%',
justifyContent: 'center', justifyContent: 'center',
left: '50%',
marginHorizontal: BaseTheme.spacing[6], marginHorizontal: BaseTheme.spacing[6],
marginVertical: BaseTheme.spacing[3], marginVertical: BaseTheme.spacing[3],
height: '100%',
left: '50%',
position: 'absolute', position: 'absolute',
width: '50%' width: '50%'
}, },
@ -124,7 +122,7 @@ export default {
formWrapper: { formWrapper: {
alignSelf: 'stretch', alignSelf: 'stretch',
marginTop: 45 justifyContent: 'center'
}, },
field: { field: {

View File

@ -1,8 +1,11 @@
// @flow // @flow
import debounce from 'lodash/debounce';
import { NativeModules } from 'react-native'; import { NativeModules } from 'react-native';
import { getAppProp } from '../../base/app'; import { getAppProp } from '../../base/app';
import { readyToClose } from '../external-api/actions';
/** /**
* Sends a specific event to the native counterpart of the External API. Native * Sends a specific event to the native counterpart of the External API. Native
@ -24,3 +27,10 @@ export function sendEvent(store: Object, name: string, data: Object) {
externalAPIScope externalAPIScope
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope); && NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
} }
/**
* Debounced sending of `readyToClose`.
*/
export const _sendReadyToClose = debounce(dispatch => {
dispatch(readyToClose());
}, 2500, { leading: true });

View File

@ -4,20 +4,25 @@ import React, { useCallback } from 'react';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { DialInSummary } from '../../../invite'; import { DialInSummary } from '../../../invite';
import Prejoin from '../../../prejoin/components/Prejoin.native';
import { isWelcomePageEnabled } from '../../../welcome/functions';
import { _ROOT_NAVIGATION_READY } from '../actionTypes'; import { _ROOT_NAVIGATION_READY } from '../actionTypes';
import { rootNavigationRef } from '../rootNavigationContainerRef'; import { rootNavigationRef } from '../rootNavigationContainerRef';
import { screen } from '../routes'; import { screen } from '../routes';
import { import {
conferenceNavigationContainerScreenOptions,
connectingScreenOptions,
dialInSummaryScreenOptions, dialInSummaryScreenOptions,
drawerNavigatorScreenOptions, drawerNavigatorScreenOptions,
navigationContainerTheme navigationContainerTheme,
preJoinScreenOptions
} from '../screenOptions'; } from '../screenOptions';
import ConnectingPage from './ConnectingPage'; import ConnectingPage from './ConnectingPage';
import ConferenceNavigationContainer import ConferenceNavigationContainer
from './conference/components/ConferenceNavigationContainer'; from './conference/components/ConferenceNavigationContainer';
import WelcomePageNavigationContainer from './welcome/components/WelcomePageNavigationContainer'; import WelcomePageNavigationContainer
import { isWelcomePageAppEnabled } from './welcome/functions'; from './welcome/components/WelcomePageNavigationContainer';
const RootStack = createNativeStackNavigator(); const RootStack = createNativeStackNavigator();
@ -70,17 +75,15 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) =>
<RootStack.Screen <RootStack.Screen
component = { ConnectingPage } component = { ConnectingPage }
name = { screen.connecting } name = { screen.connecting }
options = {{ options = { connectingScreenOptions } />
gestureEnabled: false, <RootStack.Screen
headerShown: false component = { Prejoin }
}} /> name = { screen.preJoin }
options = { preJoinScreenOptions } />
<RootStack.Screen <RootStack.Screen
component = { ConferenceNavigationContainer } component = { ConferenceNavigationContainer }
name = { screen.conference.root } name = { screen.conference.root }
options = {{ options = { conferenceNavigationContainerScreenOptions } />
gestureEnabled: false,
headerShown: false
}} />
</RootStack.Navigator> </RootStack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
@ -94,7 +97,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) =>
*/ */
function mapStateToProps(state: Object) { function mapStateToProps(state: Object) {
return { return {
isWelcomePageAvailable: isWelcomePageAppEnabled(state) isWelcomePageAvailable: isWelcomePageEnabled(state)
}; };
} }

View File

@ -31,6 +31,7 @@ import {
gifsMenuOptions, gifsMenuOptions,
inviteScreenOptions, inviteScreenOptions,
liveStreamScreenOptions, liveStreamScreenOptions,
lobbyNavigationContainerScreenOptions,
navigationContainerTheme, navigationContainerTheme,
participantsScreenOptions, participantsScreenOptions,
recordingScreenOptions, recordingScreenOptions,
@ -101,15 +102,11 @@ const ConferenceNavigationContainer = () => {
<ConferenceStack.Screen <ConferenceStack.Screen
component = { StartRecordingDialog } component = { StartRecordingDialog }
name = { screen.conference.recording } name = { screen.conference.recording }
options = {{ options = { recordingScreenOptions } />
...recordingScreenOptions
}} />
<ConferenceStack.Screen <ConferenceStack.Screen
component = { StartLiveStreamDialog } component = { StartLiveStreamDialog }
name = { screen.conference.liveStream } name = { screen.conference.liveStream }
options = {{ options = { liveStreamScreenOptions } />
...liveStreamScreenOptions
}} />
<ConferenceStack.Screen <ConferenceStack.Screen
component = { SpeakerStats } component = { SpeakerStats }
name = { screen.conference.speakerStats } name = { screen.conference.speakerStats }
@ -134,10 +131,7 @@ const ConferenceNavigationContainer = () => {
<ConferenceStack.Screen <ConferenceStack.Screen
component = { LobbyNavigationContainer } component = { LobbyNavigationContainer }
name = { screen.lobby.root } name = { screen.lobby.root }
options = {{ options = { lobbyNavigationContainerScreenOptions } />
gestureEnabled: false,
headerShown: false
}} />
<ConferenceStack.Screen <ConferenceStack.Screen
component = { AddPeopleDialog } component = { AddPeopleDialog }
name = { screen.conference.invite } name = { screen.conference.invite }
@ -158,7 +152,7 @@ const ConferenceNavigationContainer = () => {
options = {{ options = {{
...carmodeScreenOptions, ...carmodeScreenOptions,
title: t('carmode.labels.title') title: t('carmode.labels.title')
}}/> }} />
</ConferenceStack.Navigator> </ConferenceStack.Navigator>
</NavigationContainer> </NavigationContainer>
); );

View File

@ -2,25 +2,11 @@
import React from 'react'; import React from 'react';
import { getFeatureFlag, WELCOME_PAGE_ENABLED } from '../../../../base/flags';
import { IconArrowBack } from '../../../../base/icons'; import { IconArrowBack } from '../../../../base/icons';
import HeaderNavigationButton import HeaderNavigationButton
from '../HeaderNavigationButton'; from '../HeaderNavigationButton';
import { navigationStyles } from '../styles'; import { navigationStyles } from '../styles';
/**
* Determines whether the {@code WelcomePage} is enabled by the app itself
* (e.g. Programmatically via the Jitsi Meet SDK for Android and iOS). Not to be
* confused with {@link isWelcomePageUserEnabled}.
*
* @param {Function|Object} stateful - The redux state or {@link getState}
* function.
* @returns {boolean} If the {@code WelcomePage} is enabled by the app, then
* {@code true}; otherwise, {@code false}.
*/
export function isWelcomePageAppEnabled(stateful: Function | Object) {
return Boolean(getFeatureFlag(stateful, WELCOME_PAGE_ENABLED));
}
/** /**
* Render header arrow back button for navigation. * Render header arrow back button for navigation.

View File

@ -1,11 +1,19 @@
import React from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { useDispatch } from 'react-redux';
import { appNavigate } from '../../app/actions';
import {
getFeatureFlag,
PREJOIN_PAGE_ENABLED
} from '../../base/flags';
import { IconClose } from '../../base/icons'; import { IconClose } from '../../base/icons';
import { cancelKnocking } from '../../lobby/actions.native';
import HeaderNavigationButton from './components/HeaderNavigationButton'; import HeaderNavigationButton from './components/HeaderNavigationButton';
/** /**
* Close icon/text button based on platform. * Close icon/text button based on platform.
* *
@ -29,3 +37,44 @@ export function screenHeaderCloseButton(goBack: Function) {
src = { IconClose } /> src = { IconClose } />
); );
} }
/**
* Determines whether the {@code Prejoin page} is enabled by the app itself
* (e.g. Programmatically via the Jitsi Meet SDK for Android and iOS).
*
* @param {Function|Object} stateful - The redux state or {@link getState}
* function.
* @returns {boolean} If the {@code Prejoin} is enabled by the app, then
* {@code true}; otherwise, {@code false}.
*/
export function isPrejoinPageEnabled(stateful: Function | Object) {
return getFeatureFlag(stateful, PREJOIN_PAGE_ENABLED, true);
}
/**
* Close icon/text button for lobby screen based on platform.
*
* @returns {React.Component}
*/
export function lobbyScreenHeaderCloseButton() {
const dispatch = useDispatch();
const { t } = useTranslation();
const goBack = useCallback(() => {
dispatch(cancelKnocking());
dispatch(appNavigate(undefined));
}, [ dispatch ]);
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconClose } />
);
}

View File

@ -1,20 +1,11 @@
import debounce from 'lodash/debounce'; import { appNavigate } from '../../app/actions';
import { CONFERENCE_FAILED } from '../../base/conference/actionTypes';
import { CONFERENCE_FAILED, SET_ROOM } from '../../base/conference/actionTypes';
import { JitsiConferenceErrors } from '../../base/lib-jitsi-meet'; import { JitsiConferenceErrors } from '../../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../../base/redux'; import { MiddlewareRegistry } from '../../base/redux';
import { readyToClose } from '../external-api/actions';
import { isWelcomePageAppEnabled } from './components/welcome/functions';
import { navigateRoot } from './rootNavigationContainerRef';
import { screen } from './routes';
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case SET_ROOM:
return _setRoom(store, next, action);
case CONFERENCE_FAILED: case CONFERENCE_FAILED:
return _conferenceFailed(store, next, action); return _conferenceFailed(store, next, action);
@ -23,47 +14,6 @@ MiddlewareRegistry.register(store => next => action => {
return next(action); return next(action);
}); });
/**
* Debounced sending of `readyToClose`.
*/
const _sendReadyToClose = debounce(dispatch => {
dispatch(readyToClose());
}, 2500, { leading: true });
/**
* Notifies the feature base/conference that the action
* {@code SET_ROOM} is being dispatched within a specific
* redux store.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The redux action {@code SET_ROOM}
* which is being dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setRoom({ dispatch, getState }, next, action) {
const { room: oldRoom } = getState()['features/base/conference'];
const result = next(action);
const { room: newRoom } = getState()['features/base/conference'];
const isWelcomePageEnabled = isWelcomePageAppEnabled(getState());
if (!oldRoom && newRoom) {
navigateRoot(screen.conference.root);
} else if (!newRoom) {
if (isWelcomePageEnabled) {
navigateRoot(screen.root);
} else {
// For JitsiSDK, WelcomePage is not available
_sendReadyToClose(dispatch);
}
}
return result;
}
/** /**
* Function to handle the conference failed event and navigate the user to the lobby screen * Function to handle the conference failed event and navigate the user to the lobby screen
* based on the failure reason. * based on the failure reason.
@ -73,20 +23,13 @@ function _setRoom({ dispatch, getState }, next, action) {
* @param {Object} action - The Redux action. * @param {Object} action - The Redux action.
* @returns {Object} * @returns {Object}
*/ */
function _conferenceFailed({ dispatch, getState }, next, action) { function _conferenceFailed({ dispatch }, next, action) {
const state = getState();
const isWelcomePageEnabled = isWelcomePageAppEnabled(state);
const { error } = action; const { error } = action;
// We need to cover the case where knocking participant // We need to cover the case where knocking participant
// is rejected from entering the conference // is rejected from entering the conference
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) { if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
if (isWelcomePageEnabled) { dispatch(appNavigate(undefined));
navigateRoot(screen.root);
} else {
// For JitsiSDK, WelcomePage is not available
_sendReadyToClose(dispatch);
}
} }
return next(action); return next(action);

View File

@ -1,10 +1,14 @@
// @flow
import React from 'react'; import React from 'react';
// $FlowExpectedError import { toState } from '../../base/redux';
import { isWelcomePageEnabled } from '../../welcome/functions';
import { _sendReadyToClose } from '../external-api/functions';
import { screen } from './routes';
export const rootNavigationRef = React.createRef(); export const rootNavigationRef = React.createRef();
/** /**
* User defined navigation action included inside the reference to the container. * User defined navigation action included inside the reference to the container.
* *
@ -13,7 +17,32 @@ export const rootNavigationRef = React.createRef();
* @returns {Function} * @returns {Function}
*/ */
export function navigateRoot(name: string, params: Object) { export function navigateRoot(name: string, params: Object) {
// $FlowExpectedError
return rootNavigationRef.current?.navigate(name, params); return rootNavigationRef.current?.navigate(name, params);
} }
/**
* User defined navigation action included inside the reference to the container.
*
* @returns {Function}
*/
export function goBack() {
return rootNavigationRef.current?.goBack();
}
/**
* Navigates back to Welcome page, if it's available.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @param {Function} dispatch - Redux dispatch function.
* @returns {void}
*/
export function goBackToRoot(stateful: Function | Object, dispatch: Function) {
const state = toState(stateful);
if (isWelcomePageEnabled(state)) {
navigateRoot(screen.root);
} else {
// For JitsiSDK, WelcomePage is not available
_sendReadyToClose(dispatch);
}
}

View File

@ -13,6 +13,7 @@ export const screen = {
}, },
dialInSummary: 'Dial-In Info', dialInSummary: 'Dial-In Info',
connecting: 'Connecting', connecting: 'Connecting',
preJoin: 'Pre-Join',
conference: { conference: {
root: 'Conference root', root: 'Conference root',
main: 'Conference', main: 'Conference',

View File

@ -12,7 +12,8 @@ import BaseTheme from '../../base/ui/components/BaseTheme.native';
import { goBack } from './components/conference/ConferenceNavigationContainerRef'; import { goBack } from './components/conference/ConferenceNavigationContainerRef';
import { goBack as goBackToLobbyScreen } from './components/lobby/LobbyNavigationContainerRef'; import { goBack as goBackToLobbyScreen } from './components/lobby/LobbyNavigationContainerRef';
import { screenHeaderCloseButton } from './functions'; import { lobbyScreenHeaderCloseButton, screenHeaderCloseButton } from './functions';
import { goBack as goBackToWelcomeScreen } from './rootNavigationContainerRef';
/** /**
@ -81,8 +82,9 @@ export const welcomeScreenOptions = {
headerStyle: { headerStyle: {
backgroundColor: BaseTheme.palette.screen01Header backgroundColor: BaseTheme.palette.screen01Header
}, },
// eslint-disable-next-line no-empty-function headerTitleStyle: {
headerTitle: () => {} color: BaseTheme.palette.text01
}
}; };
/** /**
@ -164,7 +166,6 @@ export const chatTabBarOptions = {
* Screen options for presentation type modals. * Screen options for presentation type modals.
*/ */
export const presentationScreenOptions = { export const presentationScreenOptions = {
animation: 'slide_from_right',
headerBackTitleVisible: false, headerBackTitleVisible: false,
headerLeft: () => screenHeaderCloseButton(goBack), headerLeft: () => screenHeaderCloseButton(goBack),
headerStatusBarHeight: 0, headerStatusBarHeight: 0,
@ -195,7 +196,8 @@ export const chatScreenOptions = presentationScreenOptions;
*/ */
export const dialInSummaryScreenOptions = { export const dialInSummaryScreenOptions = {
...presentationScreenOptions, ...presentationScreenOptions,
headerLeft: undefined animation: 'slide_from_bottom',
headerLeft: () => screenHeaderCloseButton(goBackToWelcomeScreen)
}; };
/** /**
@ -231,7 +233,10 @@ export const liveStreamScreenOptions = presentationScreenOptions;
/** /**
* Screen options for lobby modal. * Screen options for lobby modal.
*/ */
export const lobbyScreenOptions = presentationScreenOptions; export const lobbyScreenOptions = {
...presentationScreenOptions,
headerLeft: () => lobbyScreenHeaderCloseButton()
};
/** /**
* Screen options for lobby chat modal. * Screen options for lobby chat modal.
@ -269,3 +274,38 @@ export const sharedDocumentScreenOptions = {
android: 'all' android: 'all'
}) })
}; };
/**
* Screen options for connecting screen.
*/
export const connectingScreenOptions = {
gestureEnabled: false,
headerShown: false
};
/**
* Screen options for pre-join screen.
*/
export const preJoinScreenOptions = {
gestureEnabled: false,
headerStyle: {
backgroundColor: BaseTheme.palette.screen02Header
},
headerTitle: ''
};
/**
* Screen options for conference navigation container screen.
*/
export const conferenceNavigationContainerScreenOptions = {
gestureEnabled: false,
headerShown: false
};
/**
* Screen options for lobby navigation container screen.
*/
export const lobbyNavigationContainerScreenOptions = {
gestureEnabled: false,
headerShown: false
};

View File

@ -0,0 +1,147 @@
import React, { useCallback, useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View, TouchableOpacity, TextInput, Platform } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { appNavigate } from '../../app/actions.native';
import { setAudioOnly } from '../../base/audio-only/actions';
import { connect } from '../../base/connection/actions.native';
import { IconClose } from '../../base/icons';
import JitsiScreen from '../../base/modal/components/JitsiScreen';
import { getLocalParticipant } from '../../base/participants';
import { getFieldValue } from '../../base/react';
import { ASPECT_RATIO_NARROW } from '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import { LargeVideo } from '../../large-video/components';
import HeaderNavigationButton from '../../mobile/navigation/components/HeaderNavigationButton';
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../mobile/navigation/routes';
import AudioMuteButton from '../../toolbox/components/AudioMuteButton';
import VideoMuteButton from '../../toolbox/components/VideoMuteButton';
import styles from './styles';
interface Props {
navigation: any;
}
const Prejoin: ({ navigation }: Props) => JSX.Element = ({ navigation }: Props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const aspectRatio = useSelector(
(state: any) => state['features/base/responsive-ui']?.aspectRatio
);
const localParticipant = useSelector(state => getLocalParticipant(state));
const participantName = localParticipant?.name;
const [ displayName, setDisplayName ]
= useState(participantName || '');
const onChangeDisplayName = useCallback(event => {
const fieldValue = getFieldValue(event);
setDisplayName(fieldValue);
dispatch(updateSettings({
displayName: fieldValue
}));
}, [ displayName ]);
const onJoin = useCallback(() => {
dispatch(connect());
navigateRoot(screen.conference.root);
}, [ dispatch ]);
const onJoinLowBandwidth = useCallback(() => {
dispatch(setAudioOnly(true));
onJoin();
}, [ dispatch ]);
const goBack = useCallback(() => {
dispatch(appNavigate(undefined));
}, [ dispatch ]);
let contentWrapperStyles;
let contentContainerStyles;
let largeVideoContainerStyles;
let toolboxContainerStyles;
if (aspectRatio === ASPECT_RATIO_NARROW) {
contentWrapperStyles = styles.contentWrapper;
contentContainerStyles = styles.contentContainer;
largeVideoContainerStyles = styles.largeVideoContainer;
toolboxContainerStyles = styles.toolboxContainer;
} else {
contentWrapperStyles = styles.contentWrapperWide;
contentContainerStyles = styles.contentContainerWide;
largeVideoContainerStyles = styles.largeVideoContainerWide;
toolboxContainerStyles = styles.toolboxContainerWide;
}
const headerLeft = useCallback(() => {
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconClose } />
);
}, []);
useLayoutEffect(() => {
navigation.setOptions({
headerLeft
});
}, [ navigation ]);
return (
<JitsiScreen
safeAreaInsets = { [ 'right' ] }
style = { contentWrapperStyles }>
<View style = { largeVideoContainerStyles }>
<LargeVideo />
</View>
<View style = { contentContainerStyles }>
<View style = { styles.formWrapper }>
<TextInput
onChangeText = { onChangeDisplayName }
placeholder = { t('dialog.enterDisplayName') }
style = { styles.field }
value = { displayName } />
<TouchableOpacity
onPress = { onJoin }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('prejoin.joinMeeting') }
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress = { onJoinLowBandwidth }
style = { [
styles.button,
styles.secondaryButton
] }>
<Text style = { styles.secondaryButtonText }>
{ t('prejoin.joinMeetingInLowBandwidthMode') }
</Text>
</TouchableOpacity>
</View>
<View style = { toolboxContainerStyles }>
<AudioMuteButton
styles = { styles.buttonStylesBorderless } />
<VideoMuteButton
styles = { styles.buttonStylesBorderless } />
</View>
</View>
</JitsiScreen>
);
};
export default Prejoin;

View File

@ -0,0 +1,121 @@
import BaseTheme from '../../base/ui/components/BaseTheme.native';
const SECONDARY_COLOR = BaseTheme.palette.border04;
const btn = {
marginTop: BaseTheme.spacing[4]
};
const btnText = {
...BaseTheme.typography.labelButtonLarge,
color: BaseTheme.palette.text01,
lineHeight: 30
};
export default {
button: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
padding: BaseTheme.spacing[2],
height: BaseTheme.spacing[7]
},
primaryButton: {
...btn,
backgroundColor: BaseTheme.palette.action01
},
primaryButtonText: {
...btnText
},
secondaryButton: {
...btn,
backgroundColor: BaseTheme.palette.action02
},
secondaryButtonText: {
...btnText
},
buttonStylesBorderless: {
iconStyle: {
color: BaseTheme.palette.icon01,
fontSize: 24
},
style: {
flexDirection: 'row',
justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[3],
height: 24,
width: 24
}
},
contentWrapper: {
flex: 1
},
contentWrapperWide: {
flex: 1,
flexDirection: 'row'
},
largeVideoContainer: {
minHeight: '50%'
},
largeVideoContainerWide: {
height: '100%',
marginRight: 'auto',
position: 'absolute',
width: '50%'
},
contentContainer: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
minHeight: '50%'
},
contentContainerWide: {
height: '100%',
justifyContent: 'center',
left: '50%',
marginHorizontal: BaseTheme.spacing[6],
marginVertical: BaseTheme.spacing[3],
position: 'absolute',
width: '50%'
},
toolboxContainer: {
alignSelf: 'center',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginTop: BaseTheme.spacing[4]
},
toolboxContainerWide: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: BaseTheme.spacing[4]
},
formWrapper: {
alignSelf: 'stretch',
justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[3]
},
field: {
backgroundColor: BaseTheme.palette.field02,
borderColor: SECONDARY_COLOR,
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 2,
height: BaseTheme.spacing[7],
marginTop: BaseTheme.spacing[2],
textAlign: 'center'
}
};

View File

@ -1,145 +0,0 @@
// @flow
import React, { Component } from 'react';
import { Switch, TouchableWithoutFeedback, View } from 'react-native';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { translate } from '../../base/i18n';
import { Text } from '../../base/react';
import { connect } from '../../base/redux';
import { updateSettings } from '../../base/settings';
import styles, { SWITCH_THUMB_COLOR, SWITCH_UNDER_COLOR } from './styles';
/**
* The type of the React {@code Component} props of {@link VideoSwitch}.
*/
type Props = {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The i18n translate function.
*/
t: Function,
/**
* Color schemed style of the header component.
*/
_headerStyles: Object,
/**
* The current settings from redux.
*/
_settings: Object
};
/**
* Renders the "Video <-> Voice" switch on the {@code WelcomePage}.
*/
class VideoSwitch extends Component<Props> {
/**
* Initializes a new {@code VideoSwitch} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onStartAudioOnlyChange = this._onStartAudioOnlyChange.bind(this);
this._onStartAudioOnlyFalse = this._onStartAudioOnlyChangeFn(false);
this._onStartAudioOnlyTrue = this._onStartAudioOnlyChangeFn(true);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { t, _headerStyles, _settings } = this.props;
return (
<View style = { styles.audioVideoSwitchContainer }>
<TouchableWithoutFeedback
onPress = { this._onStartAudioOnlyFalse }>
<View style = { styles.switchLabel }>
<Text style = { _headerStyles.headerText }>
{ t('welcomepage.audioVideoSwitch.video') }
</Text>
</View>
</TouchableWithoutFeedback>
<Switch
onValueChange = { this._onStartAudioOnlyChange }
style = { styles.audioVideoSwitch }
thumbColor = { SWITCH_THUMB_COLOR }
trackColor = {{ true: SWITCH_UNDER_COLOR }}
value = { _settings.startAudioOnly } />
<TouchableWithoutFeedback
onPress = { this._onStartAudioOnlyTrue }>
<View style = { styles.switchLabel }>
<Text style = { _headerStyles.headerText }>
{ t('welcomepage.audioVideoSwitch.audio') }
</Text>
</View>
</TouchableWithoutFeedback>
</View>
);
}
_onStartAudioOnlyChange: boolean => void;
/**
* Handles the audio-video switch changes.
*
* @private
* @param {boolean} startAudioOnly - The new startAudioOnly value.
* @returns {void}
*/
_onStartAudioOnlyChange(startAudioOnly) {
const { dispatch } = this.props;
dispatch(updateSettings({
startAudioOnly
}));
}
/**
* Creates a function that forwards the {@code startAudioOnly} changes to
* the function that handles it.
*
* @private
* @param {boolean} startAudioOnly - The new {@code startAudioOnly} value.
* @returns {void}
*/
_onStartAudioOnlyChangeFn(startAudioOnly) {
return () => this._onStartAudioOnlyChange(startAudioOnly);
}
_onStartAudioOnlyFalse: () => void;
_onStartAudioOnlyTrue: () => void;
}
/**
* Maps (parts of) the redux state to the React {@code Component} props of
* {@code VideoSwitch}.
*
* @param {Object} state - The redux state.
* @protected
* @returns {{
* _settings: Object
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header'),
_settings: state['features/base/settings']
};
}
export default translate(connect(_mapStateToProps)(VideoSwitch));

View File

@ -27,7 +27,6 @@ import {
_mapStateToProps as _abstractMapStateToProps, _mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps type Props as AbstractProps
} from './AbstractWelcomePage'; } from './AbstractWelcomePage';
import VideoSwitch from './VideoSwitch';
import styles, { PLACEHOLDER_TEXT_COLOR } from './styles'; import styles, { PLACEHOLDER_TEXT_COLOR } from './styles';
@ -101,7 +100,8 @@ class WelcomePage extends AbstractWelcomePage<*> {
const { const {
_headerStyles, _headerStyles,
navigation navigation,
t
} = this.props; } = this.props;
navigation.setOptions({ navigation.setOptions({
@ -118,9 +118,7 @@ class WelcomePage extends AbstractWelcomePage<*> {
style = { _headerStyles.headerButtonIcon } /> style = { _headerStyles.headerButtonIcon } />
</TouchableOpacity> </TouchableOpacity>
), ),
// eslint-disable-next-line react/no-multi-comp headerTitle: t('welcomepage.headerTitle')
headerRight: () =>
<VideoSwitch />
}); });
navigation.addListener('focus', () => { navigation.addListener('focus', () => {

View File

@ -159,13 +159,15 @@ class SettingsView extends AbstractSettingsView<Props, State> {
*/ */
componentDidMount() { componentDidMount() {
const { const {
navigation navigation,
t
} = this.props; } = this.props;
navigation.setOptions({ navigation.setOptions({
headerLeft: () => headerLeft: () =>
renderArrowBackButton(() => renderArrowBackButton(() =>
navigation.navigate(screen.welcome.main)) navigation.navigate(screen.welcome.main)),
headerTitle: t('settings.screenTitle')
}); });
} }

View File

@ -26,22 +26,6 @@ const TEXT_COLOR = BaseTheme.palette.text01;
*/ */
export default { export default {
/**
* The audio-video switch itself.
*/
audioVideoSwitch: {
marginHorizontal: 5
},
/**
* View that contains the audio-video switch and the labels.
*/
audioVideoSwitchContainer: {
alignItems: 'center',
flexDirection: 'row',
marginRight: BaseTheme.spacing[3]
},
blankPageText: { blankPageText: {
color: TEXT_COLOR, color: TEXT_COLOR,
fontSize: 18 fontSize: 18

View File

@ -1,24 +1,20 @@
// @flow import { WELCOME_PAGE_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { toState } from '../base/redux/functions';
import { toState } from '../base/redux';
declare var APP: Object;
/** /**
* Determines whether the {@code WelcomePage} is enabled by the user either * Determines whether the {@code WelcomePage} is enabled.
* herself or through her deployment config(uration). Not to be confused with
* {@link isWelcomePageAppEnabled}.
* *
* @param {Function|Object} stateful - The redux state or {@link getState} * @param {Function|Object} stateful - The redux state or {@link getState}
* function. * function.
* @returns {boolean} If the {@code WelcomePage} is enabled by the user, then * @returns {boolean} If the {@code WelcomePage} is enabled by the app, then
* {@code true}; otherwise, {@code false}. * {@code true}; otherwise, {@code false}.
*/ */
export function isWelcomePageUserEnabled(stateful: Function | Object) { export function isWelcomePageEnabled(stateful: Function | Object) {
return ( if (navigator.product === 'ReactNative') {
typeof APP === 'undefined' return getFeatureFlag(stateful, WELCOME_PAGE_ENABLED, false);
? true }
: toState(stateful)['features/base/config'].enableWelcomePage);
return toState(stateful)['features/base/config'].enableWelcomePage;
} }

View File

@ -1,2 +1 @@
export * from './components'; export * from './components';
export * from './functions';