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,192 @@
import React, { Component } from 'react';
import { Text } from 'react-native-paper';
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 { raiseHand } from '../../../base/participants/actions';
import {
getLocalParticipant,
hasRaisedHand
} from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether this button is enabled or not.
*/
_enabled: boolean;
/**
* The local participant.
*/
_localParticipant?: ILocalParticipant;
/**
* Whether the participant raused their hand or not.
*/
_raisedHand: boolean;
/**
* Whether or not the click is disabled.
*/
disableClick?: boolean;
/**
* Used to close the overflow menu after raise hand is clicked.
*/
onCancel: Function;
}
/**
* An implementation of a button to raise or lower hand.
*/
class RaiseHandButton extends Component<IProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
label = 'toolbar.raiseYourHand';
toggledLabel = 'toolbar.lowerYourHand';
/**
* Initializes a new {@code RaiseHandButton} instance.
*
* @param {IProps} props - The React {@code Component} props to initialize
* the new {@code RaiseHandButton} instance with.
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onClick = this._onClick.bind(this);
this._toggleRaisedHand = this._toggleRaisedHand.bind(this);
this._getLabel = this._getLabel.bind(this);
}
/**
* Handles clicking / pressing the button.
*
* @returns {void}
*/
_onClick() {
const { disableClick, onCancel } = this.props;
if (disableClick) {
return;
}
this._toggleRaisedHand();
onCancel();
}
/**
* 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));
}
/**
* Gets the current label, taking the toggled state into account. If no
* toggled label is provided, the regular label will also be used in the
* toggled state.
*
* @returns {string}
*/
_getLabel() {
const { _raisedHand, t } = this.props;
return t(_raisedHand ? this.toggledLabel : this.label);
}
/**
* Renders the "raise hand" emoji.
*
* @returns {ReactElement}
*/
_renderRaiseHandEmoji() {
return (
<Text></Text>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { _enabled } = this.props;
if (!_enabled) {
return null;
}
return (
<Button
accessibilityLabel = { this.accessibilityLabel }
icon = { this._renderRaiseHandEmoji }
labelKey = { this._getLabel() }
onClick = { this._onClick }
style = { styles.raiseHandButton }
type = { BUTTON_TYPES.SECONDARY } />
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const _localParticipant = getLocalParticipant(state);
const enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
return {
_enabled: enabled,
_localParticipant,
_raisedHand: hasRaisedHand(_localParticipant)
};
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _standaloneMapStateToProps(state: IReduxState) {
const _enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
return {
_enabled
};
}
const StandaloneRaiseHandButton = translate(connect(_standaloneMapStateToProps)(RaiseHandButton));
export { StandaloneRaiseHandButton };
export default translate(connect(_mapStateToProps)(RaiseHandButton));

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import RaiseHandButton from '../../../toolbox/components/native/RaiseHandButton';
import { shouldDisplayReactionsButtons } from '../../functions.native';
import ReactionsMenuButton from './ReactionsMenuButton';
const RaiseHandContainerButtons = (props: AbstractButtonProps) => {
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
return _shouldDisplayReactionsButtons
? <ReactionsMenuButton
{ ...props }
showRaiseHand = { true } />
: <RaiseHandButton { ...props } />;
};
export default RaiseHandContainerButtons;

View File

@@ -0,0 +1,105 @@
import React, { useCallback } from 'react';
import { WithTranslation } from 'react-i18next';
import { ColorValue, GestureResponderEvent, Text, TouchableHighlight, ViewStyle } from 'react-native';
import { useDispatch } from 'react-redux';
import { createReactionMenuEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { translate } from '../../../base/i18n/functions';
import { StyleType } from '../../../base/styles/functions.native';
import { addReactionToBuffer } from '../../actions.any';
import { REACTIONS } from '../../constants';
interface IReactionStyles {
/**
* Style for text container. Used on raise hand button.
*/
container?: StyleType;
/**
* Style for the emoji text on the button.
*/
emoji: StyleType;
/**
* Style for the gif button.
*/
gifButton: StyleType;
/**
* Style for the button.
*/
style: StyleType;
/**
* Style for the label text on the button.
*/
text?: StyleType;
/**
* Underlay color for the button.
*/
underlayColor: ColorValue;
}
/**
* The type of the React {@code Component} props of {@link ReactionButton}.
*/
interface IProps extends WithTranslation {
/**
* Component children.
*/
children?: React.ReactNode;
/**
* External click handler.
*/
onClick?: (e?: GestureResponderEvent) => void;
/**
* The reaction to be sent.
*/
reaction?: string;
/**
* Collection of styles for the button.
*/
styles: IReactionStyles;
}
/**
* An implementation of a button to send a reaction.
*
* @returns {ReactElement}
*/
function ReactionButton({
children,
onClick,
styles,
reaction,
t
}: IProps) {
const dispatch = useDispatch();
const _onClick = useCallback(() => {
if (reaction) {
dispatch(addReactionToBuffer(reaction));
sendAnalytics(createReactionMenuEvent(reaction));
}
}, [ reaction ]);
return (
<TouchableHighlight
accessibilityLabel = { t(`toolbar.accessibilityLabel.${reaction}`) }
accessibilityRole = 'button'
onPress = { onClick || _onClick }
style = { [ styles.style, children && styles?.gifButton ] as ViewStyle[] }
underlayColor = { styles.underlayColor }>
{children ?? <Text style = { styles.emoji }>{REACTIONS[reaction ?? ''].emoji}</Text>}
</TouchableHighlight>
);
}
export default translate(ReactionButton);

View File

@@ -0,0 +1,94 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Animated } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { removeReaction } from '../../actions.any';
import { IReactionEmojiProps, REACTIONS } from '../../constants';
interface IProps extends IReactionEmojiProps {
/**
* Index of reaction on the queue.
* Used to differentiate between first and other animations.
*/
index: number;
}
/**
* Animated reaction emoji.
*
* @returns {ReactElement}
*/
function ReactionEmoji({ reaction, uid, index }: IProps) {
const _styles: any = useSelector((state: IReduxState) => ColorSchemeRegistry.get(state, 'Toolbox'));
const _height = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientHeight);
const dispatch = useDispatch();
const animationVal = useRef(new Animated.Value(0)).current;
const vh = useState(_height / 100)[0];
const randomInt = (min: number, max: number) => Math.floor((Math.random() * (max - min + 1)) + min);
const animationIndex = useMemo(() => index % 21, [ index ]);
const coordinates = useState({
topX: animationIndex === 0 ? 40 : randomInt(-100, 100),
topY: animationIndex === 0 ? -70 : randomInt(-65, -75),
bottomX: animationIndex === 0 ? 140 : randomInt(150, 200),
bottomY: animationIndex === 0 ? -50 : randomInt(-40, -50)
})[0];
useEffect(() => {
setTimeout(() => dispatch(removeReaction(uid)), 5000);
}, []);
useEffect(() => {
Animated.timing(
animationVal,
{
toValue: 1,
duration: 5000,
useNativeDriver: true
}
).start();
}, [ animationVal ]);
return (
<Animated.Text
style = {{
..._styles.emojiAnimation,
transform: [
{ translateY: animationVal.interpolate({
inputRange: [ 0, 0.70, 0.75, 1 ],
outputRange: [ 0, coordinates.topY * vh, coordinates.topY * vh, coordinates.bottomY * vh ]
})
}, {
translateX: animationVal.interpolate({
inputRange: [ 0, 0.70, 0.75, 1 ],
outputRange: [ 0, coordinates.topX, coordinates.topX,
coordinates.topX < 0 ? -coordinates.bottomX : coordinates.bottomX ]
})
}, {
scale: animationVal.interpolate({
inputRange: [ 0, 0.70, 0.75, 1 ],
outputRange: [ 0.6, 1.5, 1.5, 1 ]
})
}
],
opacity: animationVal.interpolate({
inputRange: [ 0, 0.7, 0.75, 1 ],
outputRange: [ 1, 1, 1, 0 ]
})
}}>
{REACTIONS[reaction].emoji}
</Animated.Text>
);
}
export default ReactionEmoji;

View File

@@ -0,0 +1,76 @@
import React, { useCallback } from 'react';
import { Image, View } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { isGifEnabled } from '../../../gifs/functions.native';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { REACTIONS } from '../../constants';
import RaiseHandButton from './RaiseHandButton';
import ReactionButton from './ReactionButton';
/**
* The type of the React {@code Component} props of {@link ReactionMenu}.
*/
interface IProps {
/**
* Used to close the overflow menu after raise hand is clicked.
*/
onCancel: Function;
/**
* Whether or not it's displayed in the overflow menu.
*/
overflowMenu: boolean;
}
/**
* Animated reaction emoji.
*
* @returns {ReactElement}
*/
function ReactionMenu({
onCancel,
overflowMenu
}: IProps) {
const _styles: any = useSelector((state: IReduxState) => ColorSchemeRegistry.get(state, 'Toolbox'));
const gifEnabled = useSelector(isGifEnabled);
const openGifMenu = useCallback(() => {
navigate(screen.conference.gifsMenu);
onCancel();
}, []);
return (
<View style = { overflowMenu ? _styles.overflowReactionMenu : _styles.reactionMenu }>
<View style = { _styles.reactionRow }>
{
Object.keys(REACTIONS).map(key => (
<ReactionButton
key = { key }
reaction = { key }
styles = { _styles.reactionButton } />
))
}
{
gifEnabled && (
<ReactionButton
onClick = { openGifMenu }
styles = { _styles.reactionButton }>
<Image // @ts-ignore
height = { 22 }
source = { require('../../../../../images/GIPHY_icon.png') } />
</ReactionButton>
)
}
</View>
<RaiseHandButton onCancel = { onCancel } />
</View>
);
}
export default ReactionMenu;

View File

@@ -0,0 +1,141 @@
import React, { ComponentType, PureComponent } from 'react';
import { SafeAreaView, TouchableWithoutFeedback, View } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import { hideDialog } from '../../../base/dialog/actions';
import { isDialogOpen } from '../../../base/dialog/functions';
import { getParticipantCount } from '../../../base/participants/functions';
import { StyleType } from '../../../base/styles/functions.native';
import ReactionMenu from './ReactionMenu';
/**
* The type of the React {@code Component} props of {@link ReactionMenuDialog}.
*/
interface IProps {
/**
* The height of the screen.
*/
_height: number;
/**
* True if the dialog is currently visible, false otherwise.
*/
_isOpen: boolean;
/**
* Number of conference participants.
*/
_participantCount: number;
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType;
/**
* The width of the screen.
*/
_width: number;
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: IStore['dispatch'];
}
/**
* The exported React {@code Component}. We need it to execute
* {@link hideDialog}.
*
* XXX It does not break our coding style rule to not utilize globals for state,
* because it is merely another name for {@code export}'s {@code default}.
*/
let ReactionMenu_: ComponentType<any>; // eslint-disable-line prefer-const
/**
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class ReactionMenuDialog extends PureComponent<IProps> {
/**
* Initializes a new {@code ReactionMenuDialog} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { _styles, _width, _height, _participantCount } = this.props;
return (
<SafeAreaView style = { _styles }>
<TouchableWithoutFeedback
onPress = { this._onCancel }>
<View style = { _styles }>
<View
style = {{
left: (_width - 360) / 2,
top: _height - (_participantCount > 1 ? 144 : 80) - 80
}}>
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { false } />
</View>
</View>
</TouchableWithoutFeedback>
</SafeAreaView>
);
}
/**
* Hides this {@code ReactionMenuDialog}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
if (this.props._isOpen) {
this.props.dispatch(hideDialog(ReactionMenu_));
return true;
}
return false;
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
_isOpen: isDialogOpen(state, ReactionMenu_), // @ts-ignore
_styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
_width: state['features/base/responsive-ui'].clientWidth,
_height: state['features/base/responsive-ui'].clientHeight,
_participantCount: getParticipantCount(state)
};
}
ReactionMenu_ = connect(_mapStateToProps)(ReactionMenuDialog);
export default ReactionMenu_;

View File

@@ -0,0 +1,85 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { isDialogOpen } from '../../../base/dialog/functions';
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 {
getLocalParticipant, hasRaisedHand
} from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import ReactionMenuDialog from './ReactionMenuDialog';
/**
* The type of the React {@code Component} props of {@link ReactionsMenuButton}.
*/
interface IProps extends AbstractButtonProps {
/**
* Whether the participant raised their hand or not.
*/
_raisedHand: boolean;
/**
* Whether or not the reactions menu is open.
*/
_reactionsOpen: boolean;
}
/**
* An implementation of a button to raise or lower hand.
*/
class ReactionsMenuButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.reactionsMenu';
override icon = IconRaiseHand;
override label = 'toolbar.openReactionsMenu';
override toggledLabel = 'toolbar.closeReactionsMenu';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
override _handleClick() {
this.props.dispatch(openDialog(ReactionMenuDialog));
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
override _isToggled() {
return this.props._raisedHand || this.props._reactionsOpen;
}
}
/**
* 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 {
_raisedHand: hasRaisedHand(_localParticipant),
_reactionsOpen: isDialogOpen(state, ReactionMenuDialog),
visible
};
}
export default translate(connect(_mapStateToProps)(ReactionsMenuButton));

View File

@@ -0,0 +1,11 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
/**
* The styles of the native components of the feature {@code reactions}.
*/
export default {
raiseHandButton: {
marginVertical: BaseTheme.spacing[3],
width: 240
}
};