From 3c2ad24652ffc89fbad8fcce49cd040b3c7b9c50 Mon Sep 17 00:00:00 2001 From: Calinteodor Date: Wed, 4 Aug 2021 11:51:05 +0300 Subject: [PATCH] fix(shared-video,video-menu) add ability to stop shared video from video menu Specifically, in the bottom sheet (on mobile) and participants pane. --- .../filmstrip/components/native/Thumbnail.js | 30 ++- .../participants-pane/actions.native.js | 11 ++ .../native/MeetingParticipantList.js | 71 ++++++- .../components/native/ParticipantsPane.js | 2 +- .../components/native/index.js | 1 + .../web/MeetingParticipantContextMenu.js | 148 ++++++++------ .../components/web/MeetingParticipantItem.js | 53 +++++- .../components/web/styled.js | 1 - react/features/video-menu/actions.native.js | 11 +- .../components/native/SharedVideoMenu.js | 180 ++++++++++++++++++ .../video-menu/components/native/index.js | 1 + 11 files changed, 427 insertions(+), 82 deletions(-) create mode 100644 react/features/video-menu/components/native/SharedVideoMenu.js diff --git a/react/features/filmstrip/components/native/Thumbnail.js b/react/features/filmstrip/components/native/Thumbnail.js index eb3f145e9..d135ecf3a 100644 --- a/react/features/filmstrip/components/native/Thumbnail.js +++ b/react/features/filmstrip/components/native/Thumbnail.js @@ -13,7 +13,8 @@ import { getParticipantCount, isEveryoneModerator, pinParticipant, - getParticipantByIdOrUndefined + getParticipantByIdOrUndefined, + getLocalParticipant } from '../../../base/participants'; import { Container } from '../../../base/react'; import { connect } from '../../../base/redux'; @@ -24,6 +25,8 @@ import { DisplayNameLabel } from '../../../display-name'; import { toggleToolboxVisible } from '../../../toolbox/actions.native'; import { RemoteVideoMenu } from '../../../video-menu'; import ConnectionStatusComponent from '../../../video-menu/components/native/ConnectionStatusComponent'; +import SharedVideoMenu + from '../../../video-menu/components/native/SharedVideoMenu'; import AudioMutedIndicator from './AudioMutedIndicator'; import DominantSpeakerIndicator from './DominantSpeakerIndicator'; @@ -48,6 +51,11 @@ type Props = { */ _largeVideo: Object, + /** + * Shared video local participant owner. + */ + _localVideoOwner: boolean, + /** * The Redux representation of the participant to display. */ @@ -116,6 +124,7 @@ function Thumbnail(props: Props) { const { _audioMuted: audioMuted, _largeVideo: largeVideo, + _localVideoOwner, _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator, _renderModeratorIndicator: renderModeratorIndicator, _participant: participant, @@ -144,11 +153,19 @@ function Thumbnail(props: Props) { dispatch(openDialog(ConnectionStatusComponent, { participantID: participant.id })); - } else { - dispatch(openDialog(RemoteVideoMenu, { - participant - })); + } else if (participant.isFakeParticipant) { + if (_localVideoOwner) { + dispatch(openDialog(SharedVideoMenu, { + participant + })); + } + + return null; } + + dispatch(openDialog(RemoteVideoMenu, { + participant + })); }, [ participant, dispatch ]); return ( @@ -223,9 +240,11 @@ function _mapStateToProps(state, ownProps) { // filmstrip doesn't render the video of the participant who is rendered on // the stage i.e. as a large video. const largeVideo = state['features/large-video']; + const { ownerId } = state['features/shared-video']; const tracks = state['features/base/tracks']; const { participantID } = ownProps; const participant = getParticipantByIdOrUndefined(state, participantID); + const localParticipantId = getLocalParticipant(state).id; const id = participant?.id; const audioTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id); @@ -240,6 +259,7 @@ function _mapStateToProps(state, ownProps) { return { _audioMuted: audioTrack?.muted ?? true, _largeVideo: largeVideo, + _localVideoOwner: Boolean(ownerId === localParticipantId), _participant: participant, _renderDominantSpeakerIndicator: renderDominantSpeakerIndicator, _renderModeratorIndicator: renderModeratorIndicator, diff --git a/react/features/participants-pane/actions.native.js b/react/features/participants-pane/actions.native.js index e24d7cb70..3812e6951 100644 --- a/react/features/participants-pane/actions.native.js +++ b/react/features/participants-pane/actions.native.js @@ -1,6 +1,7 @@ // @flow import { openDialog } from '../base/dialog'; +import { SharedVideoMenu } from '../video-menu'; import ConnectionStatusComponent from '../video-menu/components/native/ConnectionStatusComponent'; import RemoteVideoMenu from '../video-menu/components/native/RemoteVideoMenu'; @@ -42,6 +43,16 @@ export function showContextMenuDetails(participant: Object) { return openDialog(RemoteVideoMenu, { participant }); } +/** + * Displays the shared video menu. + * + * @param {Object} participant - The selected meeting participant. + * @returns {Function} + */ +export function showSharedVideoMenu(participant: Object) { + return openDialog(SharedVideoMenu, { participant }); +} + /** * Sets the volume. * diff --git a/react/features/participants-pane/components/native/MeetingParticipantList.js b/react/features/participants-pane/components/native/MeetingParticipantList.js index 94de39c2d..a61218a4c 100644 --- a/react/features/participants-pane/components/native/MeetingParticipantList.js +++ b/react/features/participants-pane/components/native/MeetingParticipantList.js @@ -6,20 +6,34 @@ import { Text, View } from 'react-native'; import { Button } from 'react-native-paper'; import { useDispatch, useSelector } from 'react-redux'; + import { Icon, IconInviteMore } from '../../../base/icons'; import { getLocalParticipant, getParticipantCountWithFake, getRemoteParticipants } from '../../../base/participants'; +import { connect } from '../../../base/redux'; import { doInvitePeople } from '../../../invite/actions.native'; -import { showConnectionStatus, showContextMenuDetails } from '../../actions.native'; +import { + showConnectionStatus, + showContextMenuDetails, + showSharedVideoMenu +} from '../../actions.native'; import { shouldRenderInviteButton } from '../../functions'; import MeetingParticipantItem from './MeetingParticipantItem'; import styles from './styles'; -export const MeetingParticipantList = () => { +type Props = { + + /** + * Shared video local participant owner. + */ + _localVideoOwner: boolean +} + +const MeetingParticipantList = ({ _localVideoOwner }: Props) => { const dispatch = useDispatch(); const items = []; const localParticipant = useSelector(getLocalParticipant); @@ -30,14 +44,34 @@ export const MeetingParticipantList = () => { const { t } = useTranslation(); // eslint-disable-next-line react/no-multi-comp - const renderParticipant = p => ( - p.local - ? dispatch(showConnectionStatus(p.id)) : dispatch(showContextMenuDetails(p)) } - participantID = { p.id } /> - ); + const renderParticipant = p => { + if (p.isFakeParticipant) { + if (_localVideoOwner) { + return ( + dispatch(showSharedVideoMenu(p)) } + participantID = { p.id } /> + ); + } + + return ( + + ); + } + + return ( + p.local + ? dispatch(showConnectionStatus(p.id)) : dispatch(showContextMenuDetails(p)) } + participantID = { p.id } /> + ); + }; items.push(renderParticipant(localParticipant)); @@ -71,3 +105,20 @@ export const MeetingParticipantList = () => { ); }; +/** + * Maps (parts of) the redux state to the associated props for this component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {Props} + */ +function _mapStateToProps(state): Object { + const { ownerId } = state['features/shared-video']; + const localParticipantId = getLocalParticipant(state).id; + + return { + _localVideoOwner: Boolean(ownerId === localParticipantId) + }; +} + +export default connect(_mapStateToProps)(MeetingParticipantList); diff --git a/react/features/participants-pane/components/native/ParticipantsPane.js b/react/features/participants-pane/components/native/ParticipantsPane.js index e4735faa5..e61715947 100644 --- a/react/features/participants-pane/components/native/ParticipantsPane.js +++ b/react/features/participants-pane/components/native/ParticipantsPane.js @@ -19,7 +19,7 @@ import { close } from '../../actions.native'; import { ContextMenuMore } from './ContextMenuMore'; import LobbyParticipantList from './LobbyParticipantList'; -import { MeetingParticipantList } from './MeetingParticipantList'; +import MeetingParticipantList from './MeetingParticipantList'; import styles from './styles'; /** diff --git a/react/features/participants-pane/components/native/index.js b/react/features/participants-pane/components/native/index.js index 88a90cb8a..6d03bb8ae 100644 --- a/react/features/participants-pane/components/native/index.js +++ b/react/features/participants-pane/components/native/index.js @@ -1,5 +1,6 @@ // @flow +export { default as MeetingParticipantList } from './MeetingParticipantList'; export { default as ParticipantsPane } from './ParticipantsPane'; export { default as ParticipantsPaneButton } from './ParticipantsPaneButton'; export { default as ContextMenuLobbyParticipantReject } from './ContextMenuLobbyParticipantReject'; diff --git a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js index f92deac75..4981703f1 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js +++ b/react/features/participants-pane/components/web/MeetingParticipantContextMenu.js @@ -11,9 +11,11 @@ import { IconMessage, IconMicDisabled, IconMuteEveryoneElse, + IconShareVideo, IconVideoOff } from '../../../base/icons'; import { + getLocalParticipant, getParticipantByIdOrUndefined, isLocalParticipantModerator, isParticipantModerator @@ -21,6 +23,7 @@ import { import { connect } from '../../../base/redux'; import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks'; import { openChat } from '../../../chat/actions'; +import { stopSharedVideo } from '../../../shared-video/actions.any'; import { GrantModeratorDialog, KickRemoteParticipantDialog, MuteEveryoneDialog } from '../../../video-menu'; import MuteRemoteParticipantsVideoDialog from '../../../video-menu/components/web/MuteRemoteParticipantsVideoDialog'; import { getComputedOuterHeight } from '../../functions'; @@ -60,6 +63,11 @@ type Props = { */ _isParticipantAudioMuted: boolean, + /** + * Shared video local participant owner. + */ + _localVideoOwner: boolean, + /** * Participant reference */ @@ -143,6 +151,7 @@ class MeetingParticipantContextMenu extends Component { this._onMuteEveryoneElse = this._onMuteEveryoneElse.bind(this); this._onMuteVideo = this._onMuteVideo.bind(this); this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this); + this._onStopSharedVideo = this._onStopSharedVideo.bind(this); this._position = this._position.bind(this); } @@ -176,6 +185,19 @@ class MeetingParticipantContextMenu extends Component { })); } + _onStopSharedVideo: () => void; + + /** + * Stops shared video. + * + * @returns {void} + */ + _onStopSharedVideo() { + const { dispatch } = this.props; + + dispatch(stopSharedVideo()); + } + _onMuteEveryoneElse: () => void; /** @@ -282,6 +304,7 @@ class MeetingParticipantContextMenu extends Component { _isParticipantModerator, _isParticipantVideoMuted, _isParticipantAudioMuted, + _localVideoOwner, _participant, onEnter, onLeave, @@ -302,66 +325,81 @@ class MeetingParticipantContextMenu extends Component { onClick = { onSelect } onMouseEnter = { onEnter } onMouseLeave = { onLeave }> - - { - _isLocalModerator && ( - <> + { + !_participant.isFakeParticipant && ( + <> + { - !_isParticipantAudioMuted - && - - {t('dialog.muteParticipantButton')} - + _isLocalModerator && ( + <> + { + !_isParticipantAudioMuted + && + + {t('dialog.muteParticipantButton')} + + } + + + + {t('toolbar.accessibilityLabel.muteEveryoneElse')} + + + ) } - - - {t('toolbar.accessibilityLabel.muteEveryoneElse')} - - - ) - } - - { - _isLocalModerator && ( - _isParticipantVideoMuted || ( - - - {t('participantsPane.actions.stopVideo')} - - ) - ) - } - - - - { - _isLocalModerator && ( - <> { - !_isParticipantModerator && ( - - - {t('toolbar.accessibilityLabel.grantModerator')} + _isLocalModerator && ( + _isParticipantVideoMuted || ( + + + {t('participantsPane.actions.stopVideo')} + + ) + ) + } + + + + { + _isLocalModerator && ( + <> + { + !_isParticipantModerator && ( + + + {t('toolbar.accessibilityLabel.grantModerator')} + + ) + } + + + { t('videothumbnail.kick') } + + + ) + } + { + _isChatButtonEnabled && ( + + + {t('toolbar.accessibilityLabel.privateMessage')} ) } - - - { t('videothumbnail.kick') } - - - ) - } - { - _isChatButtonEnabled && ( - - - {t('toolbar.accessibilityLabel.privateMessage')} - - ) - } - + + + ) + } + + { + _participant.isFakeParticipant && _localVideoOwner && ( + + + {t('toolbar.stopSharedVideo')} + + ) + } ); } @@ -377,7 +415,8 @@ class MeetingParticipantContextMenu extends Component { */ function _mapStateToProps(state, ownProps): Object { const { participantID } = ownProps; - + const { ownerId } = state['features/shared-video']; + const localParticipantId = getLocalParticipant(state).id; const participant = getParticipantByIdOrUndefined(state, participantID); const _isLocalModerator = isLocalParticipantModerator(state); @@ -392,6 +431,7 @@ function _mapStateToProps(state, ownProps): Object { _isParticipantModerator, _isParticipantVideoMuted, _isParticipantAudioMuted, + _localVideoOwner: Boolean(ownerId === localParticipantId), _participant: participant }; } diff --git a/react/features/participants-pane/components/web/MeetingParticipantItem.js b/react/features/participants-pane/components/web/MeetingParticipantItem.js index 9da0e5768..bf32226f8 100644 --- a/react/features/participants-pane/components/web/MeetingParticipantItem.js +++ b/react/features/participants-pane/components/web/MeetingParticipantItem.js @@ -2,7 +2,11 @@ import React from 'react'; -import { getParticipantByIdOrUndefined, getParticipantDisplayName } from '../../../base/participants'; +import { + getLocalParticipant, + getParticipantByIdOrUndefined, + getParticipantDisplayName +} from '../../../base/participants'; import { connect } from '../../../base/redux'; import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks'; import { ACTION_TRIGGER, MEDIA_STATE, type MediaState } from '../../constants'; @@ -34,6 +38,16 @@ type Props = { */ _local: boolean, + /** + * Shared video local participant owner. + */ + _localVideoOwner: boolean, + + /** + * The participant. + */ + _participant: Object, + /** * The participant ID. * @@ -108,7 +122,9 @@ function MeetingParticipantItem({ _audioMediaState, _displayName, _isVideoMuted, + _localVideoOwner, _local, + _participant, _participantID, _quickActionButtonType, _raisedHand, @@ -133,15 +149,28 @@ function MeetingParticipantItem({ raisedHand = { _raisedHand } videoMuteState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } youText = { youText }> - - + { + !_participant.isFakeParticipant && ( + <> + + + + ) + } + { + _participant.isFakeParticipant && _localVideoOwner && ( + + ) + } ); } @@ -156,6 +185,8 @@ function MeetingParticipantItem({ */ function _mapStateToProps(state, ownProps): Object { const { participantID } = ownProps; + const { ownerId } = state['features/shared-video']; + const localParticipantId = getLocalParticipant(state).id; const participant = getParticipantByIdOrUndefined(state, participantID); @@ -170,6 +201,8 @@ function _mapStateToProps(state, ownProps): Object { _isAudioMuted, _isVideoMuted, _local: Boolean(participant?.local), + _localVideoOwner: Boolean(ownerId === localParticipantId), + _participant: participant, _participantID: participant?.id, _quickActionButtonType, _raisedHand: Boolean(participant?.raisedHand) diff --git a/react/features/participants-pane/components/web/styled.js b/react/features/participants-pane/components/web/styled.js index c6c076a6e..5bbe47f82 100644 --- a/react/features/participants-pane/components/web/styled.js +++ b/react/features/participants-pane/components/web/styled.js @@ -235,7 +235,6 @@ export const ParticipantActionsHover = styled(ParticipantActions)` position: absolute; top: 0; transform: translateX(-100%); - width: 40px; } `; diff --git a/react/features/video-menu/actions.native.js b/react/features/video-menu/actions.native.js index 41ede4f0b..3b8de407a 100644 --- a/react/features/video-menu/actions.native.js +++ b/react/features/video-menu/actions.native.js @@ -1,7 +1,7 @@ // @flow import { hideDialog } from '../base/dialog'; -import { RemoteVideoMenu } from './components/native'; +import { RemoteVideoMenu, SharedVideoMenu } from './components/native'; /** * Hides the remote video menu. @@ -12,4 +12,13 @@ export function hideRemoteVideoMenu() { return hideDialog(RemoteVideoMenu); } +/** + * Hides the shared video menu. + * + * @returns {Function} + */ +export function hideSharedVideoMenu() { + return hideDialog(SharedVideoMenu); +} + export * from './actions.any'; diff --git a/react/features/video-menu/components/native/SharedVideoMenu.js b/react/features/video-menu/components/native/SharedVideoMenu.js new file mode 100644 index 000000000..507144f19 --- /dev/null +++ b/react/features/video-menu/components/native/SharedVideoMenu.js @@ -0,0 +1,180 @@ +// @flow + +import React, { PureComponent } from 'react'; +import { Text, View } from 'react-native'; +import { Divider } from 'react-native-paper'; + +import { Avatar } from '../../../base/avatar'; +import { ColorSchemeRegistry } from '../../../base/color-scheme'; +import { BottomSheet, isDialogOpen } from '../../../base/dialog'; +import { + getParticipantById, + getParticipantDisplayName +} from '../../../base/participants'; +import { connect } from '../../../base/redux'; +import { StyleType } from '../../../base/styles'; +import { SharedVideoButton } from '../../../shared-video/components'; +import { hideSharedVideoMenu } from '../../actions.native'; + +import styles from './styles'; + + +/** + * Size of the rendered avatar in the menu. + */ +const AVATAR_SIZE = 24; + +type Props = { + + /** + * The Redux dispatch function. + */ + dispatch: Function, + + /** + * The participant for which this menu opened for. + */ + participant: Object, + + /** + * The color-schemed stylesheet of the BottomSheet. + */ + _bottomSheetStyles: StyleType, + + /** + * 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 ID of the participant. + */ + _participantID: ?string, +} + +// eslint-disable-next-line prefer-const +let SharedVideoMenu_; + +/** + * Class to implement a popup menu that opens upon long pressing a fake participant thumbnail. + */ +class SharedVideoMenu extends PureComponent { + /** + * Constructor of the component. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this._onCancel = this._onCancel.bind(this); + this._renderMenuHeader = this._renderMenuHeader.bind(this); + } + + /** + * Implements {@code Component#render}. + * + * @inheritdoc + */ + render() { + const { + _isParticipantAvailable, + participant + } = this.props; + + const buttonProps = { + afterClick: this._onCancel, + showLabel: true, + participantID: participant.id, + styles: this.props._bottomSheetStyles.buttons + }; + + return ( + + + + + ); + } + + _onCancel: () => boolean; + + /** + * Callback to hide the {@code SharedVideoMenu}. + * + * @private + * @returns {boolean} + */ + _onCancel() { + if (this.props._isOpen) { + this.props.dispatch(hideSharedVideoMenu()); + + return true; + } + + return false; + } + + _renderMenuHeader: () => React$Element; + + /** + * Function to render the menu's header. + * + * @returns {React$Element} + */ + _renderMenuHeader() { + const { _bottomSheetStyles, participant } = this.props; + + return ( + + + + { this.props._participantDisplayName } + + + ); + } +} + +/** + * 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, ownProps) { + const { participant } = ownProps; + const isParticipantAvailable = getParticipantById(state, participant.id); + + return { + _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'), + _isOpen: isDialogOpen(state, SharedVideoMenu_), + _isParticipantAvailable: Boolean(isParticipantAvailable), + _participantDisplayName: getParticipantDisplayName(state, participant.id), + _participantID: participant.id + }; +} + +SharedVideoMenu_ = connect(_mapStateToProps)(SharedVideoMenu); + +export default SharedVideoMenu_; diff --git a/react/features/video-menu/components/native/index.js b/react/features/video-menu/components/native/index.js index 3ee816ff2..64ca607e3 100644 --- a/react/features/video-menu/components/native/index.js +++ b/react/features/video-menu/components/native/index.js @@ -8,4 +8,5 @@ export { default as MuteEveryonesVideoDialog } from './MuteEveryonesVideoDialog' export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog'; export { default as MuteRemoteParticipantsVideoDialog } from './MuteRemoteParticipantsVideoDialog'; export { default as RemoteVideoMenu } from './RemoteVideoMenu'; +export { default as SharedVideoMenu } from './SharedVideoMenu'; export { default as VolumeSlider } from './VolumeSlider';