init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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']
};
}

View File

@@ -0,0 +1 @@
export default undefined;

View 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;

View 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;

View 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));

View 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));

View 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'
}
};

View 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 } />
)
};

View 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;
}