fix(shared-video,video-menu) add ability to stop shared video from video menu

Specifically, in the bottom sheet (on mobile) and participants pane.
This commit is contained in:
Calinteodor 2021-08-04 11:51:05 +03:00 committed by GitHub
parent e421a119e1
commit 3c2ad24652
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 427 additions and 82 deletions

View File

@ -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,

View File

@ -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.
*

View File

@ -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 => (
<MeetingParticipantItem
key = { p.id }
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */
onPress = { () => p.local
? dispatch(showConnectionStatus(p.id)) : dispatch(showContextMenuDetails(p)) }
participantID = { p.id } />
);
const renderParticipant = p => {
if (p.isFakeParticipant) {
if (_localVideoOwner) {
return (
<MeetingParticipantItem
key = { p.id }
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */
onPress = { () => dispatch(showSharedVideoMenu(p)) }
participantID = { p.id } />
);
}
return (
<MeetingParticipantItem
key = { p.id }
participantID = { p.id } />
);
}
return (
<MeetingParticipantItem
key = { p.id }
/* eslint-disable-next-line react/jsx-no-bind,no-confusing-arrow */
onPress = { () => 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);

View File

@ -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';
/**

View File

@ -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';

View File

@ -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<Props, State> {
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<Props, State> {
}));
}
_onStopSharedVideo: () => void;
/**
* Stops shared video.
*
* @returns {void}
*/
_onStopSharedVideo() {
const { dispatch } = this.props;
dispatch(stopSharedVideo());
}
_onMuteEveryoneElse: () => void;
/**
@ -282,6 +304,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
_isParticipantModerator,
_isParticipantVideoMuted,
_isParticipantAudioMuted,
_localVideoOwner,
_participant,
onEnter,
onLeave,
@ -302,66 +325,81 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
onClick = { onSelect }
onMouseEnter = { onEnter }
onMouseLeave = { onLeave }>
<ContextMenuItemGroup>
{
_isLocalModerator && (
<>
{
!_participant.isFakeParticipant && (
<>
<ContextMenuItemGroup>
{
!_isParticipantAudioMuted
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
<ContextMenuIcon src = { IconMicDisabled } />
<span>{t('dialog.muteParticipantButton')}</span>
</ContextMenuItem>
_isLocalModerator && (
<>
{
!_isParticipantAudioMuted
&& <ContextMenuItem onClick = { muteAudio(_participant) }>
<ContextMenuIcon src = { IconMicDisabled } />
<span>{t('dialog.muteParticipantButton')}</span>
</ContextMenuItem>
}
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
<ContextMenuIcon src = { IconMuteEveryoneElse } />
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
</ContextMenuItem>
</>
)
}
<ContextMenuItem onClick = { this._onMuteEveryoneElse }>
<ContextMenuIcon src = { IconMuteEveryoneElse } />
<span>{t('toolbar.accessibilityLabel.muteEveryoneElse')}</span>
</ContextMenuItem>
</>
)
}
{
_isLocalModerator && (
_isParticipantVideoMuted || (
<ContextMenuItem onClick = { this._onMuteVideo }>
<ContextMenuIcon src = { IconVideoOff } />
<span>{t('participantsPane.actions.stopVideo')}</span>
</ContextMenuItem>
)
)
}
</ContextMenuItemGroup>
<ContextMenuItemGroup>
{
_isLocalModerator && (
<>
{
!_isParticipantModerator && (
<ContextMenuItem onClick = { this._onGrantModerator }>
<ContextMenuIcon src = { IconCrown } />
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
_isLocalModerator && (
_isParticipantVideoMuted || (
<ContextMenuItem onClick = { this._onMuteVideo }>
<ContextMenuIcon src = { IconVideoOff } />
<span>{t('participantsPane.actions.stopVideo')}</span>
</ContextMenuItem>
)
)
}
</ContextMenuItemGroup>
<ContextMenuItemGroup>
{
_isLocalModerator && (
<>
{
!_isParticipantModerator && (
<ContextMenuItem onClick = { this._onGrantModerator }>
<ContextMenuIcon src = { IconCrown } />
<span>{t('toolbar.accessibilityLabel.grantModerator')}</span>
</ContextMenuItem>
)
}
<ContextMenuItem onClick = { this._onKick }>
<ContextMenuIcon src = { IconCloseCircle } />
<span>{ t('videothumbnail.kick') }</span>
</ContextMenuItem>
</>
)
}
{
_isChatButtonEnabled && (
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
<ContextMenuIcon src = { IconMessage } />
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
</ContextMenuItem>
)
}
<ContextMenuItem onClick = { this._onKick }>
<ContextMenuIcon src = { IconCloseCircle } />
<span>{ t('videothumbnail.kick') }</span>
</ContextMenuItem>
</>
)
}
{
_isChatButtonEnabled && (
<ContextMenuItem onClick = { this._onSendPrivateMessage }>
<ContextMenuIcon src = { IconMessage } />
<span>{t('toolbar.accessibilityLabel.privateMessage')}</span>
</ContextMenuItem>
)
}
</ContextMenuItemGroup>
</ContextMenuItemGroup>
</>
)
}
{
_participant.isFakeParticipant && _localVideoOwner && (
<ContextMenuItem onClick = { this._onStopSharedVideo }>
<ContextMenuIcon src = { IconShareVideo } />
<span>{t('toolbar.stopSharedVideo')}</span>
</ContextMenuItem>
)
}
</ContextMenu>
);
}
@ -377,7 +415,8 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
*/
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
};
}

View File

@ -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 }>
<ParticipantQuickAction
askUnmuteText = { askUnmuteText }
buttonType = { _quickActionButtonType }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }
participantID = { _participantID } />
<ParticipantActionEllipsis
aria-label = { participantActionEllipsisLabel }
onClick = { onContextMenu } />
{
!_participant.isFakeParticipant && (
<>
<ParticipantQuickAction
askUnmuteText = { askUnmuteText }
buttonType = { _quickActionButtonType }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }
participantID = { _participantID } />
<ParticipantActionEllipsis
aria-label = { participantActionEllipsisLabel }
onClick = { onContextMenu } />
</>
)
}
{
_participant.isFakeParticipant && _localVideoOwner && (
<ParticipantActionEllipsis
aria-label = { participantActionEllipsisLabel }
onClick = { onContextMenu } />
)
}
</ParticipantItem>
);
}
@ -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)

View File

@ -235,7 +235,6 @@ export const ParticipantActionsHover = styled(ParticipantActions)`
position: absolute;
top: 0;
transform: translateX(-100%);
width: 40px;
}
`;

View File

@ -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';

View File

@ -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<Props> {
/**
* 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 (
<BottomSheet
onCancel = { this._onCancel }
renderHeader = { this._renderMenuHeader }
showSlidingView = { _isParticipantAvailable }>
<Divider style = { styles.divider } />
<SharedVideoButton { ...buttonProps } />
</BottomSheet>
);
}
_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<any>;
/**
* Function to render the menu's header.
*
* @returns {React$Element}
*/
_renderMenuHeader() {
const { _bottomSheetStyles, participant } = this.props;
return (
<View
style = { [
_bottomSheetStyles.sheet,
styles.participantNameContainer ] }>
<Avatar
participantId = { participant.id }
size = { AVATAR_SIZE } />
<Text style = { styles.participantNameLabel }>
{ 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, 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_;

View File

@ -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';