This commit is contained in:
105
react/features/video-menu/actions.any.ts
Normal file
105
react/features/video-menu/actions.any.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
AUDIO_MUTE,
|
||||
DESKTOP_MUTE,
|
||||
VIDEO_MUTE,
|
||||
createRemoteMuteConfirmedEvent,
|
||||
createToolbarEvent
|
||||
} from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import {
|
||||
rejectParticipantAudio,
|
||||
rejectParticipantDesktop,
|
||||
rejectParticipantVideo
|
||||
} from '../av-moderation/actions';
|
||||
import { setAudioMuted, setScreenshareMuted, setVideoMuted } from '../base/media/actions';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
MediaType,
|
||||
SCREENSHARE_MUTISM_AUTHORITY,
|
||||
VIDEO_MUTISM_AUTHORITY
|
||||
} from '../base/media/constants';
|
||||
import { muteRemoteParticipant } from '../base/participants/actions';
|
||||
import { getRemoteParticipants } from '../base/participants/functions';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Mutes the local participant.
|
||||
*
|
||||
* @param {boolean} enable - Whether to mute or unmute.
|
||||
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function muteLocal(enable: boolean, mediaType: MediaType) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
switch (mediaType) {
|
||||
case MEDIA_TYPE.AUDIO: {
|
||||
sendAnalytics(createToolbarEvent(AUDIO_MUTE, { enable }));
|
||||
dispatch(setAudioMuted(enable, /* ensureTrack */ true));
|
||||
break;
|
||||
}
|
||||
case MEDIA_TYPE.SCREENSHARE: {
|
||||
sendAnalytics(createToolbarEvent(DESKTOP_MUTE, { enable }));
|
||||
dispatch(setScreenshareMuted(enable, SCREENSHARE_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
|
||||
break;
|
||||
}
|
||||
case MEDIA_TYPE.VIDEO: {
|
||||
sendAnalytics(createToolbarEvent(VIDEO_MUTE, { enable }));
|
||||
dispatch(setVideoMuted(enable, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
logger.error(`Unsupported media type: ${mediaType}`);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the remote participant with the given ID.
|
||||
*
|
||||
* @param {string} participantId - ID of the participant to mute.
|
||||
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function muteRemote(participantId: string, mediaType: MediaType) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
sendAnalytics(createRemoteMuteConfirmedEvent(participantId, mediaType));
|
||||
|
||||
// TODO(saghul): reconcile these 2 types.
|
||||
const muteMediaType = mediaType === MEDIA_TYPE.SCREENSHARE ? 'desktop' : mediaType;
|
||||
|
||||
dispatch(muteRemoteParticipant(participantId, muteMediaType));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes all participants.
|
||||
*
|
||||
* @param {Array<string>} exclude - Array of participant IDs to not mute.
|
||||
* @param {MEDIA_TYPE} mediaType - The media type to mute.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function muteAllParticipants(exclude: Array<string>, mediaType: MediaType) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
getRemoteParticipants(state).forEach((p, id) => {
|
||||
if (exclude.includes(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(muteRemote(id, mediaType));
|
||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||
dispatch(rejectParticipantAudio(id));
|
||||
} else if (mediaType === MEDIA_TYPE.VIDEO) {
|
||||
dispatch(rejectParticipantVideo(id));
|
||||
} else if (mediaType === MEDIA_TYPE.SCREENSHARE) {
|
||||
dispatch(rejectParticipantDesktop(id));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
1
react/features/video-menu/actions.native.ts
Normal file
1
react/features/video-menu/actions.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './actions.any';
|
||||
17
react/features/video-menu/actions.web.ts
Normal file
17
react/features/video-menu/actions.web.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SHOW_CONNECTION_INFO } from '../base/connection/actionTypes';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Sets whether to render the connection status info into the Popover of the thumbnail or the context menu buttons.
|
||||
*
|
||||
* @param {boolean} showConnectionInfo - Whether it should show the connection
|
||||
* info or the context menu buttons on thumbnail popover.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function renderConnectionStatus(showConnectionInfo: boolean) {
|
||||
return {
|
||||
type: SHOW_CONNECTION_INFO,
|
||||
showConnectionInfo
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IconModerator } from '../../base/icons/svg';
|
||||
import { PARTICIPANT_ROLE } from '../../base/participants/constants';
|
||||
import { getLocalParticipant, getParticipantById, isParticipantModerator } from '../../base/participants/functions';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
|
||||
import { GrantModeratorDialog } from './';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant for whom to grant moderator status.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which kicks the remote participant.
|
||||
*/
|
||||
export default class AbstractGrantModeratorButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.grantModerator';
|
||||
override icon = IconModerator;
|
||||
override label = 'videothumbnail.grantModerator';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and kicks the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(openDialog(GrantModeratorDialog, { participantID }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* visible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const targetParticipant = getParticipantById(state, participantID);
|
||||
|
||||
return {
|
||||
visible: Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR)
|
||||
&& !isParticipantModerator(targetParticipant)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { grantModerator } from '../../base/participants/actions';
|
||||
import { getParticipantById } from '../../base/participants/functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be granted moderator rights.
|
||||
*/
|
||||
participantID: string;
|
||||
|
||||
/**
|
||||
* The name of the remote participant to be granted moderator rights.
|
||||
*/
|
||||
participantName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract dialog to confirm granting moderator to a participant.
|
||||
*/
|
||||
export default class AbstractGrantModeratorDialog
|
||||
extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractGrantModeratorDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the confirm button.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'grant.moderator.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(grantModerator(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'s props.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
|
||||
return {
|
||||
participantName: getParticipantById(state, ownProps.participantID)?.name
|
||||
};
|
||||
}
|
||||
34
react/features/video-menu/components/AbstractKickButton.ts
Normal file
34
react/features/video-menu/components/AbstractKickButton.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IconUserDeleted } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
|
||||
import { KickRemoteParticipantDialog } from './';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant that this button is supposed to kick.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which kicks the remote participant.
|
||||
*/
|
||||
export default class AbstractKickButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.kick';
|
||||
override icon = IconUserDeleted;
|
||||
override label = 'videothumbnail.kick';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and kicks the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(openDialog(KickRemoteParticipantDialog, { participantID }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IStore } from '../../app/types';
|
||||
import { kickParticipant } from '../../base/participants/actions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be kicked.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract dialog to confirm a remote participant kick action.
|
||||
*/
|
||||
export default class AbstractKickRemoteParticipantDialog
|
||||
extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractKickRemoteParticipantDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the confirm button.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'kick.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(kickParticipant(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
90
react/features/video-menu/components/AbstractMuteButton.ts
Normal file
90
react/features/video-menu/components/AbstractMuteButton.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { rejectParticipantAudio } from '../../av-moderation/actions';
|
||||
import { IconMicSlash } from '../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { isRemoteTrackMuted } from '../../base/tracks/functions.any';
|
||||
import { muteRemote } from '../actions.any';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Boolean to indicate if the audio track of the participant is muted or
|
||||
* not.
|
||||
*/
|
||||
_audioTrackMuted: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the participant object that this button is supposed to
|
||||
* mute/unmute.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which mutes the remote participant.
|
||||
*/
|
||||
export default class AbstractMuteButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.remoteMute';
|
||||
override icon = IconMicSlash;
|
||||
override label = 'videothumbnail.domute';
|
||||
override toggledLabel = 'videothumbnail.muted';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and mutes the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'mute',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
|
||||
dispatch(rejectParticipantAudio(participantID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the item disabled if the participant is muted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _isDisabled() {
|
||||
return this.props._audioTrackMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the item toggled if the participant is muted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _isToggled() {
|
||||
return this.props._audioTrackMuted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _audioTrackMuted: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_audioTrackMuted: isRemoteTrackMuted(
|
||||
tracks, MEDIA_TYPE.AUDIO, ownProps.participantID)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { requestDisableAudioModeration, requestEnableAudioModeration } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import { getLocalParticipant, getParticipantDisplayName, isEveryoneModerator } from '../../base/participants/functions';
|
||||
import { muteAllParticipants } from '../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractMuteEveryoneDialog}.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
content?: string;
|
||||
dispatch: IStore['dispatch'];
|
||||
exclude: Array<string>;
|
||||
isAudioModerationEnabled?: boolean;
|
||||
isEveryoneModerator: boolean;
|
||||
isModerationSupported?: boolean;
|
||||
participantID: string;
|
||||
showAdvancedModerationToggle: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
audioModerationEnabled?: boolean;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* An abstract Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before muting all remote participants.
|
||||
*
|
||||
*/
|
||||
export default class AbstractMuteEveryoneDialog<P extends IProps> extends Component<P, IState> {
|
||||
static defaultProps = {
|
||||
exclude: [],
|
||||
muteLocal: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractMuteEveryoneDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
audioModerationEnabled: props.isAudioModerationEnabled,
|
||||
content: props.content || props.t(props.isAudioModerationEnabled
|
||||
? 'dialog.muteEveryoneDialogModerationOn' : 'dialog.muteEveryoneDialog'
|
||||
)
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onToggleModeration = this._onToggleModeration.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles advanced moderation switch.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleModeration() {
|
||||
this.setState(state => {
|
||||
return {
|
||||
audioModerationEnabled: !state.audioModerationEnabled,
|
||||
content: this.props.t(state.audioModerationEnabled
|
||||
? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the value of this dialog is submitted.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onSubmit() {
|
||||
const {
|
||||
dispatch,
|
||||
exclude
|
||||
} = this.props;
|
||||
|
||||
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
|
||||
if (this.state.audioModerationEnabled) {
|
||||
dispatch(requestEnableAudioModeration());
|
||||
} else if (this.state.audioModerationEnabled !== undefined) {
|
||||
dispatch(requestDisableAudioModeration());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'s props.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const { exclude = [], t } = ownProps;
|
||||
|
||||
const whom = exclude
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
.map(id => id === getLocalParticipant(state)?.id
|
||||
? t('dialog.muteEveryoneSelf')
|
||||
: getParticipantDisplayName(state, id))
|
||||
.join(', ');
|
||||
|
||||
return whom.length ? {
|
||||
content: t('dialog.muteEveryoneElseDialog'),
|
||||
title: t('dialog.muteEveryoneElseTitle', { whom }),
|
||||
isEveryoneModerator: isEveryoneModerator(state)
|
||||
} : {
|
||||
title: t('dialog.muteEveryoneTitle'),
|
||||
isAudioModerationEnabled: isEnabledFromState(AVM_MEDIA_TYPE.AUDIO, state),
|
||||
isModerationSupported: isSupported()(state),
|
||||
isEveryoneModerator: isEveryoneModerator(state)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IconMicSlash } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
|
||||
import { MuteEveryoneDialog } from './';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant object that this button is supposed to keep unmuted.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which mutes all the other participants.
|
||||
*/
|
||||
export default class AbstractMuteEveryoneElseButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElse';
|
||||
override icon = IconMicSlash;
|
||||
override label = 'videothumbnail.domuteOthers';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens a confirmation dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
|
||||
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IconVideoOff } from '../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
|
||||
import { MuteEveryonesVideoDialog } from './';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant object that this button is supposed to keep unmuted.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which disables the camera of all the other participants.
|
||||
*/
|
||||
export default class AbstractMuteEveryoneElsesVideoButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElsesVideoStream';
|
||||
override icon = IconVideoOff;
|
||||
override label = 'videothumbnail.domuteVideoOfOthers';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens a confirmation dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { requestDisableDesktopModeration, requestEnableDesktopModeration } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants/functions';
|
||||
import { muteAllParticipants } from '../actions';
|
||||
|
||||
import AbstractMuteRemoteParticipantsDesktopDialog, {
|
||||
type IProps as AbstractProps
|
||||
} from './AbstractMuteRemoteParticipantsDesktopDialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractMuteEveryonesDesktopDialog}.
|
||||
*/
|
||||
export interface IProps extends AbstractProps {
|
||||
content?: string;
|
||||
exclude: Array<string>;
|
||||
isModerationEnabled?: boolean;
|
||||
isModerationSupported?: boolean;
|
||||
showAdvancedModerationToggle: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
content: string;
|
||||
moderationEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* An abstract Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before disabling all remote participants cameras.
|
||||
*
|
||||
* @augments AbstractMuteRemoteParticipantsDesktopDialog
|
||||
*/
|
||||
export default class AbstractMuteEveryonesDesktopDialog<P extends IProps>
|
||||
extends AbstractMuteRemoteParticipantsDesktopDialog<P, IState> {
|
||||
static defaultProps = {
|
||||
exclude: [],
|
||||
muteLocal: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractMuteRemoteParticipantsDesktopDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
moderationEnabled: props.isModerationEnabled,
|
||||
content: props.content || props.t(props.isModerationEnabled
|
||||
? 'dialog.muteEveryonesDesktopDialogModerationOn' : 'dialog.muteEveryonesDesktopDialog'
|
||||
)
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onToggleModeration = this._onToggleModeration.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles advanced moderation switch.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleModeration() {
|
||||
this.setState(state => {
|
||||
return {
|
||||
moderationEnabled: !state.moderationEnabled,
|
||||
content: this.props.t(state.moderationEnabled
|
||||
? 'dialog.muteEveryonesDesktopDialog' : 'dialog.muteEveryonesDesktopDialogModerationOn'
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the value of this dialog is submitted.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _onSubmit() {
|
||||
const {
|
||||
dispatch,
|
||||
exclude
|
||||
} = this.props;
|
||||
|
||||
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.SCREENSHARE));
|
||||
if (this.state.moderationEnabled) {
|
||||
dispatch(requestEnableDesktopModeration());
|
||||
} else if (this.state.moderationEnabled !== undefined) {
|
||||
dispatch(requestDisableDesktopModeration());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesDesktopDialog}'s props.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const { exclude = [], t } = ownProps;
|
||||
const isModerationEnabled = isEnabledFromState(AVM_MEDIA_TYPE.DESKTOP, state);
|
||||
|
||||
const whom = exclude
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
.map(id => id === getLocalParticipant(state)?.id
|
||||
? t('dialog.muteEveryoneSelf')
|
||||
: getParticipantDisplayName(state, id))
|
||||
.join(', ');
|
||||
|
||||
return whom.length ? {
|
||||
content: t('dialog.muteEveryoneElsesDesktopDialog'),
|
||||
title: t('dialog.muteEveryoneElsesDesktopTitle', { whom })
|
||||
} : {
|
||||
title: t('dialog.muteEveryonesDesktopTitle'),
|
||||
isModerationEnabled,
|
||||
isModerationSupported: isSupported()(state)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { requestDisableVideoModeration, requestEnableVideoModeration } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { isEnabledFromState, isSupported } from '../../av-moderation/functions';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants/functions';
|
||||
import { muteAllParticipants } from '../actions';
|
||||
|
||||
import AbstractMuteRemoteParticipantsVideoDialog, {
|
||||
type IProps as AbstractProps
|
||||
} from './AbstractMuteRemoteParticipantsVideoDialog';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractMuteEveryonesVideoDialog}.
|
||||
*/
|
||||
export interface IProps extends AbstractProps {
|
||||
content?: string;
|
||||
exclude: Array<string>;
|
||||
isModerationSupported?: boolean;
|
||||
isVideoModerationEnabled?: boolean;
|
||||
showAdvancedModerationToggle: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
content: string;
|
||||
moderationEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* An abstract Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before disabling all remote participants cameras.
|
||||
*
|
||||
* @augments AbstractMuteRemoteParticipantsVideoDialog
|
||||
*/
|
||||
export default class AbstractMuteEveryonesVideoDialog<P extends IProps>
|
||||
extends AbstractMuteRemoteParticipantsVideoDialog<P, IState> {
|
||||
static defaultProps = {
|
||||
exclude: [],
|
||||
muteLocal: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
moderationEnabled: props.isVideoModerationEnabled,
|
||||
content: props.content || props.t(props.isVideoModerationEnabled
|
||||
? 'dialog.muteEveryonesVideoDialogModerationOn' : 'dialog.muteEveryonesVideoDialog'
|
||||
)
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onToggleModeration = this._onToggleModeration.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles advanced moderation switch.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleModeration() {
|
||||
this.setState(state => {
|
||||
return {
|
||||
moderationEnabled: !state.moderationEnabled,
|
||||
content: this.props.t(state.moderationEnabled
|
||||
? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the value of this dialog is submitted.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _onSubmit() {
|
||||
const {
|
||||
dispatch,
|
||||
exclude
|
||||
} = this.props;
|
||||
|
||||
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
|
||||
if (this.state.moderationEnabled) {
|
||||
dispatch(requestEnableVideoModeration());
|
||||
} else if (this.state.moderationEnabled !== undefined) {
|
||||
dispatch(requestDisableVideoModeration());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryonesVideoDialog}'s props.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState, ownProps: IProps) {
|
||||
const { exclude = [], t } = ownProps;
|
||||
const isVideoModerationEnabled = isEnabledFromState(AVM_MEDIA_TYPE.VIDEO, state);
|
||||
|
||||
const whom = exclude
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
.map(id => id === getLocalParticipant(state)?.id
|
||||
? t('dialog.muteEveryoneSelf')
|
||||
: getParticipantDisplayName(state, id))
|
||||
.join(', ');
|
||||
|
||||
return whom.length ? {
|
||||
content: t('dialog.muteEveryoneElsesVideoDialog'),
|
||||
title: t('dialog.muteEveryoneElsesVideoTitle', { whom })
|
||||
} : {
|
||||
title: t('dialog.muteEveryonesVideoTitle'),
|
||||
isVideoModerationEnabled,
|
||||
isModerationSupported: isSupported()(state)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { rejectParticipantDesktop } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { isEnabledFromState } from '../../av-moderation/functions';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import { muteRemote } from '../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractMuteRemoteParticipantsDesktopDialog}.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not desktop moderation is on.
|
||||
*/
|
||||
isModerationOn: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be muted.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract dialog to confirm a remote participant desktop mute action.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
export default class AbstractMuteRemoteParticipantsDesktopDialog<P extends IProps = IProps, State=any>
|
||||
extends Component<P, State> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractMuteRemoteParticipantsDesktopDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submit button action.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.SCREENSHARE));
|
||||
dispatch(rejectParticipantDesktop(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated
|
||||
* {@code AbstractDialogContainer}'s props.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
isModerationOn: isEnabledFromState(AVM_MEDIA_TYPE.DESKTOP, state)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { rejectParticipantVideo } from '../../av-moderation/actions';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../av-moderation/constants';
|
||||
import { isEnabledFromState } from '../../av-moderation/functions';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import { muteRemote } from '../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link AbstractMuteRemoteParticipantsVideoDialog}.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not video moderation is on.
|
||||
*/
|
||||
isVideoModerationOn: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be muted.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract dialog to confirm a remote participant video ute action.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
export default class AbstractMuteRemoteParticipantsVideoDialog<P extends IProps = IProps, State=any>
|
||||
extends Component<P, State> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractMuteRemoteParticipantsVideoDialog} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the submit button action.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.VIDEO));
|
||||
dispatch(rejectParticipantVideo(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated
|
||||
* {@code AbstractDialogContainer}'s props.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function abstractMapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
isVideoModerationOn: isEnabledFromState(AVM_MEDIA_TYPE.VIDEO, state)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IconVideoOff } from '../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../base/media/constants';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
import { isRemoteTrackMuted } from '../../base/tracks/functions.any';
|
||||
|
||||
import { MuteRemoteParticipantsVideoDialog } from './';
|
||||
|
||||
export interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Boolean to indicate if the video track of the participant is muted or
|
||||
* not.
|
||||
*/
|
||||
_videoTrackMuted: boolean;
|
||||
|
||||
/**
|
||||
* The ID of the participant object that this button is supposed to
|
||||
* mute/unmute.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which mutes the remote participant.
|
||||
*/
|
||||
export default class AbstractMuteVideoButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.remoteVideoMute';
|
||||
override icon = IconVideoOff;
|
||||
override label = 'videothumbnail.domuteVideo';
|
||||
override toggledLabel = 'videothumbnail.videoMuted';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and mutes the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'video.mute.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the item disabled if the participant is muted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _isDisabled() {
|
||||
return this.props._videoTrackMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the item toggled if the participant is muted.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _isToggled() {
|
||||
return this.props._videoTrackMuted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _videoTrackMuted: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return {
|
||||
_videoTrackMuted: isRemoteTrackMuted(
|
||||
tracks, MEDIA_TYPE.VIDEO, ownProps.participantID)
|
||||
};
|
||||
}
|
||||
13
react/features/video-menu/components/index.native.ts
Normal file
13
react/features/video-menu/components/index.native.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
// @ts-ignore
|
||||
export { default as DemoteToVisitorDialog } from './native/DemoteToVisitorDialog';
|
||||
// @ts-ignore
|
||||
export { default as GrantModeratorDialog } from './native/GrantModeratorDialog';
|
||||
// @ts-ignore
|
||||
export { default as KickRemoteParticipantDialog } from './native/KickRemoteParticipantDialog';
|
||||
// @ts-ignore
|
||||
export { default as MuteEveryoneDialog } from './native/MuteEveryoneDialog';
|
||||
// @ts-ignore
|
||||
export { default as MuteEveryonesVideoDialog } from './native/MuteEveryonesVideoDialog';
|
||||
// @ts-ignore
|
||||
export { default as MuteRemoteParticipantsVideoDialog } from './native/MuteRemoteParticipantsVideoDialog';
|
||||
6
react/features/video-menu/components/index.web.ts
Normal file
6
react/features/video-menu/components/index.web.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as DemoteToVisitorDialog } from './web/DemoteToVisitorDialog';
|
||||
export { default as GrantModeratorDialog } from './web/GrantModeratorDialog';
|
||||
export { default as KickRemoteParticipantDialog } from './web/KickRemoteParticipantDialog';
|
||||
export { default as MuteEveryoneDialog } from './web/MuteEveryoneDialog';
|
||||
export { default as MuteEveryonesVideoDialog } from './web/MuteEveryonesVideoDialog';
|
||||
export { default as MuteRemoteParticipantsVideoDialog } from './web/MuteRemoteParticipantsVideoDialog';
|
||||
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]
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { approveParticipantAudio, approveParticipantDesktop, approveParticipantVideo } from '../../../av-moderation/actions';
|
||||
import { IconMic, IconScreenshare, IconVideo } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE, MediaType } from '../../../base/media/constants';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps extends IButtonProps {
|
||||
buttonType: MediaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button that
|
||||
* allows the moderator to request from a participant to mute themselves.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const AskToUnmuteButton = ({
|
||||
buttonType,
|
||||
notifyMode,
|
||||
notifyClick,
|
||||
participantID
|
||||
}: IProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _onClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||
dispatch(approveParticipantAudio(participantID));
|
||||
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||
dispatch(approveParticipantVideo(participantID));
|
||||
} else if (buttonType === MEDIA_TYPE.SCREENSHARE) {
|
||||
dispatch(approveParticipantDesktop(participantID));
|
||||
}
|
||||
}, [ buttonType, dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
const text = useMemo(() => {
|
||||
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||
return t('participantsPane.actions.askUnmute');
|
||||
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||
return t('participantsPane.actions.allowVideo');
|
||||
} else if (buttonType === MEDIA_TYPE.SCREENSHARE) {
|
||||
return t('participantsPane.actions.allowDesktop');
|
||||
}
|
||||
|
||||
return '';
|
||||
}, [ buttonType ]);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (buttonType === MEDIA_TYPE.AUDIO) {
|
||||
return IconMic;
|
||||
} else if (buttonType === MEDIA_TYPE.VIDEO) {
|
||||
return IconVideo;
|
||||
} else if (buttonType === MEDIA_TYPE.SCREENSHARE) {
|
||||
return IconScreenshare;
|
||||
}
|
||||
}, [ buttonType ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { text }
|
||||
icon = { icon }
|
||||
onClick = { _onClick }
|
||||
testId = { `unmute-${buttonType}-${participantID}` }
|
||||
text = { text } />
|
||||
);
|
||||
};
|
||||
|
||||
export default AskToUnmuteButton;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { IconInfoCircle } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button that shows
|
||||
* the connection status for the given participant.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const ConnectionStatusButton = ({
|
||||
notifyClick,
|
||||
notifyMode
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(e => {
|
||||
e.stopPropagation();
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(renderConnectionStatus(true));
|
||||
}, [ dispatch, notifyClick, notifyMode ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.connectionInfo') }
|
||||
icon = { IconInfoCircle }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.connectionInfo') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionStatusButton;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
|
||||
const CustomOptionButton = (
|
||||
{ icon: iconSrc, onClick, text }:
|
||||
{
|
||||
icon: string;
|
||||
onClick: (e?: React.MouseEvent<Element, MouseEvent> | undefined) => void;
|
||||
text: string;
|
||||
}
|
||||
) => {
|
||||
|
||||
const icon = useCallback(props => (<img
|
||||
src = { iconSrc }
|
||||
{ ...props } />), [ iconSrc ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { text }
|
||||
icon = { icon }
|
||||
onClick = { onClick }
|
||||
text = { text } />
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomOptionButton;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import DemoteToVisitorDialog from './DemoteToVisitorDialog';
|
||||
|
||||
interface IProps extends IButtonProps {
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Whether the icon should be hidden or not.
|
||||
*/
|
||||
noIcon?: boolean;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for demoting a participant to visitor.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function DemoteToVisitorButton({
|
||||
className,
|
||||
noIcon = false,
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openDialog(DemoteToVisitorDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.demote') }
|
||||
className = { className || 'demotelink' } // can be used in tests
|
||||
icon = { noIcon ? null : IconUsers }
|
||||
id = { `demotelink_${participantID}` }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.demote') } />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { DialogProps } from '../../../base/dialog/constants';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
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 action.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function DemoteToVisitorDialog({ participantID }: IProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
dispatch(demoteRequest(participantID));
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.confirm' }}
|
||||
onSubmit = { handleSubmit }
|
||||
titleKey = 'dialog.demoteParticipantTitle'>
|
||||
<div>
|
||||
{ t('dialog.demoteParticipantDialog') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import TogglePinToStageButton from '../../../../features/video-menu/components/web/TogglePinToStageButton';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { IconPlay } from '../../../base/icons/svg';
|
||||
import { isWhiteboardParticipant } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { stopSharedVideo } from '../../../shared-video/actions';
|
||||
import { getParticipantMenuButtonsWithNotifyClick, showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { setWhiteboardOpen } from '../../../whiteboard/actions';
|
||||
import { WHITEBOARD_ID } from '../../../whiteboard/constants';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Class name for the context menu.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Closes a drawer if open.
|
||||
*/
|
||||
closeDrawer?: () => void;
|
||||
|
||||
/**
|
||||
* The participant for which the drawer is open.
|
||||
* It contains the displayName & participantID.
|
||||
*/
|
||||
drawerParticipant?: {
|
||||
displayName: string;
|
||||
participantID: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
localVideoOwner?: boolean;
|
||||
|
||||
/**
|
||||
* Target elements against which positioning calculations are made.
|
||||
*/
|
||||
offsetTarget?: HTMLElement;
|
||||
|
||||
/**
|
||||
* Callback for the mouse entering the component.
|
||||
*/
|
||||
onEnter?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback for the mouse leaving the component.
|
||||
*/
|
||||
onLeave?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback for making a selection in the menu.
|
||||
*/
|
||||
onSelect: (value?: boolean | React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Participant reference.
|
||||
*/
|
||||
participant: IParticipant;
|
||||
|
||||
/**
|
||||
* Whether or not the menu is displayed in the thumbnail remote video menu.
|
||||
*/
|
||||
thumbnailMenu?: boolean;
|
||||
}
|
||||
|
||||
const FakeParticipantContextMenu = ({
|
||||
className,
|
||||
closeDrawer,
|
||||
drawerParticipant,
|
||||
localVideoOwner,
|
||||
offsetTarget,
|
||||
onEnter,
|
||||
onLeave,
|
||||
onSelect,
|
||||
participant,
|
||||
thumbnailMenu
|
||||
}: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string, participantId?: string) => {
|
||||
const notifyMode = buttonsWithNotifyClick?.get(buttonKey);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
participantId,
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick ]);
|
||||
|
||||
|
||||
const clickHandler = useCallback(() => onSelect(true), [ onSelect ]);
|
||||
|
||||
const _onStopSharedVideo = useCallback(() => {
|
||||
clickHandler();
|
||||
dispatch(stopSharedVideo());
|
||||
}, [ stopSharedVideo ]);
|
||||
|
||||
const _onHideWhiteboard = useCallback(() => {
|
||||
clickHandler();
|
||||
dispatch(setWhiteboardOpen(false));
|
||||
}, [ setWhiteboardOpen ]);
|
||||
|
||||
const _getActions = useCallback(() => {
|
||||
if (isWhiteboardParticipant(participant)) {
|
||||
return [ {
|
||||
accessibilityLabel: t('toolbar.hideWhiteboard'),
|
||||
icon: IconPlay,
|
||||
onClick: _onHideWhiteboard,
|
||||
text: t('toolbar.hideWhiteboard')
|
||||
} ];
|
||||
}
|
||||
|
||||
if (localVideoOwner) {
|
||||
return [ {
|
||||
accessibilityLabel: t('toolbar.stopSharedVideo'),
|
||||
icon: IconPlay,
|
||||
onClick: _onStopSharedVideo,
|
||||
text: t('toolbar.stopSharedVideo')
|
||||
} ];
|
||||
}
|
||||
}, [ localVideoOwner, participant.fakeParticipant ]);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
className = { className }
|
||||
entity = { participant }
|
||||
hidden = { thumbnailMenu ? false : undefined }
|
||||
inDrawer = { thumbnailMenu && _overflowDrawer }
|
||||
isDrawerOpen = { Boolean(drawerParticipant) }
|
||||
offsetTarget = { offsetTarget }
|
||||
onClick = { onSelect }
|
||||
onDrawerClose = { thumbnailMenu ? onSelect : closeDrawer }
|
||||
onMouseEnter = { onEnter }
|
||||
onMouseLeave = { onLeave }>
|
||||
{!thumbnailMenu && _overflowDrawer && drawerParticipant && <ContextMenuItemGroup
|
||||
actions = { [ {
|
||||
accessibilityLabel: drawerParticipant.displayName,
|
||||
customIcon: <Avatar
|
||||
participantId = { drawerParticipant.participantID }
|
||||
size = { 20 } />,
|
||||
text: drawerParticipant.displayName
|
||||
} ] } />}
|
||||
|
||||
<ContextMenuItemGroup
|
||||
actions = { _getActions() }>
|
||||
{isWhiteboardParticipant(participant) && (
|
||||
<TogglePinToStageButton
|
||||
key = 'pinToStage'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.PIN_TO_STAGE, WHITEBOARD_ID) }
|
||||
notifyMode = { buttonsWithNotifyClick?.get(BUTTONS.PIN_TO_STAGE) }
|
||||
participantID = { WHITEBOARD_ID } />
|
||||
)}
|
||||
</ContextMenuItemGroup>
|
||||
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default FakeParticipantContextMenu;
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FlipLocalVideoButton}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The current local flip x status.
|
||||
*/
|
||||
_localFlipX: boolean;
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for flipping the local viedo.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class FlipLocalVideoButton extends PureComponent<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code FlipLocalVideoButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {null|ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
className,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.flip') }
|
||||
className = 'fliplink'
|
||||
id = 'flipLocalVideoButton'
|
||||
onClick = { this._onClick }
|
||||
text = { t('videothumbnail.flip') }
|
||||
textClassName = { className } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flips the local video.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _localFlipX, dispatch, notifyClick, notifyMode, onClick } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
onClick?.();
|
||||
dispatch(updateSettings({
|
||||
localFlipX: !_localFlipX
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code FlipLocalVideoButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
|
||||
return {
|
||||
_localFlipX: Boolean(localFlipX)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(FlipLocalVideoButton));
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconModerator } from '../../../base/icons/svg';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
import { getLocalParticipant, getParticipantById, isParticipantModerator } from '../../../base/participants/functions';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import GrantModeratorDialog from './GrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for granting
|
||||
* moderator to a participant.
|
||||
*
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
const GrantModeratorButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const targetParticipant = useSelector((state: IReduxState) => getParticipantById(state, participantID));
|
||||
const visible = useMemo(() => Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR)
|
||||
&& !isParticipantModerator(targetParticipant), [ isParticipantModerator, localParticipant, targetParticipant ]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openDialog(GrantModeratorDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.grantModerator') }
|
||||
className = 'grantmoderatorlink'
|
||||
icon = { IconModerator }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.grantModerator') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default GrantModeratorButton;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import AbstractGrantModeratorDialog, { abstractMapStateToProps } from '../AbstractGrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Dialog to confirm a grant moderator action.
|
||||
*/
|
||||
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.Yes' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.grantModeratorTitle'>
|
||||
<div>
|
||||
{ this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName }) }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link HideSelfViewVideoButton}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* Whether or not to hide the self view.
|
||||
*/
|
||||
disableSelfView: boolean;
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for hiding the local video.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class HideSelfViewVideoButton extends PureComponent<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code HideSelfViewVideoButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {null|ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
className,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.hideSelfView') }
|
||||
className = 'hideselflink'
|
||||
id = 'hideselfviewButton'
|
||||
onClick = { this._onClick }
|
||||
text = { t('videothumbnail.hideSelfView') }
|
||||
textClassName = { className } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the local video.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { disableSelfView, dispatch, notifyClick, notifyMode, onClick } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
onClick?.();
|
||||
dispatch(updateSettings({
|
||||
disableSelfView: !disableSelfView
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code HideSelfViewVideoButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
disableSelfView: Boolean(getHideSelfView(state))
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(HideSelfViewVideoButton));
|
||||
46
react/features/video-menu/components/web/KickButton.tsx
Normal file
46
react/features/video-menu/components/web/KickButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUserDeleted } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import KickRemoteParticipantDialog from './KickRemoteParticipantDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for kicking out
|
||||
* a participant from the conference.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const KickButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openDialog(KickRemoteParticipantDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.kick') }
|
||||
className = 'kicklink'
|
||||
icon = { IconUserDeleted }
|
||||
id = { `ejectlink_${participantID}` }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.kick') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default KickButton;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
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 (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.kickParticipantButton' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.kickParticipantTitle'>
|
||||
<div>
|
||||
{ this.props.t('dialog.kickParticipantDialog') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(KickRemoteParticipantDialog));
|
||||
@@ -0,0 +1,305 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { batch, connect, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconDotsHorizontal } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getParticipantCount } from '../../../base/participants/functions';
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions.web';
|
||||
import { getLocalVideoTrack } from '../../../base/tracks/functions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import ConnectionIndicatorContent from '../../../connection-indicator/components/web/ConnectionIndicatorContent';
|
||||
import { THUMBNAIL_TYPE } from '../../../filmstrip/constants';
|
||||
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
||||
import { getParticipantMenuButtonsWithNotifyClick } from '../../../toolbox/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import FlipLocalVideoButton from './FlipLocalVideoButton';
|
||||
import HideSelfViewVideoButton from './HideSelfViewVideoButton';
|
||||
import TogglePinToStageButton from './TogglePinToStageButton';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link LocalVideoMenuTriggerButton}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The id of the local participant.
|
||||
*/
|
||||
_localParticipantId: string;
|
||||
|
||||
/**
|
||||
* The position relative to the trigger the local video menu should display
|
||||
* from.
|
||||
*/
|
||||
_menuPosition: string;
|
||||
|
||||
/**
|
||||
* Whether to display the Popover as a drawer.
|
||||
*/
|
||||
_overflowDrawer: boolean;
|
||||
|
||||
/**
|
||||
* Whether to render the connection info pane.
|
||||
*/
|
||||
_showConnectionInfo: boolean;
|
||||
|
||||
/**
|
||||
* Shows/hides the local switch to visitor button.
|
||||
*/
|
||||
_showDemote: boolean;
|
||||
|
||||
/**
|
||||
* Whether to render the hide self view button.
|
||||
*/
|
||||
_showHideSelfViewButton: boolean;
|
||||
|
||||
/**
|
||||
* Shows/hides the local video flip button.
|
||||
*/
|
||||
_showLocalVideoFlipButton: boolean;
|
||||
|
||||
/**
|
||||
* Whether to render the pin to stage button.
|
||||
*/
|
||||
_showPinToStage: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the button should be visible.
|
||||
*/
|
||||
buttonVisible: boolean;
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Hides popover.
|
||||
*/
|
||||
hidePopover?: Function;
|
||||
|
||||
/**
|
||||
* Whether the popover is visible or not.
|
||||
*/
|
||||
popoverVisible?: boolean;
|
||||
|
||||
/**
|
||||
* Shows popover.
|
||||
*/
|
||||
showPopover?: Function;
|
||||
|
||||
/**
|
||||
* The type of the thumbnail.
|
||||
*/
|
||||
thumbnailType: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
triggerButton: {
|
||||
padding: '3px !important',
|
||||
borderRadius: '4px',
|
||||
|
||||
'& svg': {
|
||||
width: '18px',
|
||||
height: '18px'
|
||||
}
|
||||
},
|
||||
|
||||
contextMenu: {
|
||||
position: 'relative',
|
||||
marginTop: 0,
|
||||
right: 'auto',
|
||||
padding: '0',
|
||||
minWidth: '200px'
|
||||
},
|
||||
|
||||
flipText: {
|
||||
marginLeft: '36px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const LocalVideoMenuTriggerButton = ({
|
||||
_localParticipantId,
|
||||
_menuPosition,
|
||||
_overflowDrawer,
|
||||
_showConnectionInfo,
|
||||
_showDemote,
|
||||
_showHideSelfViewButton,
|
||||
_showLocalVideoFlipButton,
|
||||
_showPinToStage,
|
||||
buttonVisible,
|
||||
dispatch,
|
||||
hidePopover,
|
||||
showPopover,
|
||||
popoverVisible
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
const visitorsSupported = useSelector((state: IReduxState) => state['features/visitors'].supported);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string) => {
|
||||
const notifyMode = buttonsWithNotifyClick?.get(buttonKey);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
_localParticipantId,
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick ]);
|
||||
|
||||
const _onPopoverOpen = useCallback(() => {
|
||||
showPopover?.();
|
||||
dispatch(setParticipantContextMenuOpen(true));
|
||||
}, []);
|
||||
|
||||
const _onPopoverClose = useCallback(() => {
|
||||
hidePopover?.();
|
||||
batch(() => {
|
||||
dispatch(setParticipantContextMenuOpen(false));
|
||||
dispatch(renderConnectionStatus(false));
|
||||
});
|
||||
}, []);
|
||||
|
||||
const content = _showConnectionInfo
|
||||
? <ConnectionIndicatorContent participantId = { _localParticipantId } />
|
||||
: (
|
||||
<ContextMenu
|
||||
className = { classes.contextMenu }
|
||||
hidden = { false }
|
||||
inDrawer = { _overflowDrawer }>
|
||||
<ContextMenuItemGroup>
|
||||
{_showLocalVideoFlipButton
|
||||
&& <FlipLocalVideoButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.FLIP_LOCAL_VIDEO) }
|
||||
notifyMode = { buttonsWithNotifyClick?.get(BUTTONS.FLIP_LOCAL_VIDEO) }
|
||||
onClick = { hidePopover } />
|
||||
}
|
||||
{_showHideSelfViewButton
|
||||
&& <HideSelfViewVideoButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.HIDE_SELF_VIEW) }
|
||||
notifyMode = { buttonsWithNotifyClick?.get(BUTTONS.HIDE_SELF_VIEW) }
|
||||
onClick = { hidePopover } />
|
||||
}
|
||||
{
|
||||
_showPinToStage && <TogglePinToStageButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
noIcon = { true }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.PIN_TO_STAGE) }
|
||||
notifyMode = { buttonsWithNotifyClick?.get(BUTTONS.PIN_TO_STAGE) }
|
||||
onClick = { hidePopover }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
{
|
||||
_showDemote && visitorsSupported && <DemoteToVisitorButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
noIcon = { true }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.DEMOTE) }
|
||||
notifyMode = { buttonsWithNotifyClick?.get(BUTTONS.DEMOTE) }
|
||||
onClick = { hidePopover }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
{
|
||||
isMobileBrowser() && <ConnectionStatusButton
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.CONN_STATUS) }
|
||||
notifyMode = { buttonsWithNotifyClick?.get(BUTTONS.CONN_STATUS) }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
</ContextMenuItemGroup>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
return (
|
||||
isMobileBrowser() || _showLocalVideoFlipButton || _showHideSelfViewButton
|
||||
? <Popover
|
||||
content = { content }
|
||||
headingLabel = { t('dialog.localUserControls') }
|
||||
id = 'local-video-menu-trigger'
|
||||
onPopoverClose = { _onPopoverClose }
|
||||
onPopoverOpen = { _onPopoverOpen }
|
||||
position = { _menuPosition }
|
||||
visible = { Boolean(popoverVisible) }>
|
||||
{buttonVisible && !isMobileBrowser() && (
|
||||
<Button
|
||||
accessibilityLabel = { t('dialog.localUserControls') }
|
||||
className = { classes.triggerButton }
|
||||
icon = { IconDotsHorizontal }
|
||||
size = 'small' />
|
||||
)}
|
||||
</Popover>
|
||||
: null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code LocalVideoMenuTriggerButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { thumbnailType } = ownProps;
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { disableLocalVideoFlip, disableSelfDemote, disableSelfViewSettings } = state['features/base/config'];
|
||||
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
const { overflowDrawer } = state['features/toolbox'];
|
||||
const { showConnectionInfo } = state['features/base/connection'];
|
||||
const showHideSelfViewButton = !disableSelfViewSettings && !getHideSelfView(state);
|
||||
|
||||
let _menuPosition;
|
||||
|
||||
switch (thumbnailType) {
|
||||
case THUMBNAIL_TYPE.TILE:
|
||||
_menuPosition = 'left-start';
|
||||
break;
|
||||
case THUMBNAIL_TYPE.VERTICAL:
|
||||
_menuPosition = 'left-start';
|
||||
break;
|
||||
case THUMBNAIL_TYPE.HORIZONTAL:
|
||||
_menuPosition = 'top-start';
|
||||
break;
|
||||
default:
|
||||
_menuPosition = 'auto';
|
||||
}
|
||||
|
||||
return {
|
||||
_menuPosition,
|
||||
_showDemote: !disableSelfDemote && getParticipantCount(state) > 1,
|
||||
_showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
|
||||
_showHideSelfViewButton: showHideSelfViewButton,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_localParticipantId: localParticipant?.id ?? '',
|
||||
_showConnectionInfo: Boolean(showConnectionInfo),
|
||||
_showPinToStage: isStageFilmstripAvailable(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(LocalVideoMenuTriggerButton);
|
||||
56
react/features/video-menu/components/web/LowerHandButton.tsx
Normal file
56
react/features/video-menu/components/web/LowerHandButton.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import { raiseHand } from '../../../base/participants/actions';
|
||||
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant that's linked to the button.
|
||||
*/
|
||||
participantID?: String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for notifying certain
|
||||
* participant who raised hand to lower hand.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const LowerHandButton = ({
|
||||
participantID = ''
|
||||
}: IProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const currentConference = useSelector(getCurrentConference);
|
||||
const accessibilityText = participantID
|
||||
? t('participantsPane.actions.lowerHand')
|
||||
: t('participantsPane.actions.lowerAllHands');
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!participantID) {
|
||||
dispatch(raiseHand(false));
|
||||
}
|
||||
currentConference?.sendEndpointMessage(
|
||||
participantID,
|
||||
{
|
||||
name: LOWER_HAND_MESSAGE
|
||||
}
|
||||
);
|
||||
}, [ participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { accessibilityText }
|
||||
icon = { IconRaiseHand }
|
||||
onClick = { handleClick }
|
||||
text = { accessibilityText } />
|
||||
);
|
||||
};
|
||||
|
||||
export default LowerHandButton;
|
||||
65
react/features/video-menu/components/web/MuteButton.tsx
Normal file
65
react/features/video-menu/components/web/MuteButton.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { rejectParticipantAudio } from '../../../av-moderation/actions';
|
||||
import { IconMicSlash } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { isRemoteTrackMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { muteRemote } from '../../actions.any';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* a participant in the conference.
|
||||
*
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
const MuteButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
|
||||
const audioTrackMuted = useMemo(
|
||||
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID),
|
||||
[ isRemoteTrackMuted, participantID, tracks ]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'mute',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(muteRemote(participantID, MEDIA_TYPE.AUDIO));
|
||||
dispatch(rejectParticipantAudio(participantID));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID, sendAnalytics ]);
|
||||
|
||||
if (audioTrackMuted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('dialog.muteParticipantButton') }
|
||||
className = 'mutelink'
|
||||
icon = { IconMicSlash }
|
||||
onClick = { handleClick }
|
||||
text = { t('dialog.muteParticipantButton') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default MuteButton;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconScreenshare } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { isRemoteTrackMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteRemoteParticipantsDesktopDialog from './MuteRemoteParticipantsDesktopDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for disabling
|
||||
* the desktop share of a participant in the conference.
|
||||
*
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
const MuteDesktopButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
|
||||
|
||||
// TODO: review if we shouldn't be using isParticipantMediaMuted.
|
||||
const trackMuted = useMemo(
|
||||
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.SCREENSHARE, participantID),
|
||||
[ isRemoteTrackMuted, participantID, tracks ]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'desktop.mute.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(openDialog(MuteRemoteParticipantsDesktopDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyClick, participantID, sendAnalytics ]);
|
||||
|
||||
if (trackMuted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('participantsPane.actions.stopDesktop') }
|
||||
className = 'mutedesktoplink'
|
||||
icon = { IconScreenshare }
|
||||
onClick = { handleClick }
|
||||
text = { t('participantsPane.actions.stopDesktop') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default MuteDesktopButton;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import Switch from '../../../base/ui/components/web/Switch';
|
||||
import AbstractMuteEveryoneDialog, { type IProps, abstractMapStateToProps }
|
||||
from '../AbstractMuteEveryoneDialog';
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.muteParticipantButton' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
title = { this.props.title }>
|
||||
<div className = 'mute-dialog'>
|
||||
{this.state.content}
|
||||
{
|
||||
this.props.isModerationSupported
|
||||
&& this.props.exclude.length === 0
|
||||
&& !this.props.isEveryoneModerator && (
|
||||
<>
|
||||
<div className = 'separator-line' />
|
||||
<div className = 'control-row'>
|
||||
<label htmlFor = 'moderation-switch'>
|
||||
{this.props.t('dialog.moderationAudioLabel')}
|
||||
</label>
|
||||
<Switch
|
||||
checked = { !this.state.audioModerationEnabled }
|
||||
id = 'moderation-switch'
|
||||
onChange = { this._onToggleModeration } />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(MuteEveryoneDialog));
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconMicSlash } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteEveryoneDialog from './MuteEveryoneDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* every participant in the conference except the one with the given
|
||||
* participantID.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const MuteEveryoneElseButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
|
||||
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
|
||||
}, [ dispatch, notifyMode, notifyClick, participantID, sendAnalytics ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElse') }
|
||||
icon = { IconMicSlash }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.domuteOthers') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default MuteEveryoneElseButton;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconScreenshare } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteEveryonesDesktopDialog from './MuteEveryonesDesktopDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* every participant in the conference except the one with the given
|
||||
* participantID.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const MuteEveryoneElsesDesktopButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelsesdesktop.pressed'));
|
||||
dispatch(openDialog(MuteEveryonesDesktopDialog, { exclude: [ participantID ] }));
|
||||
}, [ notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesDesktopStream') }
|
||||
icon = { IconScreenshare }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.domuteDesktopOfOthers') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default MuteEveryoneElsesDesktopButton;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconVideoOff } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteEveryonesVideoDialog from './MuteEveryonesVideoDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for audio muting
|
||||
* every participant in the conference except the one with the given
|
||||
* participantID.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const MuteEveryoneElsesVideoButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createToolbarEvent('mute.everyoneelsesvideo.pressed'));
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog, { exclude: [ participantID ] }));
|
||||
}, [ notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.muteEveryoneElsesVideoStream') }
|
||||
icon = { IconVideoOff }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.domuteVideoOfOthers') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default MuteEveryoneElsesVideoButton;
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import Switch from '../../../base/ui/components/web/Switch';
|
||||
import AbstractMuteEveryonesDesktopDialog, { type IProps, abstractMapStateToProps }
|
||||
from '../AbstractMuteEveryonesDesktopDialog';
|
||||
|
||||
/**
|
||||
* A React Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before disabling all remote participants cameras.
|
||||
*
|
||||
* @augments AbstractMuteEveryonesDesktopDialog
|
||||
*/
|
||||
class MuteEveryonesDesktopDialog extends AbstractMuteEveryonesDesktopDialog<IProps> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.muteParticipantsDesktopButton' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
title = { this.props.title }>
|
||||
<div className = 'mute-dialog'>
|
||||
{this.state.content}
|
||||
{ this.props.isModerationSupported && this.props.exclude.length === 0 && (
|
||||
<>
|
||||
<div className = 'separator-line' />
|
||||
<div className = 'control-row'>
|
||||
<label htmlFor = 'moderation-switch'>
|
||||
{this.props.t('dialog.moderationDesktopLabel')}
|
||||
</label>
|
||||
<Switch
|
||||
checked = { !this.state.moderationEnabled }
|
||||
id = 'moderation-switch'
|
||||
onChange = { this._onToggleModeration } />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(MuteEveryonesDesktopDialog));
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import Switch from '../../../base/ui/components/web/Switch';
|
||||
import AbstractMuteEveryonesVideoDialog, { type IProps, abstractMapStateToProps }
|
||||
from '../AbstractMuteEveryonesVideoDialog';
|
||||
|
||||
/**
|
||||
* A React Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before disabling all remote participants cameras.
|
||||
*
|
||||
* @augments AbstractMuteEveryonesVideoDialog
|
||||
*/
|
||||
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<IProps> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.muteParticipantsVideoButton' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
title = { this.props.title }>
|
||||
<div className = 'mute-dialog'>
|
||||
{this.state.content}
|
||||
{ this.props.isModerationSupported && this.props.exclude.length === 0 && (
|
||||
<>
|
||||
<div className = 'separator-line' />
|
||||
<div className = 'control-row'>
|
||||
<label htmlFor = 'moderation-switch'>
|
||||
{this.props.t('dialog.moderationVideoLabel')}
|
||||
</label>
|
||||
<Switch
|
||||
checked = { !this.state.moderationEnabled }
|
||||
id = 'moderation-switch'
|
||||
onChange = { this._onToggleModeration } />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(MuteEveryonesVideoDialog));
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import AbstractMuteRemoteParticipantsDesktopDialog, {
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractMuteRemoteParticipantsDesktopDialog';
|
||||
|
||||
/**
|
||||
* A React Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before disabling a remote participants camera.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class MuteRemoteParticipantsDesktopDialog extends AbstractMuteRemoteParticipantsDesktopDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.muteParticipantsDesktopButton' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.muteParticipantsDesktopTitle'>
|
||||
<div>
|
||||
{this.props.t(this.props.isModerationOn
|
||||
? 'dialog.muteParticipantsDesktopBodyModerationOn'
|
||||
: 'dialog.muteParticipantsDesktopBody'
|
||||
) }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(MuteRemoteParticipantsDesktopDialog));
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import AbstractMuteRemoteParticipantsVideoDialog, {
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractMuteRemoteParticipantsVideoDialog';
|
||||
|
||||
/**
|
||||
* A React Component with the contents for a dialog that asks for confirmation
|
||||
* from the user before disabling a remote participants camera.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class MuteRemoteParticipantsVideoDialog extends AbstractMuteRemoteParticipantsVideoDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.muteParticipantsVideoButton' }}
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.muteParticipantsVideoTitle'>
|
||||
<div>
|
||||
{this.props.t(this.props.isVideoModerationOn
|
||||
? 'dialog.muteParticipantsVideoBodyModerationOn'
|
||||
: 'dialog.muteParticipantsVideoBody'
|
||||
) }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(MuteRemoteParticipantsVideoDialog));
|
||||
66
react/features/video-menu/components/web/MuteVideoButton.tsx
Normal file
66
react/features/video-menu/components/web/MuteVideoButton.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconVideoOff } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { isRemoteTrackMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import MuteRemoteParticipantsVideoDialog from './MuteRemoteParticipantsVideoDialog';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for disabling
|
||||
* the camera of a participant in the conference.
|
||||
*
|
||||
* @returns {JSX.Element|null}
|
||||
*/
|
||||
const MuteVideoButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element | null => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const tracks = useSelector((state: IReduxState) => state['features/base/tracks']);
|
||||
|
||||
const videoTrackMuted = useMemo(
|
||||
() => isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, participantID),
|
||||
[ isRemoteTrackMuted, participantID, tracks ]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'video.mute.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(openDialog(MuteRemoteParticipantsVideoDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyClick, participantID, sendAnalytics ]);
|
||||
|
||||
if (videoTrackMuted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('participantsPane.actions.stopVideo') }
|
||||
className = 'mutevideolink'
|
||||
icon = { IconVideoOff }
|
||||
onClick = { handleClick }
|
||||
text = { t('participantsPane.actions.stopVideo') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default MuteVideoButton;
|
||||
@@ -0,0 +1,388 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { MEDIA_TYPE as AVM_MEDIA_TYPE } from '../../../av-moderation/constants';
|
||||
import { isSupported as isAvModerationSupported, isForceMuted } from '../../../av-moderation/functions';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
import { getLocalParticipant, hasRaisedHand, isPrivateChatEnabled } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import { isParticipantAudioMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { IRoom } from '../../../breakout-rooms/types';
|
||||
import { displayVerification } from '../../../e2ee/functions';
|
||||
import { setVolume } from '../../../filmstrip/actions.web';
|
||||
import { isStageFilmstripAvailable } from '../../../filmstrip/functions.web';
|
||||
import { QUICK_ACTION_BUTTON } from '../../../participants-pane/constants';
|
||||
import { getQuickActionButtonType } from '../../../participants-pane/functions';
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control/actions';
|
||||
import { getParticipantMenuButtonsWithNotifyClick, showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
import AskToUnmuteButton from './AskToUnmuteButton';
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import CustomOptionButton from './CustomOptionButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import LowerHandButton from './LowerHandButton';
|
||||
import MuteButton from './MuteButton';
|
||||
import MuteDesktopButton from './MuteDesktopButton';
|
||||
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
|
||||
import MuteEveryoneElsesDesktopButton from './MuteEveryoneElsesDesktopButton';
|
||||
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
|
||||
import MuteVideoButton from './MuteVideoButton';
|
||||
import PrivateMessageMenuButton from './PrivateMessageMenuButton';
|
||||
import RemoteControlButton, { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
|
||||
import SendToRoomButton from './SendToRoomButton';
|
||||
import TogglePinToStageButton from './TogglePinToStageButton';
|
||||
import VerifyParticipantButton from './VerifyParticipantButton';
|
||||
import VolumeSlider from './VolumeSlider';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Class name for the context menu.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Closes a drawer if open.
|
||||
*/
|
||||
closeDrawer?: () => void;
|
||||
|
||||
/**
|
||||
* The participant for which the drawer is open.
|
||||
* It contains the displayName & participantID.
|
||||
*/
|
||||
drawerParticipant?: {
|
||||
displayName: string;
|
||||
participantID: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Target elements against which positioning calculations are made.
|
||||
*/
|
||||
offsetTarget?: HTMLElement;
|
||||
|
||||
/**
|
||||
* Callback for the mouse entering the component.
|
||||
*/
|
||||
onEnter?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback for the mouse leaving the component.
|
||||
*/
|
||||
onLeave?: (e?: React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Callback for making a selection in the menu.
|
||||
*/
|
||||
onSelect: (value?: boolean | React.MouseEvent) => void;
|
||||
|
||||
/**
|
||||
* Participant reference.
|
||||
*/
|
||||
participant: IParticipant;
|
||||
|
||||
/**
|
||||
* The current state of the participant's remote control session.
|
||||
*/
|
||||
remoteControlState?: number;
|
||||
|
||||
/**
|
||||
* Whether or not the menu is displayed in the thumbnail remote video menu.
|
||||
*/
|
||||
thumbnailMenu?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
text: {
|
||||
color: theme.palette.text02,
|
||||
padding: '10px 16px',
|
||||
height: '40px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const ParticipantContextMenu = ({
|
||||
className,
|
||||
closeDrawer,
|
||||
drawerParticipant,
|
||||
offsetTarget,
|
||||
onEnter,
|
||||
onLeave,
|
||||
onSelect,
|
||||
participant,
|
||||
remoteControlState,
|
||||
thumbnailMenu
|
||||
}: IProps) => {
|
||||
const dispatch: IStore['dispatch'] = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const _isModerator = Boolean(localParticipant?.role === PARTICIPANT_ROLE.MODERATOR);
|
||||
const _isVideoForceMuted = useSelector<IReduxState>(state =>
|
||||
isForceMuted(participant, AVM_MEDIA_TYPE.VIDEO, state));
|
||||
const _isDesktopForceMuted = useSelector<IReduxState>(state =>
|
||||
isForceMuted(participant, AVM_MEDIA_TYPE.DESKTOP, state));
|
||||
const _isAudioMuted = useSelector((state: IReduxState) => isParticipantAudioMuted(participant, state));
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
|
||||
= useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const visitorsSupported = useSelector((state: IReduxState) => state['features/visitors'].supported);
|
||||
const { disableDemote, disableKick, disableGrantModerator } = remoteVideoMenu;
|
||||
const { participantsVolume } = useSelector((state: IReduxState) => state['features/filmstrip']);
|
||||
const _volume = (participant?.local ?? true ? undefined
|
||||
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
|
||||
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
||||
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
|
||||
const raisedHands = hasRaisedHand(participant);
|
||||
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
|
||||
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
const enablePrivateChat = useSelector((state: IReduxState) => isPrivateChatEnabled(participant, state));
|
||||
|
||||
const _currentRoomId = useSelector(getCurrentRoomId);
|
||||
const _rooms: IRoom[] = Object.values(useSelector(getBreakoutRooms));
|
||||
|
||||
const _onVolumeChange = useCallback(value => {
|
||||
dispatch(setVolume(participant.id, value));
|
||||
}, [ setVolume, dispatch ]);
|
||||
|
||||
const _getCurrentParticipantId = useCallback(() => {
|
||||
const drawer = _overflowDrawer && !thumbnailMenu;
|
||||
|
||||
return (drawer ? drawerParticipant?.participantID : participant?.id) ?? '';
|
||||
}
|
||||
, [ thumbnailMenu, _overflowDrawer, drawerParticipant, participant ]);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string) => {
|
||||
const notifyMode = buttonsWithNotifyClick?.get(buttonKey);
|
||||
|
||||
if (!notifyMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.API.notifyParticipantMenuButtonClicked(
|
||||
buttonKey,
|
||||
_getCurrentParticipantId(),
|
||||
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
|
||||
);
|
||||
}, [ buttonsWithNotifyClick, _getCurrentParticipantId ]);
|
||||
|
||||
const onBreakoutRoomButtonClick = useCallback(() => {
|
||||
onSelect(true);
|
||||
}, [ onSelect ]);
|
||||
|
||||
const isClickedFromParticipantPane = useMemo(
|
||||
() => !_overflowDrawer && !thumbnailMenu,
|
||||
[ _overflowDrawer, thumbnailMenu ]);
|
||||
const quickActionButtonType = useSelector((state: IReduxState) =>
|
||||
getQuickActionButtonType(participant, state));
|
||||
|
||||
const buttons: JSX.Element[] = [];
|
||||
const buttons2: JSX.Element[] = [];
|
||||
|
||||
const showVolumeSlider = !startSilent
|
||||
&& !isIosMobileBrowser()
|
||||
&& (_overflowDrawer || thumbnailMenu)
|
||||
&& typeof _volume === 'number'
|
||||
&& !isNaN(_volume);
|
||||
|
||||
const getButtonProps = useCallback((key: string) => {
|
||||
const notifyMode = buttonsWithNotifyClick?.get(key);
|
||||
const shouldNotifyClick = typeof notifyMode !== 'undefined';
|
||||
|
||||
return {
|
||||
key,
|
||||
notifyMode,
|
||||
notifyClick: shouldNotifyClick ? () => notifyClick(key) : undefined,
|
||||
participantID: _getCurrentParticipantId()
|
||||
};
|
||||
}, [ _getCurrentParticipantId, buttonsWithNotifyClick, notifyClick ]);
|
||||
|
||||
if (_isModerator) {
|
||||
if (isModerationSupported) {
|
||||
if (_isAudioMuted && !participant.isSilent
|
||||
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ASK_TO_UNMUTE)) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
{ ...getButtonProps(BUTTONS.ASK_UNMUTE) }
|
||||
buttonType = { MEDIA_TYPE.AUDIO } />
|
||||
);
|
||||
}
|
||||
if (_isVideoForceMuted
|
||||
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_VIDEO)) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
{ ...getButtonProps(BUTTONS.ALLOW_VIDEO) }
|
||||
buttonType = { MEDIA_TYPE.VIDEO } />
|
||||
);
|
||||
}
|
||||
if (_isDesktopForceMuted
|
||||
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ALLOW_DESKTOP)) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
{ ...getButtonProps(BUTTONS.ALLOW_DESKTOP) }
|
||||
buttonType = { MEDIA_TYPE.SCREENSHARE } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!disableRemoteMute && !participant.isSilent) {
|
||||
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) {
|
||||
buttons.push(<MuteButton { ...getButtonProps(BUTTONS.MUTE) } />);
|
||||
}
|
||||
buttons.push(<MuteEveryoneElseButton { ...getButtonProps(BUTTONS.MUTE_OTHERS) } />);
|
||||
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_VIDEO)) {
|
||||
buttons.push(<MuteVideoButton { ...getButtonProps(BUTTONS.MUTE_VIDEO) } />);
|
||||
}
|
||||
buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
|
||||
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.STOP_DESKTOP)) {
|
||||
buttons.push(<MuteDesktopButton { ...getButtonProps(BUTTONS.MUTE_DESKTOP) } />);
|
||||
}
|
||||
buttons.push(<MuteEveryoneElsesDesktopButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_DESKTOP) } />);
|
||||
}
|
||||
|
||||
if (raisedHands) {
|
||||
buttons2.push(<LowerHandButton { ...getButtonProps(BUTTONS.LOWER_PARTICIPANT_HAND) } />);
|
||||
}
|
||||
|
||||
if (!disableGrantModerator && !isBreakoutRoom) {
|
||||
buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
|
||||
}
|
||||
|
||||
if (!disableDemote && visitorsSupported && _isModerator) {
|
||||
buttons2.push(<DemoteToVisitorButton { ...getButtonProps(BUTTONS.DEMOTE) } />);
|
||||
}
|
||||
|
||||
if (!disableKick) {
|
||||
buttons2.push(<KickButton { ...getButtonProps(BUTTONS.KICK) } />);
|
||||
}
|
||||
|
||||
if (shouldDisplayVerification) {
|
||||
buttons2.push(<VerifyParticipantButton { ...getButtonProps(BUTTONS.VERIFY) } />);
|
||||
}
|
||||
}
|
||||
|
||||
if (stageFilmstrip) {
|
||||
buttons2.push(<TogglePinToStageButton { ...getButtonProps(BUTTONS.PIN_TO_STAGE) } />);
|
||||
}
|
||||
|
||||
if (enablePrivateChat) {
|
||||
buttons2.push(<PrivateMessageMenuButton { ...getButtonProps(BUTTONS.PRIVATE_MESSAGE) } />);
|
||||
}
|
||||
|
||||
if (thumbnailMenu && isMobileBrowser()) {
|
||||
buttons2.push(<ConnectionStatusButton { ...getButtonProps(BUTTONS.CONN_STATUS) } />);
|
||||
}
|
||||
|
||||
if (thumbnailMenu && remoteControlState) {
|
||||
const onRemoteControlToggle = useCallback(() => {
|
||||
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED) {
|
||||
dispatch(stopController(true));
|
||||
} else if (remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
|
||||
dispatch(requestRemoteControl(_getCurrentParticipantId()));
|
||||
}
|
||||
}, [ dispatch, remoteControlState, stopController, requestRemoteControl ]);
|
||||
|
||||
buttons2.push(<RemoteControlButton
|
||||
{ ...getButtonProps(BUTTONS.REMOTE_CONTROL) }
|
||||
onClick = { onRemoteControlToggle }
|
||||
remoteControlState = { remoteControlState } />
|
||||
);
|
||||
}
|
||||
|
||||
if (customParticipantMenuButtons) {
|
||||
customParticipantMenuButtons.forEach(
|
||||
({ icon, id, text }) => {
|
||||
buttons2.push(
|
||||
<CustomOptionButton
|
||||
icon = { icon }
|
||||
key = { id }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { () => notifyClick(id) }
|
||||
text = { text } />
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const breakoutRoomsButtons: any = [];
|
||||
|
||||
if (!thumbnailMenu && _isModerator) {
|
||||
_rooms.forEach(room => {
|
||||
if (room.id !== _currentRoomId) {
|
||||
breakoutRoomsButtons.push(
|
||||
<SendToRoomButton
|
||||
{ ...getButtonProps(BUTTONS.SEND_PARTICIPANT_TO_ROOM) }
|
||||
key = { room.id }
|
||||
onClick = { onBreakoutRoomButtonClick }
|
||||
room = { room } />
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
className = { className }
|
||||
entity = { participant }
|
||||
hidden = { thumbnailMenu ? false : undefined }
|
||||
inDrawer = { thumbnailMenu && _overflowDrawer }
|
||||
isDrawerOpen = { Boolean(drawerParticipant) }
|
||||
offsetTarget = { offsetTarget }
|
||||
onClick = { onSelect }
|
||||
onDrawerClose = { thumbnailMenu ? onSelect : closeDrawer }
|
||||
onMouseEnter = { onEnter }
|
||||
onMouseLeave = { onLeave }>
|
||||
{!thumbnailMenu && _overflowDrawer && drawerParticipant && <ContextMenuItemGroup
|
||||
actions = { [ {
|
||||
accessibilityLabel: drawerParticipant.displayName,
|
||||
customIcon: <Avatar
|
||||
participantId = { drawerParticipant.participantID }
|
||||
size = { 20 } />,
|
||||
text: drawerParticipant.displayName
|
||||
} ] } />}
|
||||
{buttons.length > 0 && (
|
||||
<ContextMenuItemGroup>
|
||||
{buttons}
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
<ContextMenuItemGroup>
|
||||
{buttons2}
|
||||
</ContextMenuItemGroup>
|
||||
{showVolumeSlider && (
|
||||
<ContextMenuItemGroup>
|
||||
<VolumeSlider
|
||||
initialValue = { _volume }
|
||||
key = 'volume-slider'
|
||||
onChange = { _onVolumeChange } />
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
{breakoutRoomsButtons.length > 0 && (
|
||||
<ContextMenuItemGroup>
|
||||
<div className = { styles.text }>
|
||||
{t('breakoutRooms.actions.sendToBreakoutRoom')}
|
||||
</div>
|
||||
{breakoutRoomsButtons}
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ParticipantContextMenu;
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { CHAT_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconMessage } from '../../../base/icons/svg';
|
||||
import { getParticipantById } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { openChat } from '../../../chat/actions.web';
|
||||
import { isButtonEnabled } from '../../../toolbox/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps extends IButtonProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* True if the private chat functionality is disabled, hence the button is not visible.
|
||||
*/
|
||||
_hidden: boolean;
|
||||
|
||||
/**
|
||||
* The participant to send the message to.
|
||||
*/
|
||||
_participant?: IParticipant;
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom implementation of the PrivateMessageButton specialized for
|
||||
* the web version of the remote video menu. When the web platform starts to use
|
||||
* the {@code AbstractButton} component for the remote video menu, we can get rid
|
||||
* of this component and use the generic button in the chat feature.
|
||||
*/
|
||||
class PrivateMessageMenuButton extends Component<IProps> {
|
||||
/**
|
||||
* Instantiates a new Component instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _hidden, t } = this.props;
|
||||
|
||||
if (_hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.privateMessage') }
|
||||
icon = { IconMessage }
|
||||
onClick = { this._onClick }
|
||||
text = { t('toolbar.privateMessage') } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to be invoked on pressing the button.
|
||||
*
|
||||
* @param {React.MouseEvent|undefined} e - The click event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { _participant, dispatch, notifyClick, notifyMode } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openChat(_participant));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux store to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {IProps} ownProps - The own props of the component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
|
||||
const { visible = enabled } = ownProps;
|
||||
|
||||
return {
|
||||
_participant: getParticipantById(state, ownProps.participantID),
|
||||
visible,
|
||||
_hidden: typeof interfaceConfig !== 'undefined'
|
||||
&& (interfaceConfig.DISABLE_PRIVATE_MESSAGES || !isButtonEnabled('chat', state))
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(PrivateMessageMenuButton));
|
||||
148
react/features/video-menu/components/web/RemoteControlButton.tsx
Normal file
148
react/features/video-menu/components/web/RemoteControlButton.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { createRemoteVideoMenuButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconRemoteControlStart, IconRemoteControlStop } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
|
||||
// TODO: Move these enums into the store after further reactification of the
|
||||
// non-react RemoteVideo component.
|
||||
export const REMOTE_CONTROL_MENU_STATES = {
|
||||
NOT_SUPPORTED: 0,
|
||||
NOT_STARTED: 1,
|
||||
REQUESTING: 2,
|
||||
STARTED: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RemoteControlButton}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* The callback to invoke when the component is clicked.
|
||||
*/
|
||||
onClick: (() => void) | null;
|
||||
|
||||
/**
|
||||
* The ID of the participant linked to the onClick callback.
|
||||
*/
|
||||
participantID: string;
|
||||
|
||||
/**
|
||||
* The current status of remote control. Should be a number listed in the
|
||||
* enum REMOTE_CONTROL_MENU_STATES.
|
||||
*/
|
||||
remoteControlState: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button showing the
|
||||
* current state of remote control for a participant and can start or stop a
|
||||
* remote control session.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class RemoteControlButton extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code RemoteControlButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {null|ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { remoteControlState, t } = this.props;
|
||||
|
||||
let disabled = false, icon;
|
||||
|
||||
switch (remoteControlState) {
|
||||
case REMOTE_CONTROL_MENU_STATES.NOT_STARTED:
|
||||
icon = IconRemoteControlStart;
|
||||
break;
|
||||
case REMOTE_CONTROL_MENU_STATES.REQUESTING:
|
||||
disabled = true;
|
||||
icon = IconRemoteControlStart;
|
||||
break;
|
||||
case REMOTE_CONTROL_MENU_STATES.STARTED:
|
||||
icon = IconRemoteControlStop;
|
||||
break;
|
||||
case REMOTE_CONTROL_MENU_STATES.NOT_SUPPORTED:
|
||||
|
||||
// Intentionally fall through.
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.remoteControl') }
|
||||
className = 'kicklink'
|
||||
disabled = { disabled }
|
||||
icon = { icon }
|
||||
onClick = { this._onClick }
|
||||
text = { t('videothumbnail.remoteControl') } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends analytics event for pressing the button and executes the passed
|
||||
* onClick handler.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { notifyClick, notifyMode, onClick, participantID, remoteControlState } = this.props;
|
||||
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: What do we do in case the state is e.g. "requesting"?
|
||||
if (remoteControlState === REMOTE_CONTROL_MENU_STATES.STARTED
|
||||
|| remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED) {
|
||||
|
||||
const enable
|
||||
= remoteControlState === REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'remote.control.button',
|
||||
{
|
||||
enable,
|
||||
'participant_id': participantID
|
||||
}));
|
||||
}
|
||||
onClick?.();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(RemoteControlButton);
|
||||
@@ -0,0 +1,273 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { batch, connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconDotsHorizontal } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getParticipantById } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import ConnectionIndicatorContent from
|
||||
'../../../connection-indicator/components/web/ConnectionIndicatorContent';
|
||||
import { THUMBNAIL_TYPE } from '../../../filmstrip/constants';
|
||||
import { renderConnectionStatus } from '../../actions.web';
|
||||
|
||||
import FakeParticipantContextMenu from './FakeParticipantContextMenu';
|
||||
import ParticipantContextMenu from './ParticipantContextMenu';
|
||||
import { REMOTE_CONTROL_MENU_STATES } from './RemoteControlButton';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link RemoteVideoMenuTriggerButton}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether the remote video context menu is disabled.
|
||||
*/
|
||||
_disabled: Boolean;
|
||||
|
||||
/**
|
||||
* Shared video local participant owner.
|
||||
*/
|
||||
_localVideoOwner?: boolean;
|
||||
|
||||
/**
|
||||
* The position relative to the trigger the remote menu should display
|
||||
* from.
|
||||
*/
|
||||
_menuPosition: string;
|
||||
|
||||
/**
|
||||
* Participant reference.
|
||||
*/
|
||||
_participant: IParticipant;
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the remote video menu will act.
|
||||
*/
|
||||
_participantDisplayName: string;
|
||||
|
||||
/**
|
||||
* The current state of the participant's remote control session.
|
||||
*/
|
||||
_remoteControlState?: number;
|
||||
|
||||
/**
|
||||
* Whether the popover should render the Connection Info stats.
|
||||
*/
|
||||
_showConnectionInfo: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the button should be visible.
|
||||
*/
|
||||
buttonVisible: boolean;
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Hides popover.
|
||||
*/
|
||||
hidePopover?: Function;
|
||||
|
||||
/**
|
||||
* The ID for the participant on which the remote video menu will act.
|
||||
*/
|
||||
participantID: string;
|
||||
|
||||
/**
|
||||
* Whether the popover is visible or not.
|
||||
*/
|
||||
popoverVisible?: boolean;
|
||||
|
||||
/**
|
||||
* Shows popover.
|
||||
*/
|
||||
showPopover?: Function;
|
||||
|
||||
/**
|
||||
* The type of the thumbnail.
|
||||
*/
|
||||
thumbnailType: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
triggerButton: {
|
||||
padding: '3px !important',
|
||||
borderRadius: '4px',
|
||||
|
||||
'& svg': {
|
||||
width: '18px',
|
||||
height: '18px'
|
||||
}
|
||||
},
|
||||
|
||||
contextMenu: {
|
||||
position: 'relative',
|
||||
marginTop: 0,
|
||||
right: 'auto',
|
||||
marginRight: '4px',
|
||||
marginBottom: '4px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const RemoteVideoMenuTriggerButton = ({
|
||||
_disabled,
|
||||
_localVideoOwner,
|
||||
_menuPosition,
|
||||
_participant,
|
||||
_participantDisplayName,
|
||||
_remoteControlState,
|
||||
_showConnectionInfo,
|
||||
buttonVisible,
|
||||
dispatch,
|
||||
hidePopover,
|
||||
participantID,
|
||||
popoverVisible,
|
||||
showPopover
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const _onPopoverOpen = useCallback(() => {
|
||||
showPopover?.();
|
||||
dispatch(setParticipantContextMenuOpen(true));
|
||||
}, []);
|
||||
|
||||
const _onPopoverClose = useCallback(() => {
|
||||
hidePopover?.();
|
||||
batch(() => {
|
||||
dispatch(setParticipantContextMenuOpen(false));
|
||||
dispatch(renderConnectionStatus(false));
|
||||
});
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const _renderRemoteVideoMenu = () => {
|
||||
const props = {
|
||||
className: classes.contextMenu,
|
||||
onSelect: _onPopoverClose,
|
||||
participant: _participant,
|
||||
thumbnailMenu: true
|
||||
};
|
||||
|
||||
if (_participant?.fakeParticipant) {
|
||||
return (
|
||||
<FakeParticipantContextMenu
|
||||
{ ...props }
|
||||
localVideoOwner = { _localVideoOwner } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ParticipantContextMenu
|
||||
{ ...props }
|
||||
remoteControlState = { _remoteControlState } />
|
||||
);
|
||||
};
|
||||
|
||||
let content;
|
||||
|
||||
if (_showConnectionInfo) {
|
||||
content = <ConnectionIndicatorContent participantId = { participantID } />;
|
||||
} else if (!_disabled) {
|
||||
content = _renderRemoteVideoMenu();
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = _participantDisplayName;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content = { content }
|
||||
headingLabel = { t('dialog.remoteUserControls', { username }) }
|
||||
id = 'remote-video-menu-trigger'
|
||||
onPopoverClose = { _onPopoverClose }
|
||||
onPopoverOpen = { _onPopoverOpen }
|
||||
position = { _menuPosition }
|
||||
visible = { Boolean(popoverVisible) }>
|
||||
{buttonVisible && !_disabled && (
|
||||
!isMobileBrowser() && <Button
|
||||
accessibilityLabel = { t('dialog.remoteUserControls', { username }) }
|
||||
className = { classes.triggerButton }
|
||||
icon = { IconDotsHorizontal }
|
||||
size = 'small' />
|
||||
)}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code RemoteVideoMenuTriggerButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { participantID, thumbnailType } = ownProps;
|
||||
let _remoteControlState;
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const participant = getParticipantById(state, participantID ?? '');
|
||||
const _participantDisplayName = participant?.name;
|
||||
const _isRemoteControlSessionActive = participant?.remoteControlSessionStatus ?? false;
|
||||
const _supportsRemoteControl = participant?.supportsRemoteControl ?? false;
|
||||
const { active, controller } = state['features/remote-control'];
|
||||
const { requestedParticipant, controlled } = controller;
|
||||
const activeParticipant = requestedParticipant || controlled;
|
||||
const { showConnectionInfo } = state['features/base/connection'];
|
||||
const { remoteVideoMenu } = state['features/base/config'];
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
|
||||
if (_supportsRemoteControl
|
||||
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
|
||||
if (requestedParticipant === participantID) {
|
||||
_remoteControlState = REMOTE_CONTROL_MENU_STATES.REQUESTING;
|
||||
} else if (controlled) {
|
||||
_remoteControlState = REMOTE_CONTROL_MENU_STATES.STARTED;
|
||||
} else {
|
||||
_remoteControlState = REMOTE_CONTROL_MENU_STATES.NOT_STARTED;
|
||||
}
|
||||
}
|
||||
|
||||
let _menuPosition;
|
||||
|
||||
switch (thumbnailType) {
|
||||
case THUMBNAIL_TYPE.TILE:
|
||||
_menuPosition = 'left-start';
|
||||
break;
|
||||
case THUMBNAIL_TYPE.VERTICAL:
|
||||
_menuPosition = 'left-end';
|
||||
break;
|
||||
case THUMBNAIL_TYPE.HORIZONTAL:
|
||||
_menuPosition = 'top';
|
||||
break;
|
||||
default:
|
||||
_menuPosition = 'auto';
|
||||
}
|
||||
|
||||
return {
|
||||
_disabled: Boolean(remoteVideoMenu?.disabled),
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_menuPosition,
|
||||
_participant: participant ?? { id: '' },
|
||||
_participantDisplayName: _participantDisplayName ?? '',
|
||||
_remoteControlState,
|
||||
_showConnectionInfo: Boolean(showConnectionInfo)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(RemoteVideoMenuTriggerButton);
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { createBreakoutRoomsEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IconRingGroup } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { sendParticipantToRoom } from '../../../breakout-rooms/actions';
|
||||
import { IRoom } from '../../../breakout-rooms/types';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps extends IButtonProps {
|
||||
|
||||
/**
|
||||
* Click handler.
|
||||
*/
|
||||
onClick?: Function;
|
||||
|
||||
/**
|
||||
* The room to send the participant to.
|
||||
*/
|
||||
room: IRoom;
|
||||
}
|
||||
|
||||
const SendToRoomButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
onClick,
|
||||
participantID,
|
||||
room
|
||||
}: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const _onClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
onClick?.();
|
||||
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
|
||||
dispatch(sendParticipantToRoom(participantID, room.id));
|
||||
}, [ dispatch, notifyClick, notifyMode, onClick, participantID, room, sendAnalytics ]);
|
||||
|
||||
const roomName = room.name || t('breakoutRooms.mainRoom');
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { roomName }
|
||||
icon = { IconRingGroup }
|
||||
onClick = { _onClick }
|
||||
text = { roomName } />
|
||||
);
|
||||
};
|
||||
|
||||
export default SendToRoomButton;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IconPin, IconPinned } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { togglePinStageParticipant } from '../../../filmstrip/actions.web';
|
||||
import { getPinnedActiveParticipants } from '../../../filmstrip/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
interface IProps extends IButtonProps {
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Whether the icon should be hidden or not.
|
||||
*/
|
||||
noIcon?: boolean;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function;
|
||||
}
|
||||
|
||||
const TogglePinToStageButton = ({
|
||||
className,
|
||||
noIcon = false,
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
onClick,
|
||||
participantID
|
||||
}: IProps): JSX.Element => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isActive = Boolean(useSelector(getPinnedActiveParticipants)
|
||||
.find(p => p.participantId === participantID));
|
||||
const _onClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(togglePinStageParticipant(participantID));
|
||||
onClick?.();
|
||||
}, [ dispatch, isActive, notifyClick, onClick, participantID ]);
|
||||
|
||||
const text = isActive
|
||||
? t('videothumbnail.unpinFromStage')
|
||||
: t('videothumbnail.pinToStage');
|
||||
|
||||
const icon = isActive ? IconPinned : IconPin;
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { text }
|
||||
icon = { noIcon ? null : icon }
|
||||
onClick = { _onClick }
|
||||
text = { text }
|
||||
textClassName = { className } />
|
||||
);
|
||||
};
|
||||
|
||||
export default TogglePinToStageButton;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { IconCheck } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { startVerification } from '../../../e2ee/actions';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button that
|
||||
* verifies the participant.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const VerifyParticipantButton = ({
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const _handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(startVerification(participantID));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.verify') }
|
||||
className = 'verifylink'
|
||||
icon = { IconCheck }
|
||||
id = { `verifylink_${participantID}` }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { _handleClick }
|
||||
text = { t('videothumbnail.verify') } />
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyParticipantButton;
|
||||
111
react/features/video-menu/components/web/VolumeSlider.tsx
Normal file
111
react/features/video-menu/components/web/VolumeSlider.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconVolumeUp } from '../../../base/icons/svg';
|
||||
import { VOLUME_SLIDER_SCALE } from '../../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VolumeSlider}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The value of the audio slider should display at when the component first
|
||||
* mounts. Changes will be stored in state. The value should be a number
|
||||
* between 0 and 1.
|
||||
*/
|
||||
initialValue: number;
|
||||
|
||||
/**
|
||||
* The callback to invoke when the audio slider value changes.
|
||||
*/
|
||||
onChange: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
minHeight: '40px',
|
||||
minWidth: '180px',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '10px 16px',
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui02
|
||||
}
|
||||
},
|
||||
|
||||
icon: {
|
||||
minWidth: '20px',
|
||||
marginRight: '16px',
|
||||
position: 'relative'
|
||||
},
|
||||
|
||||
sliderContainer: {
|
||||
position: 'relative',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
slider: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
top: '50%',
|
||||
transform: 'translate(0, -50%)'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const _onClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const VolumeSlider = ({
|
||||
initialValue,
|
||||
onChange
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [ volumeLevel, setVolumeLevel ] = useState((initialValue || 0) * VOLUME_SLIDER_SCALE);
|
||||
|
||||
const _onVolumeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newVolumeLevel = Number(event.currentTarget.value);
|
||||
|
||||
onChange(newVolumeLevel / VOLUME_SLIDER_SCALE);
|
||||
setVolumeLevel(newVolumeLevel);
|
||||
}, [ onChange ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label = { t('volumeSlider') }
|
||||
className = { cx('popupmenu__contents', classes.container) }
|
||||
onClick = { _onClick }>
|
||||
<span className = { classes.icon }>
|
||||
<Icon
|
||||
size = { 22 }
|
||||
src = { IconVolumeUp } />
|
||||
</span>
|
||||
<div className = { classes.sliderContainer }>
|
||||
<input
|
||||
aria-valuemax = { VOLUME_SLIDER_SCALE }
|
||||
aria-valuemin = { 0 }
|
||||
aria-valuenow = { volumeLevel }
|
||||
className = { cx('popupmenu__volume-slider', classes.slider) }
|
||||
max = { VOLUME_SLIDER_SCALE }
|
||||
min = { 0 }
|
||||
onChange = { _onVolumeChange }
|
||||
tabIndex = { 0 }
|
||||
type = 'range'
|
||||
value = { volumeLevel } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolumeSlider;
|
||||
41
react/features/video-menu/constants.ts
Normal file
41
react/features/video-menu/constants.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Used to set maximumValue for native volume slider.
|
||||
* Slider double-precision floating-point number indicating the volume,
|
||||
* from 0 mute to 1 max, which converts to 0 mute to 19 max in our case.
|
||||
* 0 as muted, 10 as standard and 19 as max remote participant volume level.
|
||||
*/
|
||||
export const NATIVE_VOLUME_SLIDER_SCALE = 19;
|
||||
|
||||
/**
|
||||
* Used to modify initialValue, which is expected to be a decimal value between
|
||||
* 0 and 1, and converts it to a number representable by an input slider, which
|
||||
* recognizes whole numbers.
|
||||
*/
|
||||
export const VOLUME_SLIDER_SCALE = 100;
|
||||
|
||||
/**
|
||||
* Participant context menu button keys.
|
||||
*/
|
||||
export const PARTICIPANT_MENU_BUTTONS = {
|
||||
ALLOW_DESKTOP: 'allow-desktop',
|
||||
ALLOW_VIDEO: 'allow-video',
|
||||
ASK_UNMUTE: 'ask-unmute',
|
||||
CONN_STATUS: 'conn-status',
|
||||
DEMOTE: 'demote',
|
||||
FLIP_LOCAL_VIDEO: 'flip-local-video',
|
||||
GRANT_MODERATOR: 'grant-moderator',
|
||||
HIDE_SELF_VIEW: 'hide-self-view',
|
||||
KICK: 'kick',
|
||||
LOWER_PARTICIPANT_HAND: 'lower-participant-hand',
|
||||
MUTE: 'mute',
|
||||
MUTE_DESKTOP: 'mute-desktop',
|
||||
MUTE_OTHERS: 'mute-others',
|
||||
MUTE_OTHERS_DESKTOP: 'mute-others-desktop',
|
||||
MUTE_OTHERS_VIDEO: 'mute-others-video',
|
||||
MUTE_VIDEO: 'mute-video',
|
||||
PIN_TO_STAGE: 'pinToStage',
|
||||
PRIVATE_MESSAGE: 'privateMessage',
|
||||
REMOTE_CONTROL: 'remote-control',
|
||||
SEND_PARTICIPANT_TO_ROOM: 'send-participant-to-room',
|
||||
VERIFY: 'verify'
|
||||
};
|
||||
3
react/features/video-menu/logger.ts
Normal file
3
react/features/video-menu/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/video-menu');
|
||||
18
react/features/video-menu/types.ts
Normal file
18
react/features/video-menu/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface IButtonProps {
|
||||
|
||||
/**
|
||||
* Callback to execute when the button is clicked.
|
||||
*/
|
||||
notifyClick?: Function;
|
||||
|
||||
/**
|
||||
* Notify mode for the `participantMenuButtonClicked` event -
|
||||
* whether to only notify or to also prevent button click routine.
|
||||
*/
|
||||
notifyMode?: string;
|
||||
|
||||
/**
|
||||
* The ID of the participant that's linked to the button.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
Reference in New Issue
Block a user