This commit is contained in:
298
react/features/welcome/components/AbstractWelcomePage.ts
Normal file
298
react/features/welcome/components/AbstractWelcomePage.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
// @ts-expect-error
|
||||
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
|
||||
import { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { createWelcomePageEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { appNavigate } from '../../app/actions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { IDeeplinkingConfig } from '../../base/config/configType';
|
||||
import isInsecureRoomName from '../../base/util/isInsecureRoomName';
|
||||
import { isCalendarEnabled } from '../../calendar-sync/functions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../prejoin/functions';
|
||||
import { isRecentListEnabled } from '../../recent-list/functions';
|
||||
|
||||
/**
|
||||
* {@code AbstractWelcomePage}'s React {@code Component} prop types.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether the calendar functionality is enabled or not.
|
||||
*/
|
||||
_calendarEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The deeplinking config.
|
||||
*/
|
||||
_deeplinkingCfg: IDeeplinkingConfig;
|
||||
|
||||
/**
|
||||
* Whether the insecure room name functionality is enabled or not.
|
||||
*/
|
||||
_enableInsecureRoomNameWarning: boolean;
|
||||
|
||||
/**
|
||||
* URL for the moderated rooms microservice, if available.
|
||||
*/
|
||||
_moderatedRoomServiceUrl?: string;
|
||||
|
||||
/**
|
||||
* Whether the recent list is enabled.
|
||||
*/
|
||||
_recentListEnabled: Boolean;
|
||||
|
||||
/**
|
||||
* Room name to join to.
|
||||
*/
|
||||
_room: string;
|
||||
|
||||
/**
|
||||
* The current settings.
|
||||
*/
|
||||
_settings: Object;
|
||||
|
||||
/**
|
||||
* The Redux dispatch Function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
_fieldFocused?: boolean;
|
||||
animateTimeoutId?: number;
|
||||
generateRoomNames?: string;
|
||||
generatedRoomName: string;
|
||||
hintBoxAnimation?: any;
|
||||
insecureRoomName: boolean;
|
||||
isSettingsScreenFocused?: boolean;
|
||||
joining: boolean;
|
||||
room: string;
|
||||
roomNameInputAnimation?: any;
|
||||
roomPlaceholder: string;
|
||||
updateTimeoutId?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base (abstract) class for container component rendering the welcome page.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class AbstractWelcomePage<P extends IProps> extends Component<P, IState> {
|
||||
_mounted: boolean | undefined;
|
||||
|
||||
/**
|
||||
* Save room name into component's local state.
|
||||
*
|
||||
* @type {Object}
|
||||
* @property {number|null} animateTimeoutId - Identifier of the letter
|
||||
* animation timeout.
|
||||
* @property {string} generatedRoomName - Automatically generated room name.
|
||||
* @property {string} room - Room name.
|
||||
* @property {string} roomPlaceholder - Room placeholder that's used as a
|
||||
* placeholder for input.
|
||||
* @property {number|null} updateTimeoutId - Identifier of the timeout
|
||||
* updating the generated room name.
|
||||
*/
|
||||
override state: IState = {
|
||||
animateTimeoutId: undefined,
|
||||
generatedRoomName: '',
|
||||
generateRoomNames: undefined,
|
||||
insecureRoomName: false,
|
||||
joining: false,
|
||||
room: '',
|
||||
roomPlaceholder: '',
|
||||
updateTimeoutId: undefined,
|
||||
_fieldFocused: false,
|
||||
isSettingsScreenFocused: false,
|
||||
roomNameInputAnimation: 0,
|
||||
hintBoxAnimation: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractWelcomePage} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code AbstractWelcomePage} instance with.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._animateRoomNameChanging
|
||||
= this._animateRoomNameChanging.bind(this);
|
||||
this._onJoin = this._onJoin.bind(this);
|
||||
this._onRoomChange = this._onRoomChange.bind(this);
|
||||
this._renderInsecureRoomNameWarning = this._renderInsecureRoomNameWarning.bind(this);
|
||||
this._updateRoomName = this._updateRoomName.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after mounting occurs.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this._mounted = true;
|
||||
sendAnalytics(createWelcomePageEvent('viewed', undefined, { value: 1 }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount()}. Invoked
|
||||
* immediately before this component is unmounted and destroyed.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
this._clearTimeouts();
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the changing of the room name.
|
||||
*
|
||||
* @param {string} word - The part of room name that should be added to
|
||||
* placeholder.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_animateRoomNameChanging(word: string) {
|
||||
let animateTimeoutId;
|
||||
const roomPlaceholder = this.state.roomPlaceholder + word.substr(0, 1);
|
||||
|
||||
if (word.length > 1) {
|
||||
animateTimeoutId
|
||||
= window.setTimeout(
|
||||
() => {
|
||||
this._animateRoomNameChanging(
|
||||
word.substring(1, word.length));
|
||||
},
|
||||
70);
|
||||
}
|
||||
this.setState({
|
||||
animateTimeoutId,
|
||||
roomPlaceholder
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that clears timeouts for animations and updates of room name.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_clearTimeouts() {
|
||||
this.state.animateTimeoutId && clearTimeout(this.state.animateTimeoutId);
|
||||
this.state.updateTimeoutId && clearTimeout(this.state.updateTimeoutId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the insecure room name warning.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_doRenderInsecureRoomNameWarning(): JSX.Element | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles joining. Either by clicking on 'Join' button
|
||||
* or by pressing 'Enter' in room name input field.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onJoin() {
|
||||
const room = this.state.room || this.state.generatedRoomName;
|
||||
|
||||
sendAnalytics(
|
||||
createWelcomePageEvent('clicked', 'joinButton', {
|
||||
isGenerated: !this.state.room,
|
||||
room
|
||||
}));
|
||||
|
||||
if (room) {
|
||||
this.setState({ joining: true });
|
||||
|
||||
// By the time the Promise of appNavigate settles, this component
|
||||
// may have already been unmounted.
|
||||
const onAppNavigateSettled
|
||||
= () => this._mounted && this.setState({ joining: false });
|
||||
|
||||
this.props.dispatch(appNavigate(room))
|
||||
.then(onAppNavigateSettled, onAppNavigateSettled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles 'change' event for the room name text input field.
|
||||
*
|
||||
* @param {string} value - The text typed into the respective text input
|
||||
* field.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRoomChange(value: string) {
|
||||
this.setState({
|
||||
room: value,
|
||||
insecureRoomName: Boolean(this.props._enableInsecureRoomNameWarning && value && isInsecureRoomName(value))
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the insecure room name warning if needed.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderInsecureRoomNameWarning() {
|
||||
if (this.props._enableInsecureRoomNameWarning && this.state.insecureRoomName) {
|
||||
return this._doRenderInsecureRoomNameWarning();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the generation of a new room name and initiates an animation of
|
||||
* its changing.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateRoomName() {
|
||||
const generatedRoomName = generateRoomWithoutSeparator();
|
||||
const roomPlaceholder = '';
|
||||
const updateTimeoutId = window.setTimeout(this._updateRoomName, 10000);
|
||||
|
||||
this._clearTimeouts();
|
||||
this.setState(
|
||||
{
|
||||
generatedRoomName,
|
||||
roomPlaceholder,
|
||||
updateTimeoutId
|
||||
},
|
||||
() => this._animateRoomNameChanging(generatedRoomName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props of
|
||||
* {@code AbstractWelcomePage}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_calendarEnabled: isCalendarEnabled(state),
|
||||
_deeplinkingCfg: state['features/base/config'].deeplinking || {},
|
||||
_enableInsecureRoomNameWarning: isUnsafeRoomWarningEnabled(state),
|
||||
_moderatedRoomServiceUrl: state['features/base/config'].moderatedRoomServiceUrl,
|
||||
_recentListEnabled: isRecentListEnabled(),
|
||||
_room: state['features/base/conference'].room ?? '',
|
||||
_settings: state['features/base/settings']
|
||||
};
|
||||
}
|
||||
1
react/features/welcome/components/BlankPage.web.ts
Normal file
1
react/features/welcome/components/BlankPage.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default undefined;
|
||||
35
react/features/welcome/components/TabIcon.tsx
Normal file
35
react/features/welcome/components/TabIcon.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { StyleType } from '../../base/styles/functions.any';
|
||||
import BaseTheme from '../../base/ui/components/BaseTheme';
|
||||
import { INACTIVE_TAB_COLOR } from '../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Is the tab focused?
|
||||
*/
|
||||
focused?: boolean;
|
||||
|
||||
/**
|
||||
* The ImageSource to be rendered as image.
|
||||
*/
|
||||
src: Function;
|
||||
|
||||
/**
|
||||
* The component's external style.
|
||||
*/
|
||||
style?: StyleType;
|
||||
}
|
||||
|
||||
const TabIcon = ({ focused, src, style }: IProps) => (
|
||||
<Icon
|
||||
color = { focused ? BaseTheme.palette.icon01 : INACTIVE_TAB_COLOR }
|
||||
size = { 24 }
|
||||
src = { src }
|
||||
style = { style } />
|
||||
);
|
||||
|
||||
|
||||
export default TabIcon;
|
||||
111
react/features/welcome/components/Tabs.tsx
Normal file
111
react/features/welcome/components/Tabs.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Tabs}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Accessibility label for the tabs container.
|
||||
*
|
||||
*/
|
||||
accessibilityLabel: string;
|
||||
|
||||
/**
|
||||
* Tabs information.
|
||||
*/
|
||||
tabs: {
|
||||
content: any;
|
||||
id: string;
|
||||
label: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A React component that implements tabs.
|
||||
*
|
||||
* @returns {ReactElement} The component.
|
||||
*/
|
||||
const Tabs = ({ accessibilityLabel, tabs }: IProps) => {
|
||||
const [ current, setCurrent ] = useState(0);
|
||||
|
||||
const onClick = useCallback(index => (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setCurrent(index);
|
||||
}, []);
|
||||
|
||||
const onKeyDown = useCallback(index => (event: React.KeyboardEvent) => {
|
||||
let newIndex = null;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
newIndex = index === 0 ? tabs.length - 1 : index - 1;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
newIndex = index === tabs.length - 1 ? 0 : index + 1;
|
||||
}
|
||||
|
||||
if (newIndex !== null) {
|
||||
setCurrent(newIndex);
|
||||
}
|
||||
}, [ tabs ]);
|
||||
|
||||
useEffect(() => {
|
||||
// this test is needed to make sure the effect is triggered because of user actually changing tab
|
||||
if (document.activeElement?.getAttribute('role') === 'tab') {
|
||||
// @ts-ignore
|
||||
document.querySelector(`#${`${tabs[current].id}-tab`}`)?.focus();
|
||||
}
|
||||
|
||||
}, [ current, tabs ]);
|
||||
|
||||
return (
|
||||
<div className = 'tab-container'>
|
||||
{ tabs.length > 1
|
||||
? (
|
||||
<>
|
||||
<div
|
||||
aria-label = { accessibilityLabel }
|
||||
className = 'tab-buttons'
|
||||
role = 'tablist'>
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
aria-controls = { `${tab.id}-panel` }
|
||||
aria-selected = { current === index ? 'true' : 'false' }
|
||||
id = { `${tab.id}-tab` }
|
||||
key = { tab.id }
|
||||
onClick = { onClick(index) }
|
||||
onKeyDown = { onKeyDown(index) }
|
||||
role = 'tab'
|
||||
tabIndex = { current === index ? undefined : -1 }>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{tabs.map((tab, index) => (
|
||||
<div
|
||||
aria-labelledby = { `${tab.id}-tab` }
|
||||
className = { current === index ? 'tab-content' : 'hide' }
|
||||
id = { `${tab.id}-panel` }
|
||||
key = { tab.id }
|
||||
role = 'tabpanel'
|
||||
tabIndex = { 0 }>
|
||||
{tab.content}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<h2 className = 'sr-only'>{accessibilityLabel}</h2>
|
||||
<div className = 'tab-content'>{tabs[0].content}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
417
react/features/welcome/components/WelcomePage.native.tsx
Normal file
417
react/features/welcome/components/WelcomePage.native.tsx
Normal file
@@ -0,0 +1,417 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Animated,
|
||||
NativeSyntheticEvent,
|
||||
SafeAreaView,
|
||||
StyleProp,
|
||||
TextInputFocusEventData,
|
||||
TextStyle,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getName } from '../../app/functions.native';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconWarning } from '../../base/icons/svg';
|
||||
import LoadingIndicator from '../../base/react/components/native/LoadingIndicator';
|
||||
import Text from '../../base/react/components/native/Text';
|
||||
import BaseTheme from '../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../base/ui/components/native/Button';
|
||||
import Input from '../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../base/ui/constants.native';
|
||||
import getUnsafeRoomText from '../../base/util/getUnsafeRoomText.native';
|
||||
import WelcomePageTabs
|
||||
from '../../mobile/navigation/components/welcome/components/WelcomePageTabs';
|
||||
|
||||
import {
|
||||
IProps as AbstractProps,
|
||||
AbstractWelcomePage,
|
||||
_mapStateToProps as _abstractMapStateToProps
|
||||
} from './AbstractWelcomePage';
|
||||
import styles from './styles.native';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* Function for getting the unsafe room text.
|
||||
*/
|
||||
getUnsafeRoomTextFn: Function;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The native container rendering the welcome page.
|
||||
*
|
||||
* @augments AbstractWelcomePage
|
||||
*/
|
||||
class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
_onFieldBlur: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
|
||||
_onFieldFocus: (e: NativeSyntheticEvent<TextInputFocusEventData>) => void;
|
||||
|
||||
/**
|
||||
* Constructor of the Component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state._fieldFocused = false;
|
||||
|
||||
this.state.isSettingsScreenFocused = false;
|
||||
|
||||
this.state.roomNameInputAnimation = new Animated.Value(1);
|
||||
|
||||
this.state.hintBoxAnimation = new Animated.Value(0);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onFieldFocusChange = this._onFieldFocusChange.bind(this);
|
||||
this._renderHintBox = this._renderHintBox.bind(this);
|
||||
|
||||
// Specially bind functions to avoid function definition on render.
|
||||
this._onFieldBlur = this._onFieldFocusChange.bind(this, false);
|
||||
this._onFieldFocus = this._onFieldFocusChange.bind(this, true);
|
||||
this._onSettingsScreenFocused = this._onSettingsScreenFocused.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after mounting occurs. Creates a local video track if none
|
||||
* is available and the camera permission was already granted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
const {
|
||||
navigation,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
navigation.setOptions({
|
||||
headerTitle: t('welcomepage.headerTitle')
|
||||
});
|
||||
|
||||
navigation.addListener('focus', () => {
|
||||
this._updateRoomName();
|
||||
});
|
||||
|
||||
navigation.addListener('blur', () => {
|
||||
this._clearTimeouts();
|
||||
|
||||
this.setState({
|
||||
generatedRoomName: '',
|
||||
insecureRoomName: false,
|
||||
room: ''
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}. Renders a prompt for
|
||||
* entering a room name.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
// We want to have the welcome page support the reduced UI layout,
|
||||
// but we ran into serious issues enabling it so we disable it
|
||||
// until we have a proper fix in place. We leave the code here though, because
|
||||
// this part should be fine when the bug is fixed.
|
||||
//
|
||||
// NOTE: when re-enabling, don't forget to uncomment the respective _mapStateToProps line too
|
||||
|
||||
/*
|
||||
const { _reducedUI } = this.props;
|
||||
|
||||
if (_reducedUI) {
|
||||
return this._renderReducedUI();
|
||||
}
|
||||
*/
|
||||
|
||||
return this._renderFullUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the insecure room name warning.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_doRenderInsecureRoomNameWarning() {
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.messageContainer,
|
||||
styles.insecureRoomNameWarningContainer as ViewStyle
|
||||
] }>
|
||||
<Icon
|
||||
src = { IconWarning }
|
||||
style = { styles.insecureRoomNameWarningIcon } />
|
||||
<Text style = { styles.insecureRoomNameWarningText }>
|
||||
{ this.props.getUnsafeRoomTextFn(this.props.t) }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a style array to handle the hint box animation.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_getHintBoxStyle() {
|
||||
return [
|
||||
styles.messageContainer,
|
||||
styles.hintContainer,
|
||||
{
|
||||
opacity: this.state.hintBoxAnimation
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when the room field's focus changes so the hint box
|
||||
* must be rendered or removed.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} focused - The focused state of the field.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFieldFocusChange(focused: boolean) {
|
||||
if (focused) {
|
||||
// Stop placeholder animation.
|
||||
this._clearTimeouts();
|
||||
this.setState({
|
||||
_fieldFocused: true,
|
||||
roomPlaceholder: ''
|
||||
});
|
||||
} else {
|
||||
// Restart room placeholder animation.
|
||||
this._updateRoomName();
|
||||
}
|
||||
|
||||
Animated.timing(
|
||||
|
||||
this.state.hintBoxAnimation,
|
||||
|
||||
{
|
||||
duration: 300,
|
||||
toValue: focused ? 1 : 0,
|
||||
useNativeDriver: true
|
||||
})
|
||||
.start(animationState =>
|
||||
|
||||
animationState.finished
|
||||
|
||||
&& !focused
|
||||
&& this.setState({
|
||||
_fieldFocused: false
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when the settings screen is focused.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} focused - The focused state of the screen.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSettingsScreenFocused(focused: boolean) {
|
||||
this.setState({
|
||||
isSettingsScreenFocused: focused
|
||||
});
|
||||
|
||||
this.props.navigation.setOptions({
|
||||
headerShown: !focused
|
||||
});
|
||||
|
||||
Animated.timing(
|
||||
this.state.roomNameInputAnimation,
|
||||
{
|
||||
toValue: focused ? 0 : 1,
|
||||
duration: 500,
|
||||
useNativeDriver: true
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the hint box if necessary.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderHintBox() {
|
||||
const { t } = this.props;
|
||||
|
||||
if (this.state._fieldFocused) {
|
||||
return (
|
||||
<Animated.View style = { this._getHintBoxStyle() as ViewStyle[] }>
|
||||
<View style = { styles.hintTextContainer as ViewStyle } >
|
||||
<Text style = { styles.hintText as TextStyle }>
|
||||
{ t('welcomepage.roomnameHint') }
|
||||
</Text>
|
||||
</View>
|
||||
<View style = { styles.hintButtonContainer as ViewStyle } >
|
||||
{ this._renderJoinButton() }
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the join button.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderJoinButton() {
|
||||
const { t } = this.props;
|
||||
let joinButton;
|
||||
|
||||
|
||||
if (this.state.joining) {
|
||||
// TouchableHighlight is picky about what its children can be, so
|
||||
// wrap it in a native component, i.e. View to avoid having to
|
||||
// modify non-native children.
|
||||
joinButton = (
|
||||
<TouchableHighlight
|
||||
accessibilityLabel =
|
||||
{ t('welcomepage.accessibilityLabel.join') }
|
||||
onPress = { this._onJoin }
|
||||
style = { styles.button as ViewStyle }>
|
||||
<View>
|
||||
<LoadingIndicator
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = 'small' />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
} else {
|
||||
joinButton = (
|
||||
<Button
|
||||
accessibilityLabel = { 'welcomepage.accessibilityLabel.join' }
|
||||
labelKey = { 'welcomepage.join' }
|
||||
labelStyle = { styles.joinButtonLabel }
|
||||
onClick = { this._onJoin }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
);
|
||||
}
|
||||
|
||||
return joinButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the room name input.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderRoomNameInput() {
|
||||
const roomnameAccLabel = 'welcomepage.accessibilityLabel.roomname';
|
||||
const { t } = this.props;
|
||||
const { isSettingsScreenFocused } = this.state;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style = { [
|
||||
isSettingsScreenFocused && styles.roomNameInputContainer,
|
||||
{ opacity: this.state.roomNameInputAnimation }
|
||||
] as StyleProp<ViewStyle> }>
|
||||
<SafeAreaView style = { styles.roomContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.joinControls } >
|
||||
<Text style = { styles.enterRoomText as StyleProp<TextStyle> }>
|
||||
{ t('welcomepage.roomname') }
|
||||
</Text>
|
||||
<Input
|
||||
accessibilityLabel = { t(roomnameAccLabel) }
|
||||
autoCapitalize = { 'none' }
|
||||
autoFocus = { false }
|
||||
customStyles = {{ input: styles.customInput }}
|
||||
onBlur = { this._onFieldBlur }
|
||||
onChange = { this._onRoomChange }
|
||||
onFocus = { this._onFieldFocus }
|
||||
onSubmitEditing = { this._onJoin }
|
||||
placeholder = { this.state.roomPlaceholder }
|
||||
returnKeyType = { 'go' }
|
||||
value = { this.state.room } />
|
||||
{
|
||||
this._renderInsecureRoomNameWarning()
|
||||
}
|
||||
{
|
||||
this._renderHintBox()
|
||||
}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the full welcome page.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFullUI() {
|
||||
return (
|
||||
<>
|
||||
{ this._renderRoomNameInput() }
|
||||
<View style = { styles.welcomePage as ViewStyle }>
|
||||
<WelcomePageTabs
|
||||
disabled = { Boolean(this.state._fieldFocused) } // @ts-ignore
|
||||
onListContainerPress = { this._onFieldBlur }
|
||||
onSettingsScreenFocused = { this._onSettingsScreenFocused } />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a "reduced" version of the welcome page.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderReducedUI() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.reducedUIContainer as ViewStyle }>
|
||||
<Text style = { styles.reducedUIText }>
|
||||
{ t('welcomepage.reducedUIText', { app: getName() }) }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
|
||||
// _reducedUI: state['features/base/responsive-ui'].reducedUI
|
||||
getUnsafeRoomTextFn: (t: Function) => getUnsafeRoomText(state, t, 'welcome')
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(WelcomePage));
|
||||
548
react/features/welcome/components/WelcomePage.web.tsx
Normal file
548
react/features/welcome/components/WelcomePage.web.tsx
Normal file
@@ -0,0 +1,548 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { isMobileBrowser } from '../../base/environment/utils';
|
||||
import { translate, translateToHTML } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconWarning } from '../../base/icons/svg';
|
||||
import Watermarks from '../../base/react/components/web/Watermarks';
|
||||
import getUnsafeRoomText from '../../base/util/getUnsafeRoomText.web';
|
||||
import CalendarList from '../../calendar-sync/components/CalendarList.web';
|
||||
import RecentList from '../../recent-list/components/RecentList.web';
|
||||
import SettingsButton from '../../settings/components/web/SettingsButton';
|
||||
import { SETTINGS_TABS } from '../../settings/constants';
|
||||
|
||||
import { AbstractWelcomePage, IProps, _mapStateToProps } from './AbstractWelcomePage';
|
||||
import Tabs from './Tabs';
|
||||
|
||||
/**
|
||||
* The pattern used to validate room name.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const ROOM_NAME_VALIDATE_PATTERN_STR = '^[^?&:\u0022\u0027%#]+$';
|
||||
|
||||
/**
|
||||
* The Web container rendering the welcome page.
|
||||
*
|
||||
* @augments AbstractWelcomePage
|
||||
*/
|
||||
class WelcomePage extends AbstractWelcomePage<IProps> {
|
||||
_additionalContentRef: HTMLDivElement | null;
|
||||
_additionalToolbarContentRef: HTMLDivElement | null;
|
||||
_additionalCardRef: HTMLDivElement | null;
|
||||
_roomInputRef: HTMLInputElement | null;
|
||||
_additionalCardTemplate: HTMLTemplateElement | null;
|
||||
_additionalContentTemplate: HTMLTemplateElement | null;
|
||||
_additionalToolbarContentTemplate: HTMLTemplateElement | null;
|
||||
_titleHasNotAllowCharacter: boolean;
|
||||
|
||||
/**
|
||||
* Default values for {@code WelcomePage} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
_room: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new WelcomePage instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
|
||||
generateRoomNames:
|
||||
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
|
||||
};
|
||||
|
||||
/**
|
||||
* Used To display a warning massage if the title input has no allow character.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._titleHasNotAllowCharacter = false;
|
||||
|
||||
/**
|
||||
* The HTML Element used as the container for additional content. Used
|
||||
* for directly appending the additional content template to the dom.
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLTemplateElement|null}
|
||||
*/
|
||||
this._additionalContentRef = null;
|
||||
|
||||
this._roomInputRef = null;
|
||||
|
||||
/**
|
||||
* The HTML Element used as the container for additional toolbar content. Used
|
||||
* for directly appending the additional content template to the dom.
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLTemplateElement|null}
|
||||
*/
|
||||
this._additionalToolbarContentRef = null;
|
||||
|
||||
this._additionalCardRef = null;
|
||||
|
||||
/**
|
||||
* The template to use as the additional card displayed near the main one.
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLTemplateElement|null}
|
||||
*/
|
||||
this._additionalCardTemplate = document.getElementById(
|
||||
'welcome-page-additional-card-template') as HTMLTemplateElement;
|
||||
|
||||
/**
|
||||
* The template to use as the main content for the welcome page. If
|
||||
* not found then only the welcome page head will display.
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLTemplateElement|null}
|
||||
*/
|
||||
this._additionalContentTemplate = document.getElementById(
|
||||
'welcome-page-additional-content-template') as HTMLTemplateElement;
|
||||
|
||||
/**
|
||||
* The template to use as the additional content for the welcome page header toolbar.
|
||||
* If not found then only the settings icon will be displayed.
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLTemplateElement|null}
|
||||
*/
|
||||
this._additionalToolbarContentTemplate = document.getElementById(
|
||||
'settings-toolbar-additional-content-template'
|
||||
) as HTMLTemplateElement;
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onFormSubmit = this._onFormSubmit.bind(this);
|
||||
this._onRoomChange = this._onRoomChange.bind(this);
|
||||
this._setAdditionalCardRef = this._setAdditionalCardRef.bind(this);
|
||||
this._setAdditionalContentRef
|
||||
= this._setAdditionalContentRef.bind(this);
|
||||
this._setRoomInputRef = this._setRoomInputRef.bind(this);
|
||||
this._setAdditionalToolbarContentRef
|
||||
= this._setAdditionalToolbarContentRef.bind(this);
|
||||
this._renderFooter = this._renderFooter.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount()}. Invoked
|
||||
* immediately after this component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
document.body.classList.add('welcome-page');
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
|
||||
if (this.state.generateRoomNames) {
|
||||
this._updateRoomName();
|
||||
}
|
||||
|
||||
if (this._shouldShowAdditionalContent()) {
|
||||
this._additionalContentRef?.appendChild(
|
||||
this._additionalContentTemplate?.content.cloneNode(true) as Node);
|
||||
}
|
||||
|
||||
if (this._shouldShowAdditionalToolbarContent()) {
|
||||
this._additionalToolbarContentRef?.appendChild(
|
||||
this._additionalToolbarContentTemplate?.content.cloneNode(true) as Node
|
||||
);
|
||||
}
|
||||
|
||||
if (this._shouldShowAdditionalCard()) {
|
||||
this._additionalCardRef?.appendChild(
|
||||
this._additionalCardTemplate?.content.cloneNode(true) as Node
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the classname used for custom styling of the welcome page.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
|
||||
document.body.classList.remove('welcome-page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
override render() {
|
||||
const { _moderatedRoomServiceUrl, t } = this.props;
|
||||
const { DEFAULT_WELCOME_PAGE_LOGO_URL, DISPLAY_WELCOME_FOOTER } = interfaceConfig;
|
||||
const showAdditionalCard = this._shouldShowAdditionalCard();
|
||||
const showAdditionalContent = this._shouldShowAdditionalContent();
|
||||
const showAdditionalToolbarContent = this._shouldShowAdditionalToolbarContent();
|
||||
const contentClassName = showAdditionalContent ? 'with-content' : 'without-content';
|
||||
const footerClassName = DISPLAY_WELCOME_FOOTER ? 'with-footer' : 'without-footer';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `welcome ${contentClassName} ${footerClassName}` }
|
||||
id = 'welcome_page'>
|
||||
<div className = 'header'>
|
||||
<div className = 'header-image' />
|
||||
<div className = 'header-container'>
|
||||
<div className = 'header-watermark-container'>
|
||||
<div className = 'welcome-watermark'>
|
||||
<Watermarks
|
||||
defaultJitsiLogoURL = { DEFAULT_WELCOME_PAGE_LOGO_URL }
|
||||
noMargins = { true } />
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'welcome-page-settings'>
|
||||
<SettingsButton
|
||||
defaultTab = { SETTINGS_TABS.CALENDAR }
|
||||
isDisplayedOnWelcomePage = { true } />
|
||||
{showAdditionalToolbarContent
|
||||
? <div
|
||||
className = 'settings-toolbar-content'
|
||||
ref = { this._setAdditionalToolbarContentRef } />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
<h1 className = 'header-text-title'>
|
||||
{interfaceConfig.APP_NAME}
|
||||
</h1>
|
||||
<span className = 'header-text-subtitle'>
|
||||
{t('welcomepage.headerSubtitle')}
|
||||
</span>
|
||||
<div id = 'enter_room'>
|
||||
<div className = 'join-meeting-container'>
|
||||
<div className = 'enter-room-input-container'>
|
||||
<form onSubmit = { this._onFormSubmit }>
|
||||
<input
|
||||
aria-disabled = 'false'
|
||||
aria-label = 'Meeting name input'
|
||||
autoFocus = { true }
|
||||
className = 'enter-room-input'
|
||||
id = 'enter_room_field'
|
||||
onChange = { this._onRoomChange }
|
||||
pattern = { ROOM_NAME_VALIDATE_PATTERN_STR }
|
||||
placeholder = { this.state.roomPlaceholder }
|
||||
ref = { this._setRoomInputRef }
|
||||
type = 'text'
|
||||
value = { this.state.room } />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-disabled = 'false'
|
||||
aria-label = 'Start meeting'
|
||||
className = 'welcome-page-button'
|
||||
id = 'enter_room_button'
|
||||
onClick = { this._onFormSubmit }
|
||||
tabIndex = { 0 }
|
||||
type = 'button'>
|
||||
{t('welcomepage.startMeeting')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this._titleHasNotAllowCharacter && (
|
||||
<div
|
||||
className = 'not-allow-title-character-div'
|
||||
role = 'alert'>
|
||||
<Icon src = { IconWarning } />
|
||||
<span className = 'not-allow-title-character-text'>
|
||||
{t('welcomepage.roomNameAllowedChars')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{this._renderInsecureRoomNameWarning()}
|
||||
|
||||
{_moderatedRoomServiceUrl && (
|
||||
<div id = 'moderated-meetings'>
|
||||
{
|
||||
translateToHTML(
|
||||
t, 'welcomepage.moderatedMessage', { url: _moderatedRoomServiceUrl })
|
||||
}
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className = 'welcome-cards-container'>
|
||||
<div className = 'welcome-card-column'>
|
||||
<div className = 'welcome-tabs welcome-card welcome-card--blue'>
|
||||
{this._renderTabs()}
|
||||
</div>
|
||||
{showAdditionalCard
|
||||
? <div
|
||||
className = 'welcome-card welcome-card--dark'
|
||||
ref = { this._setAdditionalCardRef } />
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{showAdditionalContent
|
||||
? <div
|
||||
className = 'welcome-page-content'
|
||||
ref = { this._setAdditionalContentRef } />
|
||||
: null}
|
||||
</div>
|
||||
{DISPLAY_WELCOME_FOOTER && this._renderFooter()}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the insecure room name warning.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _doRenderInsecureRoomNameWarning() {
|
||||
return (
|
||||
<div className = 'insecure-room-name-warning'>
|
||||
<Icon src = { IconWarning } />
|
||||
<span>
|
||||
{getUnsafeRoomText(this.props.t, 'welcome')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents submission of the form and delegates join logic.
|
||||
*
|
||||
* @param {Event} event - The HTML Event which details the form submission.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFormSubmit(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!this._roomInputRef || this._roomInputRef.reportValidity()) {
|
||||
this._onJoin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the super to account for the differences in the argument types
|
||||
* provided by HTML and React Native text inputs.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @override
|
||||
* @param {Event} event - The (HTML) Event which details the change such as
|
||||
* the EventTarget.
|
||||
* @protected
|
||||
*/
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
_onRoomChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const specialCharacters = [ '?', '&', ':', '\'', '"', '%', '#', '.' ];
|
||||
|
||||
this._titleHasNotAllowCharacter = specialCharacters.some(char => event.target.value.includes(char));
|
||||
super._onRoomChange(event.target.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the footer.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderFooter() {
|
||||
const {
|
||||
t,
|
||||
_deeplinkingCfg: {
|
||||
ios = { downloadLink: undefined },
|
||||
android = {
|
||||
fDroidUrl: undefined,
|
||||
downloadLink: undefined
|
||||
}
|
||||
}
|
||||
} = this.props;
|
||||
|
||||
const { downloadLink: iosDownloadLink } = ios;
|
||||
|
||||
const { fDroidUrl, downloadLink: androidDownloadLink } = android;
|
||||
|
||||
return (<footer className = 'welcome-footer'>
|
||||
<div className = 'welcome-footer-centered'>
|
||||
<div className = 'welcome-footer-padded'>
|
||||
<div className = 'welcome-footer-row-block welcome-footer--row-1'>
|
||||
<div className = 'welcome-footer-row-1-text'>{t('welcomepage.jitsiOnMobile')}</div>
|
||||
<a
|
||||
className = 'welcome-badge'
|
||||
href = { iosDownloadLink }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
<img
|
||||
alt = { t('welcomepage.mobileDownLoadLinkIos') }
|
||||
src = './images/app-store-badge.png' />
|
||||
</a>
|
||||
<a
|
||||
className = 'welcome-badge'
|
||||
href = { androidDownloadLink }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
<img
|
||||
alt = { t('welcomepage.mobileDownLoadLinkAndroid') }
|
||||
src = './images/google-play-badge.png' />
|
||||
</a>
|
||||
<a
|
||||
className = 'welcome-badge'
|
||||
href = { fDroidUrl }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
<img
|
||||
alt = { t('welcomepage.mobileDownLoadLinkFDroid') }
|
||||
src = './images/f-droid-badge.png' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders tabs to show previous meetings and upcoming calendar events. The
|
||||
* tabs are purposefully hidden on mobile browsers.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
_renderTabs() {
|
||||
if (isMobileBrowser()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { _calendarEnabled, _recentListEnabled, t } = this.props;
|
||||
|
||||
const tabs = [];
|
||||
|
||||
if (_calendarEnabled) {
|
||||
tabs.push({
|
||||
id: 'calendar',
|
||||
label: t('welcomepage.upcomingMeetings'),
|
||||
content: <CalendarList />
|
||||
});
|
||||
}
|
||||
|
||||
if (_recentListEnabled) {
|
||||
tabs.push({
|
||||
id: 'recent',
|
||||
label: t('welcomepage.recentMeetings'),
|
||||
content: <RecentList />
|
||||
});
|
||||
}
|
||||
|
||||
if (tabs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
accessibilityLabel = { t('welcomepage.meetingsAccessibilityLabel') }
|
||||
tabs = { tabs } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal reference to the HTMLDivElement used to hold the
|
||||
* additional card shown near the tabs card.
|
||||
*
|
||||
* @param {HTMLDivElement} el - The HTMLElement for the div that is the root
|
||||
* of the welcome page content.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setAdditionalCardRef(el: HTMLDivElement) {
|
||||
this._additionalCardRef = el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal reference to the HTMLDivElement used to hold the
|
||||
* welcome page content.
|
||||
*
|
||||
* @param {HTMLDivElement} el - The HTMLElement for the div that is the root
|
||||
* of the welcome page content.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setAdditionalContentRef(el: HTMLDivElement) {
|
||||
this._additionalContentRef = el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal reference to the HTMLDivElement used to hold the
|
||||
* toolbar additional content.
|
||||
*
|
||||
* @param {HTMLDivElement} el - The HTMLElement for the div that is the root
|
||||
* of the additional toolbar content.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setAdditionalToolbarContentRef(el: HTMLDivElement) {
|
||||
this._additionalToolbarContentRef = el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal reference to the HTMLInputElement used to hold the
|
||||
* welcome page input room element.
|
||||
*
|
||||
* @param {HTMLInputElement} el - The HTMLElement for the input of the room name on the welcome page.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setRoomInputRef(el: HTMLInputElement) {
|
||||
this._roomInputRef = el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not an additional card should be displayed near the tabs.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldShowAdditionalCard() {
|
||||
return interfaceConfig.DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD
|
||||
&& this._additionalCardTemplate?.content
|
||||
&& this._additionalCardTemplate?.innerHTML?.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not additional content should be displayed below
|
||||
* the welcome page's header for entering a room name.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldShowAdditionalContent() {
|
||||
return interfaceConfig.DISPLAY_WELCOME_PAGE_CONTENT
|
||||
&& this._additionalContentTemplate?.content
|
||||
&& this._additionalContentTemplate?.innerHTML?.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not additional content should be displayed inside
|
||||
* the header toolbar.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldShowAdditionalToolbarContent() {
|
||||
return interfaceConfig.DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT
|
||||
&& this._additionalToolbarContentTemplate?.content
|
||||
&& this._additionalToolbarContentTemplate?.innerHTML.trim();
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(WelcomePage));
|
||||
231
react/features/welcome/components/styles.native.ts
Normal file
231
react/features/welcome/components/styles.native.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { BoxModel } from '../../base/styles/components/styles/BoxModel';
|
||||
import BaseTheme from '../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const AVATAR_SIZE = 104;
|
||||
|
||||
/**
|
||||
* The default color of text on the WelcomePage.
|
||||
*/
|
||||
const TEXT_COLOR = BaseTheme.palette.text01;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of the feature welcome including
|
||||
* {@code WelcomePage} and {@code BlankPage}.
|
||||
*/
|
||||
export default {
|
||||
|
||||
blankPageText: {
|
||||
color: TEXT_COLOR,
|
||||
fontSize: 18
|
||||
},
|
||||
|
||||
/**
|
||||
* View that is rendered when there is no welcome page.
|
||||
*/
|
||||
blankPageWrapper: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Join button style.
|
||||
*/
|
||||
button: {
|
||||
backgroundColor: BaseTheme.palette.action01,
|
||||
borderColor: BaseTheme.palette.action01,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
borderWidth: 1,
|
||||
height: BaseTheme.spacing[7],
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
joinButtonLabel: {
|
||||
textTransform: 'uppercase'
|
||||
},
|
||||
|
||||
joinButtonText: {
|
||||
alignSelf: 'center',
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 14
|
||||
},
|
||||
|
||||
enterRoomText: {
|
||||
color: TEXT_COLOR,
|
||||
fontSize: 18,
|
||||
marginBottom: BoxModel.margin
|
||||
},
|
||||
|
||||
/**
|
||||
* Container for the button on the hint box.
|
||||
*/
|
||||
hintButtonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Container for the hint box.
|
||||
*/
|
||||
hintContainer: {
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
/**
|
||||
* The text of the hint box.
|
||||
*/
|
||||
hintText: {
|
||||
color: BaseTheme.palette.text01,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Container for the text on the hint box.
|
||||
*/
|
||||
hintTextContainer: {
|
||||
marginBottom: 2 * BoxModel.margin
|
||||
},
|
||||
|
||||
/**
|
||||
* Container for the items in the side bar.
|
||||
*/
|
||||
itemContainer: {
|
||||
flexDirection: 'column',
|
||||
paddingTop: 10
|
||||
},
|
||||
|
||||
/**
|
||||
* A view that contains the field and hint box.
|
||||
*/
|
||||
joinControls: {
|
||||
padding: BoxModel.padding
|
||||
},
|
||||
|
||||
messageContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui03,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
marginVertical: BaseTheme.spacing[1],
|
||||
paddingHorizontal: BaseTheme.spacing[2],
|
||||
paddingVertical: 2 * BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
roomNameInputContainer: {
|
||||
height: '0%'
|
||||
},
|
||||
|
||||
/**
|
||||
* Top-level screen style.
|
||||
*/
|
||||
page: {
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
* The styles for reduced UI mode.
|
||||
*/
|
||||
reducedUIContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.link01,
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
reducedUIText: {
|
||||
color: TEXT_COLOR,
|
||||
fontSize: 12
|
||||
},
|
||||
|
||||
/**
|
||||
* Container for room name input box and 'join' button.
|
||||
*/
|
||||
roomContainer: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'column',
|
||||
marginHorizontal: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
/**
|
||||
* The container of the label of the audio-video switch.
|
||||
*/
|
||||
switchLabel: {
|
||||
paddingHorizontal: 3
|
||||
},
|
||||
|
||||
/**
|
||||
* Room input style.
|
||||
*/
|
||||
textInput: {
|
||||
backgroundColor: 'transparent',
|
||||
borderColor: BaseTheme.palette.ui10,
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
color: TEXT_COLOR,
|
||||
fontSize: 23,
|
||||
height: 50,
|
||||
padding: 4,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Application title style.
|
||||
*/
|
||||
title: {
|
||||
color: TEXT_COLOR,
|
||||
fontSize: 25,
|
||||
marginBottom: 2 * BoxModel.margin,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
insecureRoomNameWarningContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
insecureRoomNameWarningIcon: {
|
||||
color: BaseTheme.palette.warning02,
|
||||
fontSize: 24,
|
||||
marginRight: 10
|
||||
},
|
||||
|
||||
insecureRoomNameWarningText: {
|
||||
color: BaseTheme.palette.text01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the top-level container of {@code WelcomePage}.
|
||||
*/
|
||||
welcomePage: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
customInput: {
|
||||
fontSize: 18,
|
||||
letterSpacing: 0,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
recentList: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
recentListDisabled: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
flex: 1,
|
||||
opacity: 0.8,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
};
|
||||
44
react/features/welcome/constants.tsx
Normal file
44
react/features/welcome/constants.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IconCalendar, IconGear, IconRestore } from '../base/icons/svg';
|
||||
import BaseTheme from '../base/ui/components/BaseTheme';
|
||||
|
||||
import TabIcon from './components/TabIcon';
|
||||
|
||||
export const ACTIVE_TAB_COLOR = BaseTheme.palette.icon01;
|
||||
export const INACTIVE_TAB_COLOR = BaseTheme.palette.icon03;
|
||||
|
||||
export const tabBarOptions = {
|
||||
tabBarActiveTintColor: ACTIVE_TAB_COLOR,
|
||||
tabBarInactiveTintColor: INACTIVE_TAB_COLOR,
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 12,
|
||||
},
|
||||
tabBarStyle: {
|
||||
backgroundColor: BaseTheme.palette.ui01
|
||||
}
|
||||
};
|
||||
|
||||
export const recentListTabBarOptions = {
|
||||
tabBarIcon: ({ focused }: { focused: boolean; }) => (
|
||||
<TabIcon
|
||||
focused = { focused }
|
||||
src = { IconRestore } />
|
||||
)
|
||||
};
|
||||
|
||||
export const calendarListTabBarOptions = {
|
||||
tabBarIcon: ({ focused }: { focused: boolean; }) => (
|
||||
<TabIcon
|
||||
focused = { focused }
|
||||
src = { IconCalendar } />
|
||||
)
|
||||
};
|
||||
|
||||
export const settingsTabBarOptions = {
|
||||
tabBarIcon: ({ focused }: { focused: boolean; }) => (
|
||||
<TabIcon
|
||||
focused = { focused }
|
||||
src = { IconGear } />
|
||||
)
|
||||
};
|
||||
33
react/features/welcome/functions.ts
Normal file
33
react/features/welcome/functions.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { WELCOME_PAGE_ENABLED } from '../base/flags/constants';
|
||||
import { getFeatureFlag } from '../base/flags/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether the {@code WelcomePage} is enabled.
|
||||
*
|
||||
* @param {IStateful} 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 isWelcomePageEnabled(stateful: IStateful) {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return getFeatureFlag(stateful, WELCOME_PAGE_ENABLED, false);
|
||||
}
|
||||
|
||||
const config = toState(stateful)['features/base/config'];
|
||||
|
||||
return !config.welcomePage?.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured custom URL (if any) to redirect to instead of the normal landing page.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux state or {@link getState}.
|
||||
* @returns {string} - The custom URL.
|
||||
*/
|
||||
export function getCustomLandingPageURL(stateful: IStateful) {
|
||||
return toState(stateful)['features/base/config'].welcomePage?.customUrl;
|
||||
}
|
||||
Reference in New Issue
Block a user