This commit is contained in:
100
react/features/video-menu/components/native/AskUnmuteButton.ts
Normal file
100
react/features/video-menu/components/native/AskUnmuteButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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)));
|
||||
@@ -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));
|
||||
@@ -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' />
|
||||
);
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
20
react/features/video-menu/components/native/KickButton.ts
Normal file
20
react/features/video-menu/components/native/KickButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
154
react/features/video-menu/components/native/LocalVideoMenu.tsx
Normal file
154
react/features/video-menu/components/native/LocalVideoMenu.tsx
Normal 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));
|
||||
@@ -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));
|
||||
22
react/features/video-menu/components/native/MuteButton.ts
Normal file
22
react/features/video-menu/components/native/MuteButton.ts
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
59
react/features/video-menu/components/native/PinButton.ts
Normal file
59
react/features/video-menu/components/native/PinButton.ts
Normal 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));
|
||||
287
react/features/video-menu/components/native/RemoteVideoMenu.tsx
Normal file
287
react/features/video-menu/components/native/RemoteVideoMenu.tsx
Normal 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));
|
||||
@@ -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));
|
||||
149
react/features/video-menu/components/native/SharedVideoMenu.tsx
Normal file
149
react/features/video-menu/components/native/SharedVideoMenu.tsx
Normal 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);
|
||||
175
react/features/video-menu/components/native/VolumeSlider.tsx
Normal file
175
react/features/video-menu/components/native/VolumeSlider.tsx
Normal 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);
|
||||
89
react/features/video-menu/components/native/styles.ts
Normal file
89
react/features/video-menu/components/native/styles.ts
Normal 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]
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user