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,100 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { approveParticipant } from '../../../av-moderation/actions';
import { MEDIA_TYPE } from '../../../av-moderation/constants';
import { isForceMuted, isSupported } from '../../../av-moderation/functions';
import { translate } from '../../../base/i18n/functions';
import { IconMic, IconVideo } from '../../../base/icons/svg';
import { getParticipantById, isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
export interface IProps extends AbstractButtonProps {
/**
* Whether or not the participant is audio force muted.
*/
isAudioForceMuted: boolean;
/**
* Whether or not the participant is video force muted.
*/
isVideoForceMuted: boolean;
/**
* The ID of the participant object that this button is supposed to
* ask to unmute.
*/
participantID: string;
}
/**
* An abstract remote video menu button which asks the remote participant to unmute.
*/
class AskUnmuteButton extends AbstractButton<IProps> {
override accessibilityLabel = 'participantsPane.actions.askUnmute';
override icon = IconMic;
override label = 'participantsPane.actions.askUnmute';
/**
* Gets the current label.
*
* @returns {string}
*/
_getLabel() {
const { isAudioForceMuted, isVideoForceMuted } = this.props;
if (!isAudioForceMuted && isVideoForceMuted) {
return 'participantsPane.actions.allowVideo';
}
return this.label;
}
/**
* Gets the current icon.
*
* @returns {string}
*/
_getIcon() {
const { isAudioForceMuted, isVideoForceMuted } = this.props;
if (!isAudioForceMuted && isVideoForceMuted) {
return IconVideo;
}
return this.icon;
}
/**
* Handles clicking / pressing the button, and asks the participant to unmute.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, participantID } = this.props;
dispatch(approveParticipant(participantID));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState, ownProps: any) {
const { participantID } = ownProps;
const participant = getParticipantById(state, participantID);
return {
isAudioForceMuted: isForceMuted(participant, MEDIA_TYPE.AUDIO, state),
isVideoForceMuted: isForceMuted(participant, MEDIA_TYPE.VIDEO, state),
visible: isLocalParticipantModerator(state) && isSupported()(state)
};
}
export default translate(connect(mapStateToProps)(AskUnmuteButton));

View File

@@ -0,0 +1,36 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IconInfoCircle } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { showConnectionStatus } from '../../../participants-pane/actions.native';
export interface IProps extends AbstractButtonProps {
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string;
}
/**
* A remote video menu button which shows the connection statistics.
*/
class ConnectionStatusButton extends AbstractButton<IProps> {
override icon = IconInfoCircle;
override label = 'videothumbnail.connectionInfo';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, participantID } = this.props;
dispatch(showConnectionStatus(participantID));
}
}
export default translate(connect()(ConnectionStatusButton));

View File

@@ -0,0 +1,476 @@
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { withTheme } from 'react-native-paper';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { translate } from '../../../base/i18n/functions';
import { IconArrowDownLarge, IconArrowUpLarge } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import { getParticipantDisplayName } from '../../../base/participants/functions';
import BaseIndicator from '../../../base/react/components/native/BaseIndicator';
import {
getTrackByMediaTypeAndParticipant
} from '../../../base/tracks/functions.native';
import {
isTrackStreamingStatusInactive,
isTrackStreamingStatusInterrupted
} from '../../../connection-indicator/functions';
import statsEmitter from '../../../connection-indicator/statsEmitter';
import styles from './styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 25;
const CONNECTION_QUALITY = [
// Full (3 bars)
{
msg: 'connectionindicator.quality.good',
percent: 30 // INDICATOR_DISPLAY_THRESHOLD
},
// 2 bars.
{
msg: 'connectionindicator.quality.nonoptimal',
percent: 10
},
// 1 bar.
{
msg: 'connectionindicator.quality.poor',
percent: 0
}
];
interface IProps extends WithTranslation {
/**
* Whether this participant's connection is inactive.
*/
_isConnectionStatusInactive: boolean;
/**
* Whether this participant's connection is interrupted.
*/
_isConnectionStatusInterrupted: boolean;
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean;
/**
* Display name of the participant retrieved from Redux.
*/
_participantDisplayName: string;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string;
/**
* Theme used for styles.
*/
theme: any;
}
/**
* The type of the React {@code Component} state of {@link ConnectionStatusComponent}.
*/
type IState = {
codecString: string;
connectionString: string;
downloadString: string;
packetLostDownloadString: string;
packetLostUploadString: string;
resolutionString: string;
serverRegionString: string;
uploadString: string;
};
/**
* Class to implement a popup menu that show the connection statistics.
*/
class ConnectionStatusComponent extends PureComponent<IProps, IState> {
/**
* Constructor of the component.
*
* @param {P} props - The read-only properties with which the new
* instance is to be initialized.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onStatsUpdated = this._onStatsUpdated.bind(this);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
this.state = {
resolutionString: 'N/A',
downloadString: 'N/A',
uploadString: 'N/A',
packetLostDownloadString: 'N/A',
packetLostUploadString: 'N/A',
serverRegionString: 'N/A',
codecString: 'N/A',
connectionString: 'N/A'
};
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactNode}
*/
override render() {
const { t, theme } = this.props;
const { palette } = theme;
return (
<BottomSheet
onCancel = { this._onCancel }
renderHeader = { this._renderMenuHeader }>
<View style = { styles.statsWrapper as ViewStyle }>
<View style = { styles.statsInfoCell as ViewStyle }>
<Text style = { styles.statsTitleText as TextStyle }>
{ t('connectionindicator.status') }
</Text>
<Text style = { styles.statsInfoText as TextStyle }>
{ t(this.state.connectionString) }
</Text>
</View>
<View style = { styles.statsInfoCell as ViewStyle }>
<Text style = { styles.statsTitleText as TextStyle }>
{ t('connectionindicator.bitrate') }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText as TextStyle }>
{ this.state.downloadString }
</Text>
<BaseIndicator
icon = { IconArrowUpLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText as TextStyle }>
{ `${this.state.uploadString} Kbps` }
</Text>
</View>
<View style = { styles.statsInfoCell as ViewStyle }>
<Text style = { styles.statsTitleText as TextStyle }>
{ t('connectionindicator.packetloss') }
</Text>
<BaseIndicator
icon = { IconArrowDownLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText as TextStyle }>
{ this.state.packetLostDownloadString }
</Text>
<BaseIndicator
icon = { IconArrowUpLarge }
iconStyle = {{
color: palette.icon03
}} />
<Text style = { styles.statsInfoText as TextStyle }>
{ this.state.packetLostUploadString }
</Text>
</View>
<View style = { styles.statsInfoCell as ViewStyle }>
<Text style = { styles.statsTitleText as TextStyle }>
{ t('connectionindicator.resolution') }
</Text>
<Text style = { styles.statsInfoText as TextStyle }>
{ this.state.resolutionString }
</Text>
</View>
<View style = { styles.statsInfoCell as ViewStyle }>
<Text style = { styles.statsTitleText as TextStyle }>
{ t('connectionindicator.codecs') }
</Text>
<Text style = { styles.statsInfoText as TextStyle }>
{ this.state.codecString }
</Text>
</View>
</View>
</BottomSheet>
);
}
/**
* Starts listening for stat updates.
*
* @inheritdoc
* returns {void}
*/
override componentDidMount() {
statsEmitter.subscribeToClientStats(this.props.participantID, this._onStatsUpdated);
}
/**
* Updates which user's stats are being listened to.
*
* @inheritdoc
* returns {void}
*/
override componentDidUpdate(prevProps: IProps) {
if (prevProps.participantID !== this.props.participantID) {
statsEmitter.unsubscribeToClientStats(
prevProps.participantID, this._onStatsUpdated);
statsEmitter.subscribeToClientStats(
this.props.participantID, this._onStatsUpdated);
}
}
/**
* Callback invoked when new connection stats associated with the passed in
* user ID are available. Will update the component's display of current
* statistics.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {void}
*/
_onStatsUpdated(stats = {}) {
const newState = this._buildState(stats);
this.setState(newState);
}
/**
* Extracts statistics and builds the state object.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {State}
*/
_buildState(stats: any) {
const { download: downloadBitrate, upload: uploadBitrate } = this._extractBitrate(stats) ?? {};
const { download: downloadPacketLost, upload: uploadPacketLost } = this._extractPacketLost(stats) ?? {};
return {
resolutionString: this._extractResolutionString(stats) ?? this.state.resolutionString,
downloadString: downloadBitrate ?? this.state.downloadString,
uploadString: uploadBitrate ?? this.state.uploadString,
packetLostDownloadString: downloadPacketLost === undefined
? this.state.packetLostDownloadString : `${downloadPacketLost}%`,
packetLostUploadString: uploadPacketLost === undefined
? this.state.packetLostUploadString : `${uploadPacketLost}%`,
serverRegionString: this._extractServer(stats) ?? this.state.serverRegionString,
codecString: this._extractCodecs(stats) ?? this.state.codecString,
connectionString: this._extractConnection(stats)
};
}
/**
* Extracts the resolution and framerate.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractResolutionString(stats: any) {
const { framerate, resolution } = stats;
const resolutionString = Object.keys(resolution || {})
.map(ssrc => {
const { width, height } = resolution[ssrc];
return `${width}x${height}`;
})
.join(', ') || null;
const frameRateString = Object.keys(framerate || {})
.map(ssrc => framerate[ssrc])
.join(', ') || null;
return resolutionString && frameRateString ? `${resolutionString}@${frameRateString}fps` : undefined;
}
/**
* Extracts the download and upload bitrates.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {{ download, upload }}
*/
_extractBitrate(stats: any) {
return stats.bitrate;
}
/**
* Extracts the download and upload packet lost.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {{ download, upload }}
*/
_extractPacketLost(stats: any) {
return stats.packetLoss;
}
/**
* Extracts the server name.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractServer(stats: any) {
return stats.serverRegion;
}
/**
* Extracts the audio and video codecs names.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractCodecs(stats: any) {
const { codec } = stats;
let codecString;
if (codec) {
const audioCodecs = Object.values(codec)
.map((c: any) => c.audio)
.filter(Boolean);
const videoCodecs = Object.values(codec)
.map((c: any) => c.video)
.filter(Boolean);
if (audioCodecs.length || videoCodecs.length) {
// Use a Set to eliminate duplicates.
codecString = Array.from(new Set([ ...audioCodecs, ...videoCodecs ])).join(', ');
}
}
return codecString;
}
/**
* Extracts the connection percentage and sets connection quality.
*
* @param {Object} stats - Connection stats from the library.
* @private
* @returns {string}
*/
_extractConnection(stats: any) {
const { connectionQuality } = stats;
const {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted
} = this.props;
if (_isConnectionStatusInactive) {
return 'connectionindicator.quality.inactive';
} else if (_isConnectionStatusInterrupted) {
return 'connectionindicator.quality.lost';
} else if (typeof connectionQuality === 'undefined') {
return 'connectionindicator.quality.good';
}
const qualityConfig = this._getQualityConfig(connectionQuality);
return qualityConfig.msg;
}
/**
* Get the quality configuration from CONNECTION_QUALITY which has a percentage
* that matches or exceeds the passed in percentage. The implementation
* assumes CONNECTION_QUALITY is already sorted by highest to lowest
* percentage.
*
* @param {number} percent - The connection percentage, out of 100, to find
* the closest matching configuration for.
* @private
* @returns {Object}
*/
_getQualityConfig(percent: number): any {
return CONNECTION_QUALITY.find(x => percent >= x.percent) || {};
}
/**
* Callback to hide the {@code ConnectionStatusComponent}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
statsEmitter.unsubscribeToClientStats(this.props.participantID, this._onStatsUpdated);
this.props.dispatch(hideSheet());
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { participantID } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] as ViewStyle[] }>
<Avatar
participantId = { participantID }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel as TextStyle }>
{ this.props._participantDisplayName }
</Text>
</View>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const { participantID } = ownProps;
const tracks = state['features/base/tracks'];
const _videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantID);
const _isConnectionStatusInactive = isTrackStreamingStatusInactive(_videoTrack);
const _isConnectionStatusInterrupted = isTrackStreamingStatusInterrupted(_videoTrack);
return {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_participantDisplayName: getParticipantDisplayName(state, participantID)
};
}
export default translate(connect(_mapStateToProps)(withTheme(ConnectionStatusComponent)));

View File

@@ -0,0 +1,52 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import { IconUsers } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import DemoteToVisitorDialog from './DemoteToVisitorDialog';
interface IProps extends AbstractButtonProps {
/**
* The ID of the participant that this button is supposed to kick.
*/
participantID: string;
}
/**
* Implements a React {@link Component} which displays a button for demoting a participant to visitor.
*/
class DemoteToVisitorButton extends AbstractButton<IProps> {
override accessibilityLabel = 'videothumbnail.demote';
override icon = IconUsers;
override label = 'videothumbnail.demote';
/**
* Handles clicking / pressing the button, and demoting the participant.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, participantID } = this.props;
dispatch(openDialog(DemoteToVisitorDialog, { participantID }));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState) {
return {
visible: state['features/visitors'].supported
};
}
export default translate(connect(_mapStateToProps)(DemoteToVisitorButton));

View File

@@ -0,0 +1,38 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { DialogProps } from '../../../base/dialog/constants';
import { demoteRequest } from '../../../visitors/actions';
interface IProps extends DialogProps {
/**
* The ID of the remote participant to be demoted.
*/
participantID: string;
}
/**
* Dialog to confirm a remote participant demote to visitor action.
*
* @returns {JSX.Element}
*/
export default function DemoteToVisitorDialog({ participantID }: IProps): JSX.Element {
const dispatch = useDispatch();
const handleSubmit = useCallback(() => {
dispatch(demoteRequest(participantID));
return true; // close dialog
}, [ dispatch, participantID ]);
return (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.confirm'
descriptionKey = 'dialog.demoteParticipantDialog'
isConfirmDestructive = { true }
onSubmit = { handleSubmit }
title = 'dialog.demoteParticipantTitle' />
);
}

View File

@@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import AbstractGrantModeratorButton, {
_mapStateToProps
} from '../AbstractGrantModeratorButton';
export default translate(connect(_mapStateToProps)(AbstractGrantModeratorButton));

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { connect } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractGrantModeratorDialog, { abstractMapStateToProps }
from '../AbstractGrantModeratorDialog';
/**
* Dialog to confirm a remote participant kick action.
*/
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<ConfirmDialog
descriptionKey = {
`${this.props.t('dialog.grantModeratorDialog',
{ participantName: this.props.participantName })}`
}
onSubmit = { this._onSubmit } />
);
}
}
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

View File

@@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractKickButton from '../AbstractKickButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractKickButton));

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { connect } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractKickRemoteParticipantDialog
from '../AbstractKickRemoteParticipantDialog';
/**
* Dialog to confirm a remote participant kick action.
*/
class KickRemoteParticipantDialog extends AbstractKickRemoteParticipantDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.kickParticipantButton'
descriptionKey = 'dialog.kickParticipantDialog'
isConfirmDestructive = { true }
onSubmit = { this._onSubmit }
title = 'dialog.kickParticipantTitle' />
);
}
}
export default translate(connect()(KickRemoteParticipantDialog));

View File

@@ -0,0 +1,154 @@
import React, { PureComponent } from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { translate } from '../../../base/i18n/functions';
import {
getLocalParticipant,
getParticipantCount,
getParticipantDisplayName
} from '../../../base/participants/functions';
import { ILocalParticipant } from '../../../base/participants/types';
import ToggleSelfViewButton from '../../../toolbox/components/native/ToggleSelfViewButton';
import ConnectionStatusButton from './ConnectionStatusButton';
import DemoteToVisitorButton from './DemoteToVisitorButton';
import styles from './styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 24;
interface IProps {
/**
* The local participant.
*/
_participant?: ILocalParticipant;
/**
* Display name of the participant retrieved from Redux.
*/
_participantDisplayName: string;
/**
* Shows/hides the local switch to visitor button.
*/
_showDemote: boolean;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Translation function.
*/
t: Function;
}
/**
* Class to implement a popup menu that opens upon long pressing a thumbnail.
*/
class LocalVideoMenu extends PureComponent<IProps> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const { _participant, _showDemote } = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participantID: _participant?.id ?? '',
styles: bottomSheetStyles.buttons
};
const connectionStatusButtonProps = {
...buttonProps,
afterClick: undefined
};
return (
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { true }>
<ToggleSelfViewButton { ...buttonProps } />
{ _showDemote && <DemoteToVisitorButton { ...buttonProps } /> }
<ConnectionStatusButton { ...connectionStatusButtonProps } />
</BottomSheet>
);
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { _participant } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] as ViewStyle[] }>
<Avatar
participantId = { _participant?.id }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel as TextStyle }>
{ this.props._participantDisplayName }
</Text>
</View>
);
}
/**
* Callback to hide the {@code RemoteVideoMenu}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { disableSelfDemote } = state['features/base/config'];
const participant = getLocalParticipant(state);
return {
_participant: participant,
_participantDisplayName: getParticipantDisplayName(state, participant?.id ?? ''),
_showDemote: !disableSelfDemote && getParticipantCount(state) > 1
};
}
export default translate(connect(_mapStateToProps)(LocalVideoMenu));

View File

@@ -0,0 +1,71 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getCurrentConference } from '../../../base/conference/functions';
import { IJitsiConference } from '../../../base/conference/reducer';
import { translate } from '../../../base/i18n/functions';
import { IconRaiseHand } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
interface IProps extends AbstractButtonProps {
/**
* The current conference.
*/
_conference: IJitsiConference | undefined;
/**
* The ID of the participant object that this button is supposed to
* ask to lower the hand.
*/
participantId: String | undefined;
}
/**
* Implements a React {@link Component} which displays a button for lowering certain
* participant raised hands.
*
* @returns {JSX.Element}
*/
class LowerHandButton extends AbstractButton<IProps> {
override icon = IconRaiseHand;
override accessibilityLabel = 'participantsPane.actions.lowerHand';
override label = 'participantsPane.actions.lowerHand';
/**
* Handles clicking / pressing the button, and asks the participant to lower hand.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { participantId, _conference } = this.props;
_conference?.sendEndpointMessage(
participantId,
{
name: LOWER_HAND_MESSAGE
}
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState, ownProps: any) {
const { participantID } = ownProps;
const currentConference = getCurrentConference(state);
return {
_conference: currentConference,
participantId: participantID
};
}
export default translate(connect(mapStateToProps)(LowerHandButton));

View File

@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractMuteButton, { _mapStateToProps as _abstractMapStateToProps } from '../AbstractMuteButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
..._abstractMapStateToProps(state, ownProps),
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteButton));

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { ViewStyle } from 'react-native';
import Dialog from 'react-native-dialog';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractMuteEveryoneDialog, {
type IProps,
abstractMapStateToProps as _mapStateToProps } from '../AbstractMuteEveryoneDialog';
import styles from './styles';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @augments AbstractMuteEveryoneDialog
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<IProps> {
/**
* Renders the dialog switch.
*
* @returns {React$Component}
*/
_renderSwitch() {
return (
this.props.exclude.length === 0
&& <Dialog.Switch
label = { this.props.t('dialog.moderationAudioLabel') }
onValueChange = { this._onToggleModeration }
value = { !this.state.audioModerationEnabled } />
);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
return (
<ConfirmDialog
confirmLabel = 'dialog.muteParticipantButton'
descriptionKey = { this.state.content }
onSubmit = { this._onSubmit }
title = { this.props.title } >
{/* @ts-ignore */}
<Divider style = { styles.dividerDialog as ViewStyle } />
{ this._renderSwitch() }
</ConfirmDialog>
);
}
}
export default translate(connect(_mapStateToProps)(MuteEveryoneDialog));

View File

@@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractMuteEveryoneElseButton from '../AbstractMuteEveryoneElseButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteEveryoneElseButton));

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { ViewStyle } from 'react-native';
import Dialog from 'react-native-dialog';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractMuteEveryonesVideoDialog, {
type IProps,
abstractMapStateToProps as _mapStateToProps } from '../AbstractMuteEveryonesVideoDialog';
import styles from './styles';
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @augments AbstractMuteEveryonesVideoDialog
*/
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<IProps> {
/**
* Renders the dialog switch.
*
* @returns {React$Component}
*/
_renderSwitch() {
return (
this.props.exclude.length === 0
&& <Dialog.Switch
label = { this.props.t('dialog.moderationVideoLabel') }
onValueChange = { this._onToggleModeration }
value = { !this.state.moderationEnabled } />
);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
return (
<ConfirmDialog
confirmLabel = 'dialog.muteEveryonesVideoDialogOk'
descriptionKey = { this.state.content }
onSubmit = { this._onSubmit }
title = { this.props.title }>
{/* @ts-ignore */}
<Divider style = { styles.dividerDialog as ViewStyle } />
{ this._renderSwitch() }
</ConfirmDialog>
);
}
}
export default translate(connect(_mapStateToProps)(MuteEveryonesVideoDialog));

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { connect } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import AbstractMuteRemoteParticipantsVideoDialog, {
abstractMapStateToProps
} from '../AbstractMuteRemoteParticipantsVideoDialog';
/**
* Dialog to confirm a remote participant's video stop action.
*/
class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<ConfirmDialog
descriptionKey = { this.props.isVideoModerationOn
? 'dialog.muteParticipantsVideoDialogModerationOn'
: 'dialog.muteParticipantsVideoDialog'
}
onSubmit = { this._onSubmit } />
);
}
}
export default translate(connect(abstractMapStateToProps)(MuteRemoteParticipantsVideoDialog));

View File

@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractMuteVideoButton, { _mapStateToProps as _abstractMapStateToProps } from '../AbstractMuteVideoButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {Props}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
..._abstractMapStateToProps(state, ownProps),
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteVideoButton));

View File

@@ -0,0 +1,59 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconEnlarge } from '../../../base/icons/svg';
import { pinParticipant } from '../../../base/participants/actions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { shouldDisplayTileView } from '../../../video-layout/functions';
export interface IProps extends AbstractButtonProps {
/**
* True if tile view is currently enabled.
*/
_tileViewEnabled?: boolean;
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string;
}
/**
* A remote video menu button which pins a participant and exist the tile view.
*/
class PinButton extends AbstractButton<IProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.show';
override icon = IconEnlarge;
override label = 'videothumbnail.show';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
// Pin participant, it will automatically exit the tile view
dispatch(pinParticipant(this.props.participantID));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { isOpen } = state['features/participants-pane'];
return {
visible: !isOpen && shouldDisplayTileView(state)
};
}
export default translate(connect(_mapStateToProps)(PinButton));

View File

@@ -0,0 +1,287 @@
/* eslint-disable lines-around-comment */
import React, { PureComponent } from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { KICK_OUT_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import {
getParticipantById,
getParticipantDisplayName,
hasRaisedHand,
isLocalParticipantModerator,
isPrivateChatEnabled
} from '../../../base/participants/functions';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { IRoom } from '../../../breakout-rooms/types';
import PrivateMessageButton from '../../../chat/components/native/PrivateMessageButton';
import AskUnmuteButton from './AskUnmuteButton';
import ConnectionStatusButton from './ConnectionStatusButton';
import DemoteToVisitorButton from './DemoteToVisitorButton';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import LowerHandButton from './LowerHandButton';
import MuteButton from './MuteButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import MuteVideoButton from './MuteVideoButton';
import PinButton from './PinButton';
import SendToBreakoutRoom from './SendToBreakoutRoom';
import VolumeSlider from './VolumeSlider';
import styles from './styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 24;
interface IProps {
/**
* The id of the current room.
*/
_currentRoomId: string;
/**
* Whether or not to display the grant moderator button.
*/
_disableGrantModerator: boolean;
/**
* Whether or not to display the kick button.
*/
_disableKick: boolean;
/**
* Whether or not to display the remote mute buttons.
*/
_disableRemoteMute: boolean;
/**
* Whether or not to display the send private message button.
*/
_enablePrivateChat: boolean;
/**
* Whether or not the current room is a breakout room.
*/
_isBreakoutRoom: boolean;
/**
* Whether the participant is present in the room or not.
*/
_isParticipantAvailable?: boolean;
/**
* Whether or not the targeted participant joined without audio.
*/
_isParticipantSilent: boolean;
/**
* Whether the local participant is moderator or not.
*/
_moderator: boolean;
/**
* Display name of the participant retrieved from Redux.
*/
_participantDisplayName: string;
/**
* Whether the targeted participant raised hand or not.
*/
_raisedHand: boolean;
/**
* Array containing the breakout rooms.
*/
_rooms: Array<IRoom>;
/**
* Whether to display the demote button.
*/
_showDemote: boolean;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The ID of the participant for which this menu opened for.
*/
participantId: string;
/**
* Translation function.
*/
t: Function;
}
/**
* Class to implement a popup menu that opens upon long pressing a thumbnail.
*/
class RemoteVideoMenu extends PureComponent<IProps> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const {
_disableKick,
_disableRemoteMute,
_disableGrantModerator,
_enablePrivateChat,
_isBreakoutRoom,
_isParticipantAvailable,
_isParticipantSilent,
_moderator,
_raisedHand,
_rooms,
_showDemote,
_currentRoomId,
participantId,
t
} = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participantID: participantId,
styles: bottomSheetStyles.buttons
};
const connectionStatusButtonProps = {
...buttonProps,
afterClick: undefined
};
return (
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { _isParticipantAvailable }>
{!_isParticipantSilent && <AskUnmuteButton { ...buttonProps } />}
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
<MuteEveryoneElseButton { ...buttonProps } />
{ _moderator && _raisedHand && <LowerHandButton { ...buttonProps } /> }
{ !_disableRemoteMute && !_isParticipantSilent && <MuteVideoButton { ...buttonProps } /> }
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{ !_disableKick && <KickButton { ...buttonProps } /> }
{ !_disableGrantModerator && !_isBreakoutRoom && <GrantModeratorButton { ...buttonProps } /> }
<PinButton { ...buttonProps } />
{ _showDemote && <DemoteToVisitorButton { ...buttonProps } /> }
{ _enablePrivateChat && <PrivateMessageButton { ...buttonProps } /> }
<ConnectionStatusButton { ...connectionStatusButtonProps } />
{_moderator && _rooms.length > 1 && <>
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<View style = { styles.contextMenuItem as ViewStyle }>
<Text style = { styles.contextMenuItemText as TextStyle }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}
</Text>
</View>
{_rooms.map(room => _currentRoomId !== room.id && (<SendToBreakoutRoom
key = { room.id }
room = { room }
{ ...buttonProps } />))}
</>}
<VolumeSlider participantID = { participantId } />
</BottomSheet>
);
}
/**
* Callback to hide the {@code RemoteVideoMenu}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { participantId } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] as ViewStyle[] }>
<Avatar
participantId = { participantId }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel as TextStyle }>
{ this.props._participantDisplayName }
</Text>
</View>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
const { participantId } = ownProps;
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
const participant = getParticipantById(state, participantId);
const { disableKick } = remoteVideoMenu;
const _rooms = Object.values(getBreakoutRooms(state));
const _currentRoomId = getCurrentRoomId(state);
const shouldDisableKick = disableKick || !kickOutEnabled;
const moderator = isLocalParticipantModerator(state);
const _iAmVisitor = state['features/visitors'].iAmVisitor;
const _isBreakoutRoom = isInBreakoutRoom(state);
const raisedHand = hasRaisedHand(participant);
return {
_currentRoomId,
_disableKick: Boolean(shouldDisableKick),
_disableRemoteMute: Boolean(disableRemoteMute),
_enablePrivateChat: isPrivateChatEnabled(participant, state),
_isBreakoutRoom,
_isParticipantAvailable: Boolean(participant),
_isParticipantSilent: Boolean(participant?.isSilent),
_moderator: moderator,
_participantDisplayName: getParticipantDisplayName(state, participantId),
_raisedHand: raisedHand,
_rooms,
_showDemote: moderator
};
}
export default translate(connect(_mapStateToProps)(RemoteVideoMenu));

View File

@@ -0,0 +1,71 @@
import { connect } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconRingGroup } from '../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
import { IRoom } from '../../../breakout-rooms/types';
export interface IProps extends AbstractButtonProps {
/**
* ID of the participant to send to breakout room.
*/
participantID: string;
/**
* Room to send participant to.
*/
room: IRoom;
}
/**
* An abstract remote video menu button which sends the remote participant to a breakout room.
*/
class SendToBreakoutRoom extends AbstractButton<IProps> {
override accessibilityLabel = 'breakoutRooms.actions.sendToBreakoutRoom';
override icon = IconRingGroup;
/**
* Gets the current label.
*
* @returns {string}
*/
_getLabel() {
const { t, room } = this.props;
return room.name || t('breakoutRooms.mainRoom');
}
/**
* Handles clicking / pressing the button, and asks the participant to unmute.
*
* @private
* @returns {void}
*/
override _handleClick() {
const { dispatch, participantID, room } = this.props;
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
dispatch(sendParticipantToRoom(participantID, room.id));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(mapStateToProps)(SendToBreakoutRoom));

View File

@@ -0,0 +1,149 @@
import React, { PureComponent } from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { hideSheet } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import {
getParticipantById,
getParticipantDisplayName
} from '../../../base/participants/functions';
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
import styles from './styles';
/**
* Size of the rendered avatar in the menu.
*/
const AVATAR_SIZE = 24;
interface IProps {
/**
* True if the menu is currently open, false otherwise.
*/
_isOpen: boolean;
/**
* Whether the participant is present in the room or not.
*/
_isParticipantAvailable?: boolean;
/**
* Display name of the participant retrieved from Redux.
*/
_participantDisplayName: string;
/**
* The Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* The ID of the participant for which this menu opened for.
*/
participantId: string;
}
/**
* Class to implement a popup menu that opens upon long pressing a fake participant thumbnail.
*/
class SharedVideoMenu extends PureComponent<IProps> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onCancel = this._onCancel.bind(this);
this._renderMenuHeader = this._renderMenuHeader.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const {
_isParticipantAvailable,
participantId
} = this.props;
const buttonProps = {
afterClick: this._onCancel,
showLabel: true,
participantID: participantId,
styles: bottomSheetStyles.buttons
};
return (
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { _isParticipantAvailable }>
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<SharedVideoButton { ...buttonProps } />
</BottomSheet>
);
}
/**
* Callback to hide the {@code SharedVideoMenu}.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(hideSheet());
}
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { participantId } = this.props;
return (
<View
style = { [
bottomSheetStyles.sheet,
styles.participantNameContainer ] as ViewStyle[] }>
<Avatar
participantId = { participantId }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel as TextStyle }>
{ this.props._participantDisplayName }
</Text>
</View>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @param {Object} ownProps - Properties of component.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { participantId } = ownProps;
const isParticipantAvailable = getParticipantById(state, participantId);
return {
_isParticipantAvailable: Boolean(isParticipantAvailable),
_participantDisplayName: getParticipantDisplayName(state, participantId)
};
}
export default connect(_mapStateToProps)(SharedVideoMenu);

View File

@@ -0,0 +1,175 @@
/* eslint-disable lines-around-comment */
import Slider from '@react-native-community/slider';
import { throttle } from 'lodash-es';
import React, { PureComponent } from 'react';
import { View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconVolumeUp } from '../../../base/icons/svg';
import { MEDIA_TYPE } from '../../../base/media/constants';
import {
getTrackByMediaTypeAndParticipant,
getTrackState
} from '../../../base/tracks/functions.native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import { setVolume } from '../../../participants-pane/actions.native';
import { NATIVE_VOLUME_SLIDER_SCALE } from '../../constants';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link VolumeSlider}.
*/
interface IProps {
/**
* Whether the participant enters the conference silent.
*/
_startSilent?: boolean;
/**
* Remote audio track.
*/
_track?: any;
/**
* The volume level for the participant.
*/
_volume?: number;
/**
* The redux dispatch function.
*/
dispatch?: Function;
/**
* The ID of the participant.
*/
participantID?: string;
}
/**
* The type of the React {@code Component} state of {@link VolumeSlider}.
*/
interface IState {
/**
* The volume of the participant's audio element. The value will
* be represented by a slider.
*/
volumeLevel: number;
}
/**
* Component that renders the volume slider.
*
* @returns {React$Element<any>}
*/
class VolumeSlider extends PureComponent<IProps, IState> {
_originalVolumeChange: Function;
/**
* Initializes a new {@code VolumeSlider} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
this.state = {
volumeLevel: props._volume || Math.ceil(NATIVE_VOLUME_SLIDER_SCALE / 2)
};
this._originalVolumeChange = this._onVolumeChange;
this._onVolumeChange = throttle(
volumeLevel => this._originalVolumeChange(volumeLevel), 500
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { _startSilent } = this.props;
const { volumeLevel } = this.state;
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
return (
<View style = { styles.volumeSliderContainer as ViewStyle } >
<Icon
size = { 24 }
src = { IconVolumeUp } />
<Slider
maximumTrackTintColor = { BaseTheme.palette.ui10 }
maximumValue = { NATIVE_VOLUME_SLIDER_SCALE }
minimumTrackTintColor = { BaseTheme.palette.action01 }
minimumValue = { 0 }
onValueChange = { onVolumeChange }
style = { styles.sliderContainer as ViewStyle }
thumbTintColor = { BaseTheme.palette.ui10 }
value = { volumeLevel } />
</View>
);
}
/**
* Sets the internal state of the volume level for the volume slider.
* Invokes the prop onVolumeChange to notify of volume changes.
*
* @param {number} volumeLevel - Selected volume on slider.
* @private
* @returns {void}
*/
_onVolumeChange(volumeLevel: any) {
const { _track, dispatch, participantID } = this.props;
const audioTrack = _track?.jitsiTrack.track;
let newVolumeLevel;
if (volumeLevel <= 10) {
newVolumeLevel = volumeLevel / 10;
} else {
newVolumeLevel = volumeLevel - 9;
}
audioTrack?._setVolume(newVolumeLevel);
// @ts-ignore
dispatch(setVolume(participantID, newVolumeLevel));
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code VolumeSlider} component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState, ownProps: IProps) {
const { participantID } = ownProps;
const { participantsVolume } = state['features/filmstrip'];
const { startSilent } = state['features/base/config'];
const tracks = getTrackState(state);
return {
_startSilent: Boolean(startSilent),
_track: getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID),
_volume: participantID && participantsVolume[participantID]
};
}
// @ts-ignore
export default connect(mapStateToProps)(VolumeSlider);

View File

@@ -0,0 +1,89 @@
import {
MD_FONT_SIZE,
MD_ITEM_HEIGHT,
MD_ITEM_MARGIN_PADDING
} from '../../../base/dialog/components/native/styles';
import { ColorPalette } from '../../../base/styles/components/styles/ColorPalette';
import { createStyleSheet } from '../../../base/styles/functions.native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export default createStyleSheet({
participantNameContainer: {
alignItems: 'center',
borderBottomColor: BaseTheme.palette.ui07,
borderBottomWidth: 0.4,
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
flexDirection: 'row',
height: MD_ITEM_HEIGHT,
paddingLeft: MD_ITEM_MARGIN_PADDING
},
participantNameLabel: {
color: ColorPalette.lightGrey,
flexShrink: 1,
fontSize: MD_FONT_SIZE,
marginLeft: MD_ITEM_MARGIN_PADDING,
opacity: 0.90
},
statsTitleText: {
color: BaseTheme.palette.text01,
fontSize: 16,
fontWeight: 'bold',
marginRight: 3
},
statsInfoText: {
color: BaseTheme.palette.text01,
fontSize: 16,
marginRight: 2,
marginLeft: 2
},
statsInfoCell: {
alignItems: 'center',
flexDirection: 'row',
height: 30,
justifyContent: 'flex-start'
},
statsWrapper: {
margin: BaseTheme.spacing[3]
},
volumeSliderContainer: {
alignItems: 'center',
flexDirection: 'row',
marginHorizontal: BaseTheme.spacing[3],
marginVertical: BaseTheme.spacing[2]
},
sliderContainer: {
marginLeft: BaseTheme.spacing[3],
minWidth: '80%'
},
divider: {
backgroundColor: BaseTheme.palette.ui07
},
dividerDialog: {
backgroundColor: BaseTheme.palette.ui07,
marginBottom: BaseTheme.spacing[3]
},
contextMenuItem: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
height: BaseTheme.spacing[7],
marginLeft: BaseTheme.spacing[3]
},
contextMenuItemText: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginLeft: BaseTheme.spacing[4]
}
});