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,95 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState, IStore } from '../../../app/types';
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 AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether or not the click is disabled.
*/
disableClick?: boolean;
/**
* Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Whether or not the hand is raised.
*/
raisedHand: boolean;
}
/**
* Implementation of a button for raising hand.
*/
class RaiseHandButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.lowerHand';
override icon = IconRaiseHand;
override label = 'toolbar.raiseHand';
override toggledLabel = 'toolbar.lowerYourHand';
override tooltip = 'toolbar.raiseHand';
override toggledTooltip = 'toolbar.lowerYourHand';
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props.raisedHand;
}
/**
* Handles clicking the button, and toggles the raise hand.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { disableClick, dispatch, raisedHand } = this.props;
if (disableClick) {
return;
}
sendAnalytics(createToolbarEvent(
'raise.hand',
{ enable: !raisedHand }));
dispatch(raiseHand(!raisedHand));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
const localParticipant = getLocalParticipant(state);
return {
raisedHand: hasRaisedHand(localParticipant)
};
};
export { RaiseHandButton };
export default translate(connect(mapStateToProps)(RaiseHandButton));

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../functions.web';
import RaiseHandButton from './RaiseHandButton';
import ReactionsMenuButton from './ReactionsMenuButton';
const RaiseHandContainerButton = (props: AbstractButtonProps) => {
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout);
const showReactionsAsPartOfRaiseHand
= _shouldDisplayReactionsButtons && !reactionsButtonEnabled && !isNarrowLayout && !isMobileBrowser();
return showReactionsAsPartOfRaiseHand
? <ReactionsMenuButton
{ ...props }
showRaiseHand = { true } />
: <RaiseHandButton { ...props } />;
};
export default RaiseHandContainerButton;

View File

@@ -0,0 +1,231 @@
import React, { Component } from 'react';
import Tooltip from '../../../base/tooltip/components/Tooltip';
import { TOOLTIP_POSITION } from '../../../base/ui/constants.any';
/**
* The type of the React {@code Component} props of {@link ReactionButton}.
*/
interface IProps {
/**
* A succinct description of what the button does. Used by accessibility
* tools and torture tests.
*/
accessibilityLabel: string;
/**
* The Icon of this {@code AbstractToolbarButton}.
*/
icon: Object;
/**
* The style of the Icon of this {@code AbstractToolbarButton}.
*/
iconStyle?: Object;
/**
* Optional label for the button.
*/
label?: string;
/**
* On click handler.
*/
onClick: Function;
/**
* {@code AbstractToolbarButton} Styles.
*/
style?: Array<string> | Object;
/**
* An optional modifier to render the button toggled.
*/
toggled?: boolean;
/**
* Optional text to display in the tooltip.
*/
tooltip?: string;
/**
* From which direction the tooltip should appear, relative to the
* button.
*/
tooltipPosition: TOOLTIP_POSITION;
/**
* The color underlying the button.
*/
underlayColor?: any;
}
/**
* The type of the React {@code Component} state of {@link ReactionButton}.
*/
interface IState {
/**
* Used to determine zoom level on reaction burst.
*/
increaseLevel: number;
/**
* Timeout ID to reset reaction burst.
*/
increaseTimeout: number | null;
}
/**
* Represents a button in the reactions menu.
*
* @augments AbstractToolbarButton
*/
class ReactionButton extends Component<IProps, IState> {
/**
* Default values for {@code ReactionButton} component's properties.
*
* @static
*/
static defaultProps = {
tooltipPosition: 'top'
};
/**
* Initializes a new {@code ReactionButton} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onKeyDown = this._onKeyDown.bind(this);
this._onClickHandler = this._onClickHandler.bind(this);
this._onClick = this._onClick.bind(this);
this.state = {
increaseLevel: 0,
increaseTimeout: null
};
}
/**
* Handles clicking/pressing this {@code AbstractToolbarButton} by
* forwarding the event to the {@code onClick} prop of this instance if any.
*
* @protected
* @returns {*} The result returned by the invocation of the {@code onClick}
* prop of this instance if any.
*/
_onClick(...args: any) {
const { onClick } = this.props;
return onClick?.(...args);
}
/**
* Handles 'Enter' key on the button to trigger onClick for accessibility.
* We should be handling Space onKeyUp but it conflicts with PTT.
*
* @param {Object} event - The key event.
* @private
* @returns {void}
*/
_onKeyDown(event: React.KeyboardEvent) {
// If the event coming to the dialog has been subject to preventDefault
// we don't handle it here.
if (event.defaultPrevented) {
return;
}
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
this.props.onClick();
}
}
/**
* Handles reaction button click.
*
* @param {Event} event - The click event.
* @returns {void}
*/
_onClickHandler(event: any) {
event.preventDefault();
event.stopPropagation();
this.props.onClick();
clearTimeout(this.state.increaseTimeout ?? 0);
const timeout = window.setTimeout(() => {
this.setState({
increaseLevel: 0
});
}, 500);
this.setState(state => {
return {
increaseLevel: state.increaseLevel + 1,
increaseTimeout: timeout
};
});
}
/**
* Renders the button of this {@code ReactionButton}.
*
* @param {Object} children - The children, if any, to be rendered inside
* the button. Presumably, contains the emoji of this {@code ReactionButton}.
* @protected
* @returns {ReactElement} The button of this {@code ReactionButton}.
*/
_renderButton(children: React.ReactElement) {
return (
<div
aria-label = { this.props.accessibilityLabel }
aria-pressed = { this.props.toggled }
className = 'toolbox-button'
onClick = { this._onClickHandler }
onKeyDown = { this._onKeyDown }
role = 'button'
tabIndex = { 0 }>
{ this.props.tooltip
? <Tooltip
content = { this.props.tooltip }
position = { this.props.tooltipPosition }>
{ children }
</Tooltip>
: children }
</div>
);
}
/**
* Renders the icon (emoji) of this {@code reactionButton}.
*
* @inheritdoc
*/
_renderIcon() {
const { toggled, icon, label } = this.props;
const { increaseLevel } = this.state;
return (
<div className = { `toolbox-icon ${toggled ? 'toggled' : ''}` }>
<span className = { `emoji increase-${increaseLevel > 12 ? 12 : increaseLevel}` }>{icon}</span>
{label && <span className = 'text'>{label}</span>}
</div>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return this._renderButton(this._renderIcon());
}
}
export default ReactionButton;

View File

@@ -0,0 +1,94 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { IStore } from '../../../app/types';
import { removeReaction } from '../../actions.any';
import { REACTIONS } from '../../constants';
interface IProps {
/**
* Index of the reaction in the queue.
*/
index: number;
/**
* Reaction to be displayed.
*/
reaction: string;
/**
* Removes reaction from redux state.
*/
reactionRemove: Function;
/**
* Id of the reaction.
*/
uid: string;
}
interface IState {
/**
* Index of CSS animation. Number between 0-20.
*/
index: number;
}
/**
* Used to display animated reactions.
*
* @returns {ReactElement}
*/
class ReactionEmoji extends Component<IProps, IState> {
/**
* Initializes a new {@code ReactionEmoji} instance.
*
* @param {IProps} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
this.state = {
index: props.index % 21
};
}
/**
* Implements React Component's componentDidMount.
*
* @inheritdoc
*/
override componentDidMount() {
setTimeout(() => this.props.reactionRemove(this.props.uid), 5000);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
override render() {
const { reaction, uid } = this.props;
const { index } = this.state;
return (
<div
className = { `reaction-emoji reaction-${index}` }
id = { uid }>
{ REACTIONS[reaction].emoji }
</div>
);
}
}
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
return {
reactionRemove: (uid: string) => dispatch(removeReaction(uid))
};
};
export default connect(undefined, mapDispatchToProps)(ReactionEmoji);

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getReactionsQueue, isReactionsEnabled, shouldDisplayReactionsButtons } from '../../functions.any';
import ReactionEmoji from './ReactionEmoji';
/**
* Renders the reactions animations in the case when there is no buttons displayed.
*
* @returns {ReactNode}
*/
export default function ReactionAnimations() {
const reactionsQueue = useSelector(getReactionsQueue);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const reactionsEnabled = useSelector(isReactionsEnabled);
if (reactionsEnabled && !_shouldDisplayReactionsButtons) {
return (<div className = 'reactions-animations-container'>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>);
}
return null;
}

View File

@@ -0,0 +1,259 @@
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { createReactionMenuEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState, IStore } from '../../../app/types';
import { raiseHand } from '../../../base/participants/actions';
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
import GifsMenu from '../../../gifs/components/web/GifsMenu';
import GifsMenuButton from '../../../gifs/components/web/GifsMenuButton';
import { isGifEnabled, isGifsMenuOpen } from '../../../gifs/functions';
import { dockToolbox } from '../../../toolbox/actions.web';
import { addReactionToBuffer } from '../../actions.any';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import {
GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU,
RAISE_HAND_ROW_HEIGHT, REACTIONS,
REACTIONS_MENU_HEIGHT_DRAWER,
REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU
} from '../../constants';
import { IReactionsMenuParent } from '../../types';
import ReactionButton from './ReactionButton';
interface IProps {
/**
* Docks the toolbox.
*/
_dockToolbox: Function;
/**
* Whether or not the GIF feature is enabled.
*/
_isGifEnabled: boolean;
/**
* Whether or not the GIF menu is visible.
*/
_isGifMenuVisible: boolean;
/**
* The ID of the local participant.
*/
_localParticipantID?: string;
/**
* Whether or not the local participant's hand is raised.
*/
_raisedHand: boolean;
/**
* The Redux Dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Indicates the parent of the reactions menu.
*/
parent: IReactionsMenuParent;
/**
* Whether to show the raised hand button.
*/
showRaisedHand?: boolean;
}
const useStyles = makeStyles<IProps>()((theme, props: IProps) => {
const { parent, showRaisedHand, _isGifMenuVisible } = props;
let reactionsMenuHeight = REACTIONS_MENU_HEIGHT_DRAWER;
if (parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu) {
if (parent === IReactionsMenuParent.OverflowMenu) {
reactionsMenuHeight = REACTIONS_MENU_HEIGHT_IN_OVERFLOW_MENU;
if (_isGifMenuVisible) {
reactionsMenuHeight += GIFS_MENU_HEIGHT_IN_OVERFLOW_MENU;
}
}
if (!showRaisedHand) {
reactionsMenuHeight -= RAISE_HAND_ROW_HEIGHT;
}
}
return {
reactionsMenuInOverflowMenu: {
'&.reactions-menu': {
'&.with-gif': {
width: 'inherit'
},
'.reactions-row': {
'.toolbox-icon': {
width: '24px',
height: '24px',
'span.emoji': {
width: '24px',
height: '24px',
lineHeight: '1.5rem',
fontSize: '1rem'
}
}
},
'.raise-hand-row': {
'.toolbox-icon': {
height: '32px'
}
}
}
},
overflow: {
width: 'auto',
paddingBottom: 'max(env(safe-area-inset-bottom, 0), 16px)',
backgroundColor: theme.palette.ui01,
boxShadow: 'none',
borderRadius: 0,
position: 'relative',
boxSizing: 'border-box',
height: `${reactionsMenuHeight}px`
}
};
});
const _getReactionButtons = (dispatch: IStore['dispatch'], t: Function) => {
let modifierKey = 'Alt';
if (window.navigator?.platform) {
if (window.navigator.platform.indexOf('Mac') !== -1) {
modifierKey = '⌥';
}
}
return Object.keys(REACTIONS).map(key => {
/**
* Sends reaction message.
*
* @returns {void}
*/
function doSendReaction() {
dispatch(addReactionToBuffer(key));
sendAnalytics(createReactionMenuEvent(key));
}
return (<ReactionButton
accessibilityLabel = { t(`toolbar.accessibilityLabel.${key}`) }
icon = { REACTIONS[key].emoji }
key = { key }
// eslint-disable-next-line react/jsx-no-bind
onClick = { doSendReaction }
toggled = { false }
tooltip = { `${t(`toolbar.${key}`)} (${modifierKey} + ${REACTIONS[key].shortcutChar})` } />);
});
};
const ReactionsMenu = (props: IProps) => {
const {
_dockToolbox,
_isGifEnabled,
_isGifMenuVisible,
_raisedHand,
dispatch,
parent,
showRaisedHand = false
} = props;
const isInOverflowMenu
= parent === IReactionsMenuParent.OverflowDrawer || parent === IReactionsMenuParent.OverflowMenu;
const { classes, cx } = useStyles(props);
const { t } = useTranslation();
useEffect(() => {
_dockToolbox(true);
return () => {
_dockToolbox(false);
};
}, []);
const _doToggleRaiseHand = useCallback(() => {
dispatch(raiseHand(!_raisedHand));
}, [ _raisedHand ]);
const _onToolbarToggleRaiseHand = useCallback(() => {
sendAnalytics(createToolbarEvent(
'raise.hand',
{ enable: !_raisedHand }));
_doToggleRaiseHand();
dispatch(toggleReactionsMenuVisibility());
}, [ _raisedHand ]);
const buttons = _getReactionButtons(dispatch, t);
if (_isGifEnabled) {
buttons.push(<GifsMenuButton parent = { parent } />);
}
return (
<div
className = { cx('reactions-menu',
parent === IReactionsMenuParent.OverflowMenu && classes.reactionsMenuInOverflowMenu,
_isGifEnabled && 'with-gif',
isInOverflowMenu && `overflow ${classes.overflow}`) }>
{_isGifEnabled && _isGifMenuVisible
&& <GifsMenu
columns = { parent === IReactionsMenuParent.OverflowMenu ? 1 : undefined }
parent = { parent } />}
<div className = 'reactions-row'>
{ buttons }
</div>
{showRaisedHand && (
<div className = 'raise-hand-row'>
<ReactionButton
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
icon = '✋'
key = 'raisehand'
label = {
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
${isInOverflowMenu ? '' : ' (R)'}`
}
onClick = { _onToolbarToggleRaiseHand }
toggled = { true } />
</div>
)}
</div>
);
};
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const localParticipant = getLocalParticipant(state);
return {
_localParticipantID: localParticipant?.id,
_isGifEnabled: isGifEnabled(state),
_isGifMenuVisible: isGifsMenuOpen(state),
_raisedHand: hasRaisedHand(localParticipant)
};
}
/**
* Function that maps parts of Redux actions into component props.
*
* @param {Object} dispatch - Redux dispatch.
* @returns {Object}
*/
function mapDispatchToProps(dispatch: IStore['dispatch']) {
return {
dispatch,
_dockToolbox: (dock: boolean) => dispatch(dockToolbox(dock))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ReactionsMenu);

View File

@@ -0,0 +1,188 @@
import React, { ReactElement, useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { IconArrowUp, IconFaceSmile } from '../../../base/icons/svg';
import AbstractButton, { type IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import ToolboxButtonWithPopup from '../../../base/toolbox/components/web/ToolboxButtonWithPopup';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { IReactionEmojiProps } from '../../constants';
import { getReactionsQueue } from '../../functions.any';
import { getReactionsMenuVisibility, isReactionsButtonEnabled } from '../../functions.web';
import { IReactionsMenuParent } from '../../types';
import RaiseHandButton from './RaiseHandButton';
import ReactionEmoji from './ReactionEmoji';
import ReactionsMenu from './ReactionsMenu';
interface IProps extends WithTranslation {
/**
* Whether a mobile browser is used or not.
*/
_isMobile: boolean;
/**
* Whether the reactions should be displayed on separate button or not.
*/
_reactionsButtonEnabled: boolean;
/**
* The button's key.
*/
buttonKey?: string;
/**
* Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Whether or not it's narrow mode or mobile browser.
*/
isNarrow: boolean;
/**
* Whether or not the reactions menu is open.
*/
isOpen: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
*/
notifyMode?: string;
/**
* The array of reactions to be displayed.
*/
reactionsQueue: Array<IReactionEmojiProps>;
/**
* Whether or not to show the raise hand button.
*/
showRaiseHand?: boolean;
}
/**
* Implementation of a button for reactions.
*/
class ReactionsButtonImpl extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.reactions';
override icon = IconFaceSmile;
override label = 'toolbar.reactions';
override toggledLabel = 'toolbar.reactions';
override tooltip = 'toolbar.reactions';
}
const ReactionsButton = translate(connect()(ReactionsButtonImpl));
/**
* Button used for the reactions menu.
*
* @returns {ReactElement}
*/
function ReactionsMenuButton({
_reactionsButtonEnabled,
_isMobile,
buttonKey,
dispatch,
isOpen,
isNarrow,
notifyMode,
reactionsQueue,
showRaiseHand,
t
}: IProps) {
const toggleReactionsMenu = useCallback(() => {
dispatch(toggleReactionsMenuVisibility());
}, [ dispatch ]);
const openReactionsMenu = useCallback(() => {
!isOpen && toggleReactionsMenu();
}, [ isOpen, toggleReactionsMenu ]);
const closeReactionsMenu = useCallback(() => {
isOpen && toggleReactionsMenu();
}, [ isOpen, toggleReactionsMenu ]);
if (!showRaiseHand && !_reactionsButtonEnabled) {
return null;
}
const reactionsMenu = (<div className = 'reactions-menu-container'>
<ReactionsMenu parent = { IReactionsMenuParent.Button } />
</div>);
let content: ReactElement | null = null;
if (showRaiseHand) {
content = isNarrow
? (
<RaiseHandButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />)
: (
<ToolboxButtonWithPopup
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconArrowUp }
iconDisabled = { false }
onPopoverClose = { toggleReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }
visible = { isOpen }>
<RaiseHandButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithPopup>);
} else {
content = (
<ToolboxButtonWithPopup
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
onPopoverClose = { closeReactionsMenu }
onPopoverOpen = { openReactionsMenu }
popoverContent = { reactionsMenu }
trigger = { _isMobile ? 'click' : undefined }
visible = { isOpen }>
<ReactionsButton
buttonKey = { buttonKey }
notifyMode = { notifyMode } />
</ToolboxButtonWithPopup>);
}
return (
<div className = 'reactions-menu-popup-container'>
{ content }
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />))}
</div>
);
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
_reactionsButtonEnabled: isReactionsButtonEnabled(state),
_isMobile: isMobileBrowser(),
isOpen: getReactionsMenuVisibility(state),
isNarrow: isNarrowLayout,
reactionsQueue: getReactionsQueue(state)
};
}
export default translate(connect(mapStateToProps)(ReactionsMenuButton));