diff --git a/react/features/participants-pane/components/native/MeetingParticipantItem.tsx b/react/features/participants-pane/components/native/MeetingParticipantItem.tsx index 350388bee3..67c817ff23 100644 --- a/react/features/participants-pane/components/native/MeetingParticipantItem.tsx +++ b/react/features/participants-pane/components/native/MeetingParticipantItem.tsx @@ -15,7 +15,7 @@ import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.native'; -import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native'; +import { showContextMenuDetails, showSharedVideoMenu } from '../../actions.native'; import type { MediaState } from '../../constants'; import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions'; @@ -117,11 +117,7 @@ class MeetingParticipantItem extends PureComponent { if (_fakeParticipant && _localVideoOwner) { dispatch(showSharedVideoMenu(_participantID)); } else if (!_fakeParticipant) { - if (_local) { - dispatch(showConnectionStatus(_participantID)); - } else { - dispatch(showContextMenuDetails(_participantID)); - } + dispatch(showContextMenuDetails(_participantID, _local)); } // else no-op } diff --git a/react/features/video-menu/components/index.native.ts b/react/features/video-menu/components/index.native.ts index 976ea9ba87..bbb8d85336 100644 --- a/react/features/video-menu/components/index.native.ts +++ b/react/features/video-menu/components/index.native.ts @@ -1,5 +1,7 @@ /* 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'; diff --git a/react/features/video-menu/components/native/DemoteToVisitorButton.ts b/react/features/video-menu/components/native/DemoteToVisitorButton.ts new file mode 100644 index 0000000000..619cecf4a3 --- /dev/null +++ b/react/features/video-menu/components/native/DemoteToVisitorButton.ts @@ -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 { + accessibilityLabel = 'videothumbnail.demote'; + icon = IconUsers; + label = 'videothumbnail.demote'; + + /** + * Handles clicking / pressing the button, and demoting the participant. + * + * @private + * @returns {void} + */ + _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)); diff --git a/react/features/video-menu/components/native/DemoteToVisitorDialog.tsx b/react/features/video-menu/components/native/DemoteToVisitorDialog.tsx new file mode 100644 index 0000000000..f09527ae59 --- /dev/null +++ b/react/features/video-menu/components/native/DemoteToVisitorDialog.tsx @@ -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 ( + + ); +} diff --git a/react/features/video-menu/components/native/LocalVideoMenu.tsx b/react/features/video-menu/components/native/LocalVideoMenu.tsx index 64d10097b0..020c4daab5 100644 --- a/react/features/video-menu/components/native/LocalVideoMenu.tsx +++ b/react/features/video-menu/components/native/LocalVideoMenu.tsx @@ -4,17 +4,20 @@ 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'; /** @@ -34,6 +37,11 @@ interface IProps { */ _participantDisplayName: string; + /** + * Shows/hides the local switch to visitor button. + */ + _showDemote: boolean; + /** * The Redux dispatch function. */ @@ -57,6 +65,7 @@ class LocalVideoMenu extends PureComponent { constructor(props: IProps) { super(props); + this._onCancel = this._onCancel.bind(this); this._renderMenuHeader = this._renderMenuHeader.bind(this); } @@ -66,8 +75,9 @@ class LocalVideoMenu extends PureComponent { * @inheritdoc */ render() { - const { _participant } = this.props; + const { _participant, _showDemote } = this.props; const buttonProps = { + afterClick: this._onCancel, showLabel: true, participantID: _participant?.id ?? '', styles: bottomSheetStyles.buttons @@ -78,6 +88,7 @@ class LocalVideoMenu extends PureComponent { renderHeader = { this._renderMenuHeader } showSlidingView = { true }> + { _showDemote && } ); @@ -105,6 +116,16 @@ class LocalVideoMenu extends PureComponent { ); } + + /** + * Callback to hide the {@code RemoteVideoMenu}. + * + * @private + * @returns {boolean} + */ + _onCancel() { + this.props.dispatch(hideSheet()); + } } /** @@ -119,7 +140,8 @@ function _mapStateToProps(state: IReduxState) { return { _participant: participant, - _participantDisplayName: getParticipantDisplayName(state, participant?.id ?? '') + _participantDisplayName: getParticipantDisplayName(state, participant?.id ?? ''), + _showDemote: getParticipantCount(state) > 1 }; } diff --git a/react/features/video-menu/components/native/RemoteVideoMenu.tsx b/react/features/video-menu/components/native/RemoteVideoMenu.tsx index 2410a0af9f..89d09ca88c 100644 --- a/react/features/video-menu/components/native/RemoteVideoMenu.tsx +++ b/react/features/video-menu/components/native/RemoteVideoMenu.tsx @@ -24,6 +24,7 @@ import PrivateMessageButton from '../../../chat/components/native/PrivateMessage import AskUnmuteButton from './AskUnmuteButton'; import ConnectionStatusButton from './ConnectionStatusButton'; +import DemoteToVisitorButton from './DemoteToVisitorButton'; import GrantModeratorButton from './GrantModeratorButton'; import KickButton from './KickButton'; import MuteButton from './MuteButton'; @@ -92,6 +93,11 @@ interface IProps { */ _rooms: Array; + /** + * Whether to display the demote button. + */ + _showDemote: boolean; + /** * The Redux dispatch function. */ @@ -139,6 +145,7 @@ class RemoteVideoMenu extends PureComponent { _isParticipantAvailable, _moderator, _rooms, + _showDemote, _currentRoomId, participantId, t @@ -168,6 +175,7 @@ class RemoteVideoMenu extends PureComponent { { !_disableKick && } { !_disableGrantModerator && !_isBreakoutRoom && } + { _showDemote && } { !_disablePrivateChat && } {_moderator && _rooms.length > 1 && <> @@ -252,7 +260,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { _isParticipantAvailable: Boolean(isParticipantAvailable), _moderator: moderator, _participantDisplayName: getParticipantDisplayName(state, participantId), - _rooms + _rooms, + _showDemote: moderator }; }