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,6 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractAudioMuteButton, { IProps, mapStateToProps } from '../AbstractAudioMuteButton';
export default translate(connect(mapStateToProps)(AbstractAudioMuteButton<IProps>));

View File

@@ -0,0 +1,96 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { setAudioOnly, toggleAudioOnly } from '../../../base/audio-only/actions';
import { AUDIO_ONLY_BUTTON_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnly, IconAudioOnlyOff } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import {
navigate
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* The type of the React {@code Component} props of {@link AudioOnlyButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Indicates whether the car mode is enabled.
*/
_startCarMode?: boolean;
}
/**
* An implementation of a button for toggling the audio-only mode.
*/
class AudioOnlyButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.audioOnly';
override icon = IconAudioOnly;
override label = 'toolbar.audioOnlyOn';
override toggledIcon = IconAudioOnlyOff;
override toggledLabel = 'toolbar.audioOnlyOff';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const { _audioOnly, _startCarMode, dispatch } = this.props;
if (!_audioOnly && _startCarMode) {
dispatch(setAudioOnly(true));
navigate(screen.conference.carmode);
} else {
dispatch(toggleAudioOnly());
}
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._audioOnly;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code AudioOnlyButton} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const enabledInFeatureFlags = getFeatureFlag(state, AUDIO_ONLY_BUTTON_ENABLED, true);
const { startCarMode } = state['features/base/settings'];
const { visible = enabledInFeatureFlags } = ownProps;
return {
_audioOnly: Boolean(audioOnly),
_startCarMode: startCarMode,
visible
};
}
export default translate(connect(_mapStateToProps)(AudioOnlyButton));

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { Image, View, ViewStyle } from 'react-native';
import { SvgCssUri } from 'react-native-svg/css';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import styles from './styles';
export interface ICustomOptionButton extends AbstractButtonProps {
backgroundColor?: string;
icon: any;
id?: string;
isToolboxButton?: boolean;
text: string;
}
/**
* Component that renders a custom button.
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<ICustomOptionButton> {
backgroundColor = this.props.backgroundColor;
iconSrc = this.props.icon;
id = this.props.id;
text = this.props.text;
/**
* Custom icon component.
*
* @returns {React.Component}
*/
icon = () => {
let iconComponent;
if (!this.iconSrc) {
return null;
}
if (this.iconSrc?.includes('svg')) {
iconComponent = (
<SvgCssUri
// @ts-ignore
height = { BaseTheme.spacing[4] }
uri = { this.iconSrc }
width = { BaseTheme.spacing[4] } />
);
} else {
iconComponent = (
<Image
height = { BaseTheme.spacing[4] }
resizeMode = { 'contain' }
source = {{ uri: this.iconSrc }}
width = { BaseTheme.spacing[4] } />
);
}
return (
<View
style = { this.props.isToolboxButton && [
styles.toolboxButtonIconContainer,
{ backgroundColor: this.backgroundColor } ] as ViewStyle[] }>
{ iconComponent }
</View>
);
};
label = this.text || '';
}
export default translate(connect()(CustomOptionButton));

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import HangupButton from '../HangupButton';
import HangupMenuButton from './HangupMenuButton';
const HangupContainerButtons = (props: AbstractButtonProps) => {
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
const endConferenceSupported = conference?.isEndConferenceSupported();
return endConferenceSupported
// @ts-ignore
? <HangupMenuButton { ...props } />
: <HangupButton { ...props } />;
};
export default HangupContainerButtons;

View File

@@ -0,0 +1,77 @@
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { appNavigate } from '../../../app/actions';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { endConference } from '../../../base/conference/actions';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { moveToRoom } from '../../../breakout-rooms/actions';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
/**
* Menu presenting options to leave a room or meeting and to end meeting.
*
* @returns {JSX.Element} - The hangup menu.
*/
function HangupMenu() {
const dispatch = useDispatch();
const _styles: any = useSelector((state: IReduxState) => ColorSchemeRegistry.get(state, 'Toolbox'));
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isModerator = useSelector((state: IReduxState) =>
getLocalParticipant(state)?.role === PARTICIPANT_ROLE.MODERATOR);
const { DESTRUCTIVE, SECONDARY } = BUTTON_TYPES;
const handleEndConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('endmeeting'));
dispatch(endConference());
}, [ hideSheet ]);
const handleLeaveConference = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('hangup'));
dispatch(appNavigate(undefined));
}, [ hideSheet ]);
const handleLeaveBreakoutRoom = useCallback(() => {
dispatch(hideSheet());
sendAnalytics(createBreakoutRoomsEvent('leave'));
dispatch(moveToRoom());
}, [ hideSheet ]);
return (
<BottomSheet>
<View style = { _styles.hangupMenuContainer }>
{ isModerator && <Button
accessibilityLabel = 'toolbar.endConference'
labelKey = 'toolbar.endConference'
onClick = { handleEndConference }
style = { _styles.hangupButton }
type = { DESTRUCTIVE } /> }
<Button
accessibilityLabel = 'toolbar.leaveConference'
labelKey = 'toolbar.leaveConference'
onClick = { handleLeaveConference }
style = { _styles.hangupButton }
type = { SECONDARY } />
{ inBreakoutRoom && <Button
accessibilityLabel = 'breakoutRooms.actions.leaveBreakoutRoom'
labelKey = 'breakoutRooms.actions.leaveBreakoutRoom'
onClick = { handleLeaveBreakoutRoom }
style = { _styles.hangupButton }
type = { SECONDARY } /> }
</View>
</BottomSheet>
);
}
export default HangupMenu;

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../base/dialog/actions';
import { IconHangup } from '../../../base/icons/svg';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import HangupMenu from './HangupMenu';
/**
* Button for showing the hangup menu.
*
* @returns {JSX.Element} - The hangup menu button.
*/
const HangupMenuButton = (): JSX.Element => {
const dispatch = useDispatch();
const onSelect = useCallback(() => {
dispatch(openSheet(HangupMenu));
}, [ dispatch ]);
return (
<IconButton
accessibilityLabel = 'toolbar.accessibilityLabel.hangup'
onPress = { onSelect }
src = { IconHangup }
type = { BUTTON_TYPES.PRIMARY } />
);
};
export default HangupMenuButton;

View File

@@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
sendAnalytics(createToolbarEvent('link.to.salesforce'));
return navigate(screen.conference.salesforce);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state: IReduxState) {
return {
visible: isSalesforceEnabled(state)
};
}
export default translate(connect(mapStateToProps)(LinkToSalesforceButton));

View File

@@ -0,0 +1,49 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { CAR_MODE_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconCar } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* Implements an {@link AbstractButton} to open the carmode.
*/
class OpenCarmodeButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.carmode';
override icon = IconCar;
override label = 'carmode.labels.buttonLabel';
/**
* Handles clicking / pressing the button, and opens the carmode mode.
*
* @private
* @returns {void}
*/
override _handleClick() {
return navigate(screen.conference.carmode);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {IReduxState} state - The Redux state.
* @param {AbstractButtonProps} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState, ownProps: AbstractButtonProps): Object {
const enabled = getFeatureFlag(state, CAR_MODE_ENABLED, true);
const { visible = enabled } = ownProps;
return {
visible
};
}
export default translate(connect(_mapStateToProps)(OpenCarmodeButton));

View File

@@ -0,0 +1,316 @@
import React, { PureComponent } from 'react';
import { ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import SettingsButton from '../../../base/settings/components/native/SettingsButton';
import BreakoutRoomsButton
from '../../../breakout-rooms/components/native/BreakoutRoomsButton';
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.native';
import ReactionMenu from '../../../reactions/components/native/ReactionMenu';
import { shouldDisplayReactionsButtons } from '../../../reactions/functions.any';
import LiveStreamButton from '../../../recording/components/LiveStream/native/LiveStreamButton';
import RecordButton from '../../../recording/components/Recording/native/RecordButton';
import SecurityDialogButton
from '../../../security/components/security-dialog/native/SecurityDialogButton';
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
import { isSharedVideoEnabled } from '../../../shared-video/functions';
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
import styles from '../../../video-menu/components/native/styles';
import { iAmVisitor } from '../../../visitors/functions';
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
import { customButtonPressed } from '../../actions.native';
import { getVisibleNativeButtons } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import AudioOnlyButton from './AudioOnlyButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OpenCarmodeButton from './OpenCarmodeButton';
import RaiseHandButton from './RaiseHandButton';
/**
* The type of the React {@code Component} props of {@link OverflowMenu}.
*/
interface IProps {
/**
* True if breakout rooms feature is available, false otherwise.
*/
_isBreakoutRoomsSupported?: boolean;
/**
* True if the overflow menu is currently visible, false otherwise.
*/
_isOpen: boolean;
/**
* Whether the shared video is enabled or not.
*/
_isSharedVideoEnabled: boolean;
/**
* Whether or not speaker stats is disable.
*/
_isSpeakerStatsDisabled?: boolean;
/**
* Toolbar buttons.
*/
_mainMenuButtons?: Array<IToolboxNativeButton>;
/**
* Overflow menu buttons.
*/
_overflowMenuButtons?: Array<IToolboxNativeButton>;
/**
* Whether the recoding button should be enabled or not.
*/
_recordingEnabled: boolean;
/**
* Whether or not any reactions buttons should be displayed.
*/
_shouldDisplayReactionsButtons: boolean;
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: IStore['dispatch'];
}
interface IState {
/**
* True if the bottom sheet is scrolled to the top.
*/
scrolledToTop: boolean;
}
/**
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class OverflowMenu extends PureComponent<IProps, IState> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
scrolledToTop: true
};
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._renderReactionMenu = this._renderReactionMenu.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
_isBreakoutRoomsSupported,
_isSpeakerStatsDisabled,
_isSharedVideoEnabled,
dispatch
} = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
styles: bottomSheetStyles.buttons
};
const topButtonProps = {
afterClick: this._onCancel,
dispatch,
showLabel: true,
styles: {
...bottomSheetStyles.buttons,
style: {
...bottomSheetStyles.buttons.style,
borderTopLeftRadius: 16,
borderTopRightRadius: 16
}
}
};
return (
<BottomSheet
renderFooter = { this._renderReactionMenu }>
<Divider style = { styles.divider as ViewStyle } />
<OpenCarmodeButton { ...topButtonProps } />
<AudioOnlyButton { ...buttonProps } />
{ this._renderRaiseHandButton(buttonProps) }
{/* @ts-ignore */}
<SecurityDialogButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<LinkToSalesforceButton { ...buttonProps } />
<WhiteboardButton { ...buttonProps } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
{ this._renderOverflowMenuButtons(topButtonProps) }
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
{_isBreakoutRoomsSupported && <BreakoutRoomsButton { ...buttonProps } />}
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<ClosedCaptionButton { ...buttonProps } />
<SharedDocumentButton { ...buttonProps } />
<SettingsButton { ...buttonProps } />
</BottomSheet>
);
}
/**
* Hides this {@code OverflowMenu}.
*
* @private
* @returns {void}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @returns {React.ReactElement}
*/
_renderReactionMenu() {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { true } />
);
}
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @param {Object} buttonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderRaiseHandButton(buttonProps: Object) {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (!_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<RaiseHandButton { ...buttonProps } />
);
}
}
/**
* Function to render the custom buttons for the overflow menu.
*
* @param {Object} topButtonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderOverflowMenuButtons(topButtonProps: Object) {
const { _overflowMenuButtons, dispatch } = this.props;
if (!_overflowMenuButtons?.length) {
return;
}
return (
<>
{
_overflowMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => {
if (key === 'raisehand') {
return null;
}
return (
<Content
{ ...topButtonProps }
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { false }
key = { key }
text = { text } />
);
})
}
<Divider style = { styles.divider as ViewStyle } />
</>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
return {
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
_isSharedVideoEnabled: isSharedVideoEnabled(state),
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state)
};
}
export default connect(_mapStateToProps)(props => {
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const _iAmVisitor = useSelector(iAmVisitor);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons, overflowMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
mainToolbarButtonsThresholds,
toolbarButtons,
iAmVisitor: _iAmVisitor
});
return (
<OverflowMenu
// @ts-ignore
{ ... props }
_mainMenuButtons = { mainMenuButtons }
_overflowMenuButtons = { overflowMenuButtons } />
);
});

View File

@@ -0,0 +1,50 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openSheet } from '../../../base/dialog/actions';
import { OVERFLOW_MENU_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconDotsHorizontal } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import OverflowMenu from './OverflowMenu';
/**
* An implementation of a button for showing the {@code OverflowMenu}.
*/
class OverflowMenuButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.moreActions';
override icon = IconDotsHorizontal;
override label = 'toolbar.moreActions';
/**
* Handles clicking / pressing this {@code OverflowMenuButton}.
*
* @protected
* @returns {void}
*/
override _handleClick() {
// @ts-ignore
this.props.dispatch(openSheet(OverflowMenu));
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code OverflowMenuButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState) {
const enabledFlag = getFeatureFlag(state, OVERFLOW_MENU_ENABLED, true);
return {
visible: enabledFlag
};
}
export default translate(connect(_mapStateToProps)(OverflowMenuButton));

View File

@@ -0,0 +1,99 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { RAISE_HAND_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconRaiseHand } from '../../../base/icons/svg';
import { raiseHand } from '../../../base/participants/actions';
import {
getLocalParticipant,
hasRaisedHand
} from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* The local participant.
*/
_localParticipant?: ILocalParticipant;
/**
* Whether the participant raised their hand or not.
*/
_raisedHand: boolean;
}
/**
* An implementation of a button to raise or lower hand.
*/
class RaiseHandButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
override icon = IconRaiseHand;
override label = 'toolbar.raiseYourHand';
override toggledLabel = 'toolbar.lowerYourHand';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this._toggleRaisedHand();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._raisedHand;
}
/**
* Toggles the rased hand status of the local participant.
*
* @returns {void}
*/
_toggleRaisedHand() {
const enable = !this.props._raisedHand;
sendAnalytics(createToolbarEvent('raise.hand', { enable }));
this.props.dispatch(raiseHand(enable));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The properties explicitly passed to the component instance.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const _localParticipant = getLocalParticipant(state);
const enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
const { visible = enabled } = ownProps;
return {
_localParticipant,
_raisedHand: hasRaisedHand(_localParticipant),
visible
};
}
export default translate(connect(_mapStateToProps)(RaiseHandButton));

View File

@@ -0,0 +1,91 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { ANDROID_SCREENSHARING_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { toggleScreensharing } from '../../../base/tracks/actions.native';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ScreenSharingAndroidButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* True if the button needs to be disabled.
*/
_disabled: boolean;
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean;
}
/**
* An implementation of a button for toggling screen sharing.
*/
class ScreenSharingAndroidButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override icon = IconScreenshare;
override label = 'toolbar.startScreenSharing';
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const enable = !this._isToggled();
this.props.dispatch(toggleScreensharing(enable));
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _screensharing: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, ANDROID_SCREENSHARING_ENABLED, true);
return {
_screensharing: isLocalVideoTrackDesktop(state),
visible: enabled
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingAndroidButton));

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Platform } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isDesktopShareButtonDisabled } from '../../functions.native';
import ScreenSharingAndroidButton from './ScreenSharingAndroidButton';
import ScreenSharingIosButton from './ScreenSharingIosButton';
const ScreenSharingButton = (props: any) => (
<>
{Platform.OS === 'android'
&& <ScreenSharingAndroidButton { ...props } />
}
{Platform.OS === 'ios'
&& <ScreenSharingIosButton { ...props } />
}
</>
);
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ScreenSharingButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
return {
_disabled: isDesktopShareButtonDisabled(state)
};
}
export default connect(_mapStateToProps)(ScreenSharingButton);

View File

@@ -0,0 +1,148 @@
import React from 'react';
import { NativeModules, Platform, findNodeHandle } from 'react-native';
import { ScreenCapturePickerView } from 'react-native-webrtc';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IOS_SCREENSHARING_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconScreenshare } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ScreenSharingIosButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* True if the button needs to be disabled.
*/
_disabled: boolean;
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean;
}
const styles = {
screenCapturePickerView: {
display: 'none'
}
};
/**
* An implementation of a button for toggling screen sharing on iOS.
*/
class ScreenSharingIosButton extends AbstractButton<IProps> {
_nativeComponent: React.Component<any, any> | null;
override accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
override icon = IconScreenshare;
override label = 'toolbar.startScreenSharing';
override toggledLabel = 'toolbar.stopScreenSharing';
/**
* Initializes a new {@code ScreenSharingIosButton} instance.
*
* @param {Object} props - The React {@code Component} props to initialize
* the new {@code ScreenSharingIosButton} instance with.
*/
constructor(props: IProps) {
super(props);
this._nativeComponent = null;
// Bind event handlers so they are only bound once per instance.
this._setNativeComponent = this._setNativeComponent.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {React$Node}
*/
override render() {
return (
<>
{ super.render() }
<ScreenCapturePickerView
ref = { this._setNativeComponent } // @ts-ignore
style = { styles.screenCapturePickerView } />
</>
);
}
/**
* Sets the internal reference to the React Component wrapping the
* {@code RPSystemBroadcastPickerView} component.
*
* @param {ReactComponent} component - React Component.
* @returns {void}
*/
_setNativeComponent(component: React.Component<any, any> | null) {
this._nativeComponent = component;
}
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const handle = findNodeHandle(this._nativeComponent);
NativeModules.ScreenCapturePickerViewManager.show(handle);
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ScreenSharingIosButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disabled: boolean,
* }}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, IOS_SCREENSHARING_ENABLED, false);
return {
_screensharing: isLocalVideoTrackDesktop(state),
// TODO: this should work on iOS 12 too, but our trick to show the picker doesn't work.
visible: enabled
&& Platform.OS === 'ios'
&& Number.parseInt(Platform.Version.split('.')[0], 10) >= 14
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingIosButton));

View File

@@ -0,0 +1,79 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCameraRefresh } from '../../../base/icons/svg';
import { toggleCameraFacingMode } from '../../../base/media/actions';
import { MEDIA_TYPE } from '../../../base/media/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isLocalTrackMuted } from '../../../base/tracks/functions.native';
/**
* The type of the React {@code Component} props of {@link ToggleCameraButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the current conference is in audio only mode or not.
*/
_audioOnly: boolean;
/**
* Whether video is currently muted or not.
*/
_videoMuted: boolean;
}
/**
* An implementation of a button for toggling the camera facing mode.
*/
class ToggleCameraButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.toggleCamera';
override icon = IconCameraRefresh;
override label = 'toolbar.toggleCamera';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this.props.dispatch(toggleCameraFacingMode());
}
/**
* Indicates whether this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._audioOnly || this.props._videoMuted;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean,
* _videoMuted: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const tracks = state['features/base/tracks'];
return {
_audioOnly: Boolean(audioOnly),
_videoMuted: isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO)
};
}
export default translate(connect(_mapStateToProps)(ToggleCameraButton));

View File

@@ -0,0 +1,74 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconAudioOnlyOff } from '../../../base/icons/svg';
import { updateSettings } from '../../../base/settings/actions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link ToggleSelfViewButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the self view is disabled or not.
*/
_disableSelfView: boolean;
}
/**
* An implementation of a button for toggling the self view.
*/
class ToggleSelfViewButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.selfView';
override icon = IconAudioOnlyOff;
override label = 'videothumbnail.hideSelfView';
override toggledLabel = 'videothumbnail.showSelfView';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
const { _disableSelfView, dispatch } = this.props;
dispatch(updateSettings({
disableSelfView: !_disableSelfView
}));
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._disableSelfView;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleSelfViewButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disableSelfView: boolean
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { disableSelfView } = state['features/base/settings'];
return {
_disableSelfView: Boolean(disableSelfView)
};
}
export default translate(connect(_mapStateToProps)(ToggleSelfViewButton));

View File

@@ -0,0 +1,142 @@
import React from 'react';
import { View, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import Platform from '../../../base/react/Platform.native';
import { iAmVisitor } from '../../../visitors/functions';
import { customButtonPressed } from '../../actions.native';
import { getVisibleNativeButtons, isToolboxVisible } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import styles from './styles';
/**
* The type of {@link Toolbox}'s React {@code Component} props.
*/
interface IProps {
/**
* Whether we are in visitors mode.
*/
_iAmVisitor: boolean;
/**
* The color-schemed stylesheet of the feature.
*/
_styles: any;
/**
* The indicator which determines whether the toolbox is visible.
*/
_visible: boolean;
/**
* Redux store dispatch method.
*/
dispatch: IStore['dispatch'];
}
/**
* Implements the conference Toolbox on React Native.
*
* @param {Object} props - The props of the component.
* @returns {React$Element}
*/
function Toolbox(props: IProps) {
const {
_iAmVisitor,
_styles,
_visible,
dispatch
} = props;
if (!_visible) {
return null;
}
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
iAmVisitor: _iAmVisitor,
mainToolbarButtonsThresholds,
toolbarButtons
});
const bottomEdge = Platform.OS === 'ios' && _visible;
const { buttonStylesBorderless, hangupButtonStyles } = _styles;
const style = { ...styles.toolbox };
// We have only hangup and raisehand button in _iAmVisitor mode
if (_iAmVisitor) {
style.justifyContent = 'center';
}
const renderToolboxButtons = () => {
if (!mainMenuButtons?.length) {
return;
}
return (
<>
{
mainMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => (
<Content
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { true }
key = { key }
styles = { key === 'hangup' ? hangupButtonStyles : buttonStylesBorderless } />
))
}
</>
);
};
return (
<View
style = { styles.toolboxContainer as ViewStyle }>
<SafeAreaView
accessibilityRole = 'toolbar'
// @ts-ignore
edges = { [ bottomEdge && 'bottom' ].filter(Boolean) }
pointerEvents = 'box-none'
style = { style as ViewStyle }>
{ renderToolboxButtons() }
</SafeAreaView>
</View>
);
}
/**
* Maps parts of the redux state to {@link Toolbox} (React {@code Component})
* props.
*
* @param {Object} state - The redux state of which parts are to be mapped to
* {@code Toolbox} props.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
_iAmVisitor: iAmVisitor(state),
_styles: ColorSchemeRegistry.get(state, 'Toolbox'),
_visible: isToolboxVisible(state),
};
}
export default connect(_mapStateToProps)(Toolbox);

View File

@@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractVideoMuteButton, { IProps, mapStateToProps } from '../AbstractVideoMuteButton';
export default translate(connect(mapStateToProps)(AbstractVideoMuteButton<IProps>));

View File

@@ -0,0 +1,216 @@
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { schemeColor } from '../../../base/color-scheme/functions';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const BUTTON_SIZE = 48;
// Toolbox, toolbar:
/**
* The style of toolbar buttons.
*/
const toolbarButton = {
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 0,
flex: 0,
flexDirection: 'row',
height: BUTTON_SIZE,
justifyContent: 'center',
marginHorizontal: 6,
marginVertical: 6,
width: BUTTON_SIZE
};
/**
* The icon style of the toolbar buttons.
*/
const toolbarButtonIcon = {
alignSelf: 'center',
color: BaseTheme.palette.icon04,
fontSize: 24
};
/**
* The icon style of toolbar buttons which display white icons.
*/
const whiteToolbarButtonIcon = {
...toolbarButtonIcon,
color: BaseTheme.palette.icon01
};
/**
* The style of reaction buttons.
*/
const reactionButton = {
...toolbarButton,
backgroundColor: 'transparent',
alignItems: 'center',
marginTop: 0,
marginHorizontal: 0
};
const gifButton = {
...reactionButton,
backgroundColor: '#000'
};
/**
* The style of the emoji on the reaction buttons.
*/
const reactionEmoji = {
fontSize: 20,
color: BaseTheme.palette.icon01
};
const reactionMenu = {
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01
};
/**
* The Toolbox and toolbar related styles.
*/
const styles = {
sheetGestureRecognizer: {
alignItems: 'stretch',
flexDirection: 'column'
},
/**
* The style of the toolbar.
*/
toolbox: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.uiBackground,
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
flexDirection: 'row',
justifyContent: 'space-between'
},
/**
* The style of the root/top-level container of {@link Toolbox}.
*/
toolboxContainer: {
backgroundColor: BaseTheme.palette.uiBackground,
flexDirection: 'column',
maxWidth: 580,
marginHorizontal: 'auto',
marginVertical: BaseTheme.spacing[0],
paddingHorizontal: BaseTheme.spacing[2],
width: '100%'
},
toolboxButtonIconContainer: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
justifyContent: 'center',
width: BaseTheme.spacing[7]
}
};
export default styles;
/**
* Color schemed styles for the @{Toolbox} component.
*/
ColorSchemeRegistry.register('Toolbox', {
/**
* Styles for buttons in the toolbar.
*/
buttonStyles: {
iconStyle: toolbarButtonIcon,
style: toolbarButton
},
buttonStylesBorderless: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton,
backgroundColor: 'transparent'
},
underlayColor: 'transparent'
},
backgroundToggle: {
backgroundColor: BaseTheme.palette.ui04
},
hangupMenuContainer: {
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButton: {
flex: 1,
marginHorizontal: BaseTheme.spacing[2],
marginVertical: BaseTheme.spacing[2]
},
hangupButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton,
backgroundColor: schemeColor('hangup')
},
underlayColor: BaseTheme.palette.ui04
},
reactionDialog: {
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'transparent'
},
overflowReactionMenu: {
...reactionMenu,
padding: BaseTheme.spacing[3]
},
reactionMenu: {
...reactionMenu,
paddingHorizontal: BaseTheme.spacing[3],
borderRadius: 3,
width: 360
},
reactionRow: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
},
reactionButton: {
gifButton,
style: reactionButton,
underlayColor: BaseTheme.palette.ui04,
emoji: reactionEmoji
},
emojiAnimation: {
color: BaseTheme.palette.icon01,
position: 'absolute',
zIndex: 1001,
elevation: 2,
fontSize: 20,
left: '50%',
top: '100%'
},
/**
* Styles for toggled buttons in the toolbar.
*/
toggledButtonStyles: {
iconStyle: whiteToolbarButtonIcon,
style: {
...toolbarButton
},
underlayColor: 'transparent'
}
});