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:
parent
d31eb3b248
commit
dbf7bf4750
|
@ -208,6 +208,9 @@
|
|||
"selectADevice": "Select a device",
|
||||
"testAudio": "Play a test sound"
|
||||
},
|
||||
"dialIn": {
|
||||
"screenTitle": "Dial-in summary"
|
||||
},
|
||||
"dialOut": {
|
||||
"statusMessage": "is now {{status}}"
|
||||
},
|
||||
|
@ -815,6 +818,7 @@
|
|||
"initiated": "Call initiated",
|
||||
"joinAudioByPhone": "Join with phone audio",
|
||||
"joinMeeting": "Join meeting",
|
||||
"joinMeetingInLowBandwidthMode": "Join in low bandwidth mode",
|
||||
"joinWithoutAudio": "Join without audio",
|
||||
"keyboardShortcuts": "Enable Keyboard shortcuts",
|
||||
"linkCopied": "Link copied to clipboard",
|
||||
|
@ -949,6 +953,7 @@
|
|||
"playSounds": "Play sound on",
|
||||
"reactions": "Meeting reactions",
|
||||
"sameAsSystem": "Same as system ({{label}})",
|
||||
"screenTitle": "Settings",
|
||||
"selectAudioOutput": "Audio output",
|
||||
"selectCamera": "Camera",
|
||||
"selectMic": "Microphone",
|
||||
|
|
|
@ -19,7 +19,11 @@ import {
|
|||
parseURIString,
|
||||
toURLString
|
||||
} 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 { setFatalError } from '../overlay';
|
||||
|
||||
|
@ -128,7 +132,15 @@ export function appNavigate(uri: ?string) {
|
|||
|
||||
if (room) {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
|
||||
if (isPrejoinPageEnabled(getState())) {
|
||||
navigateRoot(screen.preJoin);
|
||||
} else {
|
||||
dispatch(connect());
|
||||
navigateRoot(screen.conference.root);
|
||||
}
|
||||
} else {
|
||||
goBackToRoot(getState(), dispatch);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ import { toState } from '../base/redux';
|
|||
import { Conference } from '../conference';
|
||||
import { getDeepLinkingPage } from '../deep-linking';
|
||||
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
|
||||
|
@ -72,7 +73,7 @@ function _getWebConferenceRoute(state) {
|
|||
function _getWebWelcomePageRoute(state) {
|
||||
const route = _getEmptyRoute();
|
||||
|
||||
if (isWelcomePageUserEnabled(state)) {
|
||||
if (isWelcomePageEnabled(state)) {
|
||||
if (isSupportedBrowser()) {
|
||||
route.component = WelcomePage;
|
||||
} else {
|
||||
|
|
|
@ -160,6 +160,12 @@ export const OVERFLOW_MENU_ENABLED = 'overflow-menu.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.
|
||||
* Default: enabled.
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
// @flow
|
||||
|
||||
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 { type Dispatch } from 'redux';
|
||||
|
||||
import { openDialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { IconClose } from '../../../../base/icons';
|
||||
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
|
||||
import { LoadingIndicator } from '../../../../base/react';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import HeaderNavigationButton
|
||||
from '../../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { getDialInfoPageURLForURIString } from '../../../functions';
|
||||
|
||||
import DialInSummaryErrorDialog from './DialInSummaryErrorDialog';
|
||||
import styles, { INDICATOR_COLOR } from './styles';
|
||||
|
||||
|
||||
type Props = {
|
||||
|
||||
dispatch: Dispatch<any>,
|
||||
|
@ -65,28 +63,9 @@ class DialInSummary extends PureComponent<Props> {
|
|||
*/
|
||||
componentDidMount() {
|
||||
const { navigation, t } = this.props;
|
||||
const onNavigationClose = () => {
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
navigation.setOptions({
|
||||
headerLeft: () => {
|
||||
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 } />
|
||||
);
|
||||
}
|
||||
headerTitle: t('dialIn.screenTitle')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -15,12 +15,10 @@ export default {
|
|||
|
||||
buttonStylesBorderless: {
|
||||
iconStyle: {
|
||||
backgroundColor: 'transparent',
|
||||
color: BaseTheme.palette.icon01,
|
||||
fontSize: 24
|
||||
},
|
||||
style: {
|
||||
backgroundColor: 'transparent',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
|
@ -70,25 +68,25 @@ export default {
|
|||
},
|
||||
|
||||
largeVideoContainerWide: {
|
||||
position: 'absolute',
|
||||
marginRight: 'auto',
|
||||
height: '100%',
|
||||
marginRight: 'auto',
|
||||
position: 'absolute',
|
||||
width: '50%'
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
minHeight: '50%'
|
||||
},
|
||||
|
||||
contentContainerWide: {
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: '50%',
|
||||
marginHorizontal: BaseTheme.spacing[6],
|
||||
marginVertical: BaseTheme.spacing[3],
|
||||
height: '100%',
|
||||
left: '50%',
|
||||
position: 'absolute',
|
||||
width: '50%'
|
||||
},
|
||||
|
@ -124,7 +122,7 @@ export default {
|
|||
|
||||
formWrapper: {
|
||||
alignSelf: 'stretch',
|
||||
marginTop: 45
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
field: {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import { getAppProp } from '../../base/app';
|
||||
import { readyToClose } from '../external-api/actions';
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounced sending of `readyToClose`.
|
||||
*/
|
||||
export const _sendReadyToClose = debounce(dispatch => {
|
||||
dispatch(readyToClose());
|
||||
}, 2500, { leading: true });
|
||||
|
|
|
@ -4,20 +4,25 @@ import React, { useCallback } from 'react';
|
|||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { DialInSummary } from '../../../invite';
|
||||
import Prejoin from '../../../prejoin/components/Prejoin.native';
|
||||
import { isWelcomePageEnabled } from '../../../welcome/functions';
|
||||
import { _ROOT_NAVIGATION_READY } from '../actionTypes';
|
||||
import { rootNavigationRef } from '../rootNavigationContainerRef';
|
||||
import { screen } from '../routes';
|
||||
import {
|
||||
conferenceNavigationContainerScreenOptions,
|
||||
connectingScreenOptions,
|
||||
dialInSummaryScreenOptions,
|
||||
drawerNavigatorScreenOptions,
|
||||
navigationContainerTheme
|
||||
navigationContainerTheme,
|
||||
preJoinScreenOptions
|
||||
} from '../screenOptions';
|
||||
|
||||
import ConnectingPage from './ConnectingPage';
|
||||
import ConferenceNavigationContainer
|
||||
from './conference/components/ConferenceNavigationContainer';
|
||||
import WelcomePageNavigationContainer from './welcome/components/WelcomePageNavigationContainer';
|
||||
import { isWelcomePageAppEnabled } from './welcome/functions';
|
||||
import WelcomePageNavigationContainer
|
||||
from './welcome/components/WelcomePageNavigationContainer';
|
||||
|
||||
const RootStack = createNativeStackNavigator();
|
||||
|
||||
|
@ -70,17 +75,15 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) =>
|
|||
<RootStack.Screen
|
||||
component = { ConnectingPage }
|
||||
name = { screen.connecting }
|
||||
options = {{
|
||||
gestureEnabled: false,
|
||||
headerShown: false
|
||||
}} />
|
||||
options = { connectingScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { Prejoin }
|
||||
name = { screen.preJoin }
|
||||
options = { preJoinScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { ConferenceNavigationContainer }
|
||||
name = { screen.conference.root }
|
||||
options = {{
|
||||
gestureEnabled: false,
|
||||
headerShown: false
|
||||
}} />
|
||||
options = { conferenceNavigationContainerScreenOptions } />
|
||||
</RootStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
@ -94,7 +97,7 @@ const RootNavigationContainer = ({ dispatch, isWelcomePageAvailable }: Props) =>
|
|||
*/
|
||||
function mapStateToProps(state: Object) {
|
||||
return {
|
||||
isWelcomePageAvailable: isWelcomePageAppEnabled(state)
|
||||
isWelcomePageAvailable: isWelcomePageEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
gifsMenuOptions,
|
||||
inviteScreenOptions,
|
||||
liveStreamScreenOptions,
|
||||
lobbyNavigationContainerScreenOptions,
|
||||
navigationContainerTheme,
|
||||
participantsScreenOptions,
|
||||
recordingScreenOptions,
|
||||
|
@ -101,15 +102,11 @@ const ConferenceNavigationContainer = () => {
|
|||
<ConferenceStack.Screen
|
||||
component = { StartRecordingDialog }
|
||||
name = { screen.conference.recording }
|
||||
options = {{
|
||||
...recordingScreenOptions
|
||||
}} />
|
||||
options = { recordingScreenOptions } />
|
||||
<ConferenceStack.Screen
|
||||
component = { StartLiveStreamDialog }
|
||||
name = { screen.conference.liveStream }
|
||||
options = {{
|
||||
...liveStreamScreenOptions
|
||||
}} />
|
||||
options = { liveStreamScreenOptions } />
|
||||
<ConferenceStack.Screen
|
||||
component = { SpeakerStats }
|
||||
name = { screen.conference.speakerStats }
|
||||
|
@ -134,10 +131,7 @@ const ConferenceNavigationContainer = () => {
|
|||
<ConferenceStack.Screen
|
||||
component = { LobbyNavigationContainer }
|
||||
name = { screen.lobby.root }
|
||||
options = {{
|
||||
gestureEnabled: false,
|
||||
headerShown: false
|
||||
}} />
|
||||
options = { lobbyNavigationContainerScreenOptions } />
|
||||
<ConferenceStack.Screen
|
||||
component = { AddPeopleDialog }
|
||||
name = { screen.conference.invite }
|
||||
|
@ -158,7 +152,7 @@ const ConferenceNavigationContainer = () => {
|
|||
options = {{
|
||||
...carmodeScreenOptions,
|
||||
title: t('carmode.labels.title')
|
||||
}}/>
|
||||
}} />
|
||||
</ConferenceStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
|
|
@ -2,25 +2,11 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { getFeatureFlag, WELCOME_PAGE_ENABLED } from '../../../../base/flags';
|
||||
import { IconArrowBack } from '../../../../base/icons';
|
||||
import HeaderNavigationButton
|
||||
from '../HeaderNavigationButton';
|
||||
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.
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 { cancelKnocking } from '../../lobby/actions.native';
|
||||
|
||||
import HeaderNavigationButton from './components/HeaderNavigationButton';
|
||||
|
||||
|
||||
/**
|
||||
* Close icon/text button based on platform.
|
||||
*
|
||||
|
@ -29,3 +37,44 @@ export function screenHeaderCloseButton(goBack: Function) {
|
|||
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 } />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
|
||||
import { CONFERENCE_FAILED, SET_ROOM } from '../../base/conference/actionTypes';
|
||||
import { appNavigate } from '../../app/actions';
|
||||
import { CONFERENCE_FAILED } from '../../base/conference/actionTypes';
|
||||
import { JitsiConferenceErrors } from '../../base/lib-jitsi-meet';
|
||||
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 => {
|
||||
switch (action.type) {
|
||||
case SET_ROOM:
|
||||
return _setRoom(store, next, action);
|
||||
|
||||
case CONFERENCE_FAILED:
|
||||
return _conferenceFailed(store, next, action);
|
||||
|
@ -23,47 +14,6 @@ MiddlewareRegistry.register(store => 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
|
||||
* based on the failure reason.
|
||||
|
@ -73,20 +23,13 @@ function _setRoom({ dispatch, getState }, next, action) {
|
|||
* @param {Object} action - The Redux action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||
const state = getState();
|
||||
const isWelcomePageEnabled = isWelcomePageAppEnabled(state);
|
||||
function _conferenceFailed({ dispatch }, next, action) {
|
||||
const { error } = action;
|
||||
|
||||
// We need to cover the case where knocking participant
|
||||
// is rejected from entering the conference
|
||||
if (error.name === JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED) {
|
||||
if (isWelcomePageEnabled) {
|
||||
navigateRoot(screen.root);
|
||||
} else {
|
||||
// For JitsiSDK, WelcomePage is not available
|
||||
_sendReadyToClose(dispatch);
|
||||
}
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
// @flow
|
||||
|
||||
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();
|
||||
|
||||
|
||||
/**
|
||||
* User defined navigation action included inside the reference to the container.
|
||||
*
|
||||
|
@ -13,7 +17,32 @@ export const rootNavigationRef = React.createRef();
|
|||
* @returns {Function}
|
||||
*/
|
||||
export function navigateRoot(name: string, params: Object) {
|
||||
// $FlowExpectedError
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export const screen = {
|
|||
},
|
||||
dialInSummary: 'Dial-In Info',
|
||||
connecting: 'Connecting',
|
||||
preJoin: 'Pre-Join',
|
||||
conference: {
|
||||
root: 'Conference root',
|
||||
main: 'Conference',
|
||||
|
|
|
@ -12,7 +12,8 @@ import BaseTheme from '../../base/ui/components/BaseTheme.native';
|
|||
|
||||
import { goBack } from './components/conference/ConferenceNavigationContainerRef';
|
||||
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: {
|
||||
backgroundColor: BaseTheme.palette.screen01Header
|
||||
},
|
||||
// eslint-disable-next-line no-empty-function
|
||||
headerTitle: () => {}
|
||||
headerTitleStyle: {
|
||||
color: BaseTheme.palette.text01
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -164,7 +166,6 @@ export const chatTabBarOptions = {
|
|||
* Screen options for presentation type modals.
|
||||
*/
|
||||
export const presentationScreenOptions = {
|
||||
animation: 'slide_from_right',
|
||||
headerBackTitleVisible: false,
|
||||
headerLeft: () => screenHeaderCloseButton(goBack),
|
||||
headerStatusBarHeight: 0,
|
||||
|
@ -195,7 +196,8 @@ export const chatScreenOptions = presentationScreenOptions;
|
|||
*/
|
||||
export const dialInSummaryScreenOptions = {
|
||||
...presentationScreenOptions,
|
||||
headerLeft: undefined
|
||||
animation: 'slide_from_bottom',
|
||||
headerLeft: () => screenHeaderCloseButton(goBackToWelcomeScreen)
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -231,7 +233,10 @@ export const liveStreamScreenOptions = presentationScreenOptions;
|
|||
/**
|
||||
* Screen options for lobby modal.
|
||||
*/
|
||||
export const lobbyScreenOptions = presentationScreenOptions;
|
||||
export const lobbyScreenOptions = {
|
||||
...presentationScreenOptions,
|
||||
headerLeft: () => lobbyScreenHeaderCloseButton()
|
||||
};
|
||||
|
||||
/**
|
||||
* Screen options for lobby chat modal.
|
||||
|
@ -269,3 +274,38 @@ export const sharedDocumentScreenOptions = {
|
|||
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
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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'
|
||||
}
|
||||
};
|
|
@ -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));
|
|
@ -27,7 +27,6 @@ import {
|
|||
_mapStateToProps as _abstractMapStateToProps,
|
||||
type Props as AbstractProps
|
||||
} from './AbstractWelcomePage';
|
||||
import VideoSwitch from './VideoSwitch';
|
||||
import styles, { PLACEHOLDER_TEXT_COLOR } from './styles';
|
||||
|
||||
|
||||
|
@ -101,7 +100,8 @@ class WelcomePage extends AbstractWelcomePage<*> {
|
|||
|
||||
const {
|
||||
_headerStyles,
|
||||
navigation
|
||||
navigation,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
|
@ -118,9 +118,7 @@ class WelcomePage extends AbstractWelcomePage<*> {
|
|||
style = { _headerStyles.headerButtonIcon } />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
headerRight: () =>
|
||||
<VideoSwitch />
|
||||
headerTitle: t('welcomepage.headerTitle')
|
||||
});
|
||||
|
||||
navigation.addListener('focus', () => {
|
||||
|
|
|
@ -159,13 +159,15 @@ class SettingsView extends AbstractSettingsView<Props, State> {
|
|||
*/
|
||||
componentDidMount() {
|
||||
const {
|
||||
navigation
|
||||
navigation,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
headerLeft: () =>
|
||||
renderArrowBackButton(() =>
|
||||
navigation.navigate(screen.welcome.main))
|
||||
navigation.navigate(screen.welcome.main)),
|
||||
headerTitle: t('settings.screenTitle')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -26,22 +26,6 @@ const TEXT_COLOR = BaseTheme.palette.text01;
|
|||
*/
|
||||
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: {
|
||||
color: TEXT_COLOR,
|
||||
fontSize: 18
|
||||
|
|
|
@ -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
|
||||
* herself or through her deployment config(uration). Not to be confused with
|
||||
* {@link isWelcomePageAppEnabled}.
|
||||
* Determines whether the {@code WelcomePage} is enabled.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux state or {@link getState}
|
||||
* 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}.
|
||||
*/
|
||||
export function isWelcomePageUserEnabled(stateful: Function | Object) {
|
||||
return (
|
||||
typeof APP === 'undefined'
|
||||
? true
|
||||
: toState(stateful)['features/base/config'].enableWelcomePage);
|
||||
export function isWelcomePageEnabled(stateful: Function | Object) {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return getFeatureFlag(stateful, WELCOME_PAGE_ENABLED, false);
|
||||
}
|
||||
|
||||
return toState(stateful)['features/base/config'].enableWelcomePage;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export * from './components';
|
||||
export * from './functions';
|
||||
|
|
Loading…
Reference in New Issue