feat(rn,av-moderation) updated advanced moderation on Native

Updated participants list to:
- show Moderator label
- show correct status icons (red for force muted)
- show participants in the right order

Updated moderation to:
- show moderation menu at all times
- make moderation options functional

Updated notifications:
- fixed raise hand to show name
- display moderator rights granted

Updated mute/ stop video for all dialogs to include moderation toggles

Added ask to unmute button

Fix comments on ask to unmute

Co-authored-by: robertpin <robert.pin9@gmail.com>
This commit is contained in:
robertpin 2021-09-22 17:05:42 +03:00 committed by GitHub
parent 703e43ecd7
commit c3dae1f6e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 327 additions and 71 deletions

View File

@ -1,6 +1,7 @@
// @flow
import '../analytics/middleware';
import '../av-moderation/middleware';
import '../base/conference/middleware';
import '../base/config/middleware';
import '../base/jwt/middleware';

View File

@ -1,7 +1,6 @@
// @flow
import '../authentication/middleware';
import '../av-moderation/middleware';
import '../base/devices/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';

View File

@ -2,6 +2,7 @@
import '../analytics/reducer';
import '../authentication/reducer';
import '../av-moderation/reducer';
import '../base/app/reducer';
import '../base/audio-only/reducer';
import '../base/color-scheme/reducer';

View File

@ -1,6 +1,5 @@
// @flow
import '../av-moderation/reducer';
import '../base/devices/reducer';
import '../e2ee/reducer';
import '../feedback/reducer';

View File

@ -546,6 +546,7 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
title: getParticipantDisplayName(state, participantId),
descriptionKey: 'notify.raisedHand',
raiseHandNotification: true,
concatText: true,
...action
}, NOTIFICATION_TIMEOUT * (shouldDisplayAllowAction ? 2 : 1)));
dispatch(playSound(RAISE_HAND_SOUND_ID));

View File

@ -217,7 +217,9 @@ class Thumbnail extends PureComponent<Props> {
const indicators = [];
if (renderModeratorIndicator) {
indicators.push(<View style = { styles.moderatorIndicatorContainer }>
indicators.push(<View
key = 'moderator-indicator'
style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View>);
}

View File

@ -12,6 +12,11 @@ export type Props = {
*/
appearance: string,
/**
* Whether or not the title and description should be concatenated.
*/
concatText?: boolean,
/**
* Callback invoked when the custom button is clicked.
*/

View File

@ -66,12 +66,17 @@ class Notification extends AbstractNotification<Props> {
* @private
*/
_renderContent() {
const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey } = this.props;
const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey, concatText } = this.props;
const titleText = title || (titleKey && t(titleKey, titleArguments));
const description = this._getDescription();
const titleConcat = [];
if (concatText) {
titleConcat.push(titleText);
}
if (description && description.length) {
return description.map((line, index) => (
return [ ...titleConcat, ...description ].map((line, index) => (
<Text
key = { index }
numberOfLines = { maxLines }

View File

@ -2,20 +2,29 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableOpacity } from 'react-native';
import { Text } from 'react-native-paper';
import { TouchableOpacity, View } from 'react-native';
import { Divider, Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import {
requestDisableAudioModeration,
requestDisableVideoModeration,
requestEnableAudioModeration,
requestEnableVideoModeration
} from '../../../av-moderation/actions';
import {
isSupported as isAvModerationSupported,
isEnabled as isAvModerationEnabled
} from '../../../av-moderation/functions';
import { openDialog, hideDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import {
Icon,
IconCheck,
IconVideoOff
} from '../../../base/icons';
import {
getLocalParticipant,
getParticipantCount
} from '../../../base/participants';
import { MEDIA_TYPE } from '../../../base/media';
import { getParticipantCount, isEveryoneModerator } from '../../../base/participants';
import MuteEveryonesVideoDialog
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
@ -24,21 +33,29 @@ import styles from './styles';
export const ContextMenuMore = () => {
const dispatch = useDispatch();
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
const { id } = useSelector(getLocalParticipant);
const participantsCount = useSelector(getParticipantCount);
const showSlidingView = participantsCount > 2;
const muteAllVideo = useCallback(() =>
dispatch(openDialog(MuteEveryonesVideoDialog,
{ exclude: [ id ] })),
dispatch(openDialog(MuteEveryonesVideoDialog)),
[ dispatch ]);
const { t } = useTranslation();
const isModerationSupported = useSelector(isAvModerationSupported());
const allModerators = useSelector(isEveryoneModerator);
const participantCount = useSelector(getParticipantCount);
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
const disableAudioModeration = useCallback(() => dispatch(requestDisableAudioModeration()), [ dispatch ]);
const disableVideoModeration = useCallback(() => dispatch(requestDisableVideoModeration()), [ dispatch ]);
const enableAudioModeration = useCallback(() => dispatch(requestEnableAudioModeration()), [ dispatch ]);
const enableVideoModeration = useCallback(() => dispatch(requestEnableVideoModeration()), [ dispatch ]);
return (
<BottomSheet
addScrollViewPadding = { false }
onCancel = { cancel }
showSlidingView = { showSlidingView }
style = { styles.contextMenuMore }>
showSlidingView = { true }>
<TouchableOpacity
onPress = { muteAllVideo }
style = { styles.contextMenuItem }>
@ -47,6 +64,48 @@ export const ContextMenuMore = () => {
src = { IconVideoOff } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
</TouchableOpacity>
{isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
<Divider style = { styles.divider } />
<View style = { styles.contextMenuItem }>
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.allow')}</Text>
</View>
{isAudioModerationEnabled
? <TouchableOpacity
onPress = { disableAudioModeration }
style = { styles.contextMenuItem }>
<Text style = { styles.contextMenuItemTextNoIcon }>
{t('participantsPane.actions.audioModeration')}
</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { enableAudioModeration }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconCheck } />
<Text style = { styles.contextMenuItemText }>
{t('participantsPane.actions.audioModeration')}
</Text>
</TouchableOpacity> }
{isVideoModerationEnabled
? <TouchableOpacity
onPress = { disableVideoModeration }
style = { styles.contextMenuItem }>
<Text style = { styles.contextMenuItemTextNoIcon }>
{t('participantsPane.actions.videoModeration')}
</Text>
</TouchableOpacity>
: <TouchableOpacity
onPress = { enableVideoModeration }
style = { styles.contextMenuItem }>
<Icon
size = { 24 }
src = { IconCheck } />
<Text style = { styles.contextMenuItemText }>
{t('participantsPane.actions.videoModeration')}
</Text>
</TouchableOpacity>}
</>}
</BottomSheet>
);
};

View File

@ -0,0 +1,10 @@
// @flow
import React from 'react';
import { Icon, IconHorizontalPoints } from '../../../base/icons';
const HorizontalDotsIcon = () => (<Icon
size = { 20 }
src = { IconHorizontalPoints } />);
export default HorizontalDotsIcon;

View File

@ -6,7 +6,8 @@ import { translate } from '../../../base/i18n';
import {
getLocalParticipant,
getParticipantByIdOrUndefined,
getParticipantDisplayName
getParticipantDisplayName,
isParticipantModerator
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import {
@ -14,9 +15,8 @@ import {
isParticipantVideoMuted
} from '../../../base/tracks';
import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
import { MEDIA_STATE } from '../../constants';
import type { MediaState } from '../../constants';
import { getParticipantAudioMediaState } from '../../functions';
import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
import ParticipantItem from './ParticipantItem';
@ -39,9 +39,9 @@ type Props = {
_isFakeParticipant: boolean,
/**
* True if the participant is video muted.
* Whether or not the user is a moderator.
*/
_isVideoMuted: boolean,
_isModerator: boolean,
/**
* True if the participant is the local participant.
@ -63,6 +63,11 @@ type Props = {
*/
_raisedHand: boolean,
/**
* Media state for video.
*/
_videoMediaState: MediaState,
/**
* The redux dispatch function.
*/
@ -127,10 +132,11 @@ class MeetingParticipantItem extends PureComponent<Props> {
const {
_audioMediaState,
_displayName,
_isVideoMuted,
_isModerator,
_local,
_participantID,
_raisedHand
_raisedHand,
_videoMediaState
} = this.props;
return (
@ -138,11 +144,12 @@ class MeetingParticipantItem extends PureComponent<Props> {
audioMediaState = { _audioMediaState }
displayName = { _displayName }
isKnockingParticipant = { false }
isModerator = { _isModerator }
local = { _local }
onPress = { this._onPress }
participantID = { _participantID }
raisedHand = { _raisedHand }
videoMediaState = { _isVideoMuted ? MEDIA_STATE.MUTED : MEDIA_STATE.UNMUTED } />
videoMediaState = { _videoMediaState } />
);
}
}
@ -161,19 +168,21 @@ function mapStateToProps(state, ownProps): Object {
const localParticipantId = getLocalParticipant(state).id;
const participant = getParticipantByIdOrUndefined(state, participantID);
const _isAudioMuted = isParticipantAudioMuted(participant, state);
const isVideoMuted = isParticipantVideoMuted(participant, state);
const _isVideoMuted = isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
return {
_audioMediaState: audioMediaState,
_displayName: getParticipantDisplayName(state, participant?.id),
_isAudioMuted,
_isFakeParticipant: Boolean(participant?.isFakeParticipant),
_isVideoMuted: isVideoMuted,
_isModerator: isParticipantModerator(participant),
_local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId),
_participantID: participant?.id,
_raisedHand: Boolean(participant?.raisedHand)
_raisedHand: Boolean(participant?.raisedHand),
_videoMediaState: videoMediaState
};
}

View File

@ -34,6 +34,11 @@ type Props = {
*/
isKnockingParticipant: boolean,
/**
* Whether or not the user is a moderator.
*/
isModerator?: boolean,
/**
* True if the participant is local.
*/
@ -69,6 +74,7 @@ function ParticipantItem({
children,
displayName,
isKnockingParticipant,
isModerator,
local,
onPress,
participantID,
@ -88,11 +94,14 @@ function ParticipantItem({
className = 'participant-avatar'
participantId = { participantID }
size = { 32 } />
<View style = { styles.participantNameContainer }>
<Text style = { styles.participantName }>
{ displayName }
</Text>
{ local ? <Text style = { styles.isLocal }>({t('chat.you')})</Text> : null }
<View style = { styles.participantDetailsContainer }>
<View style = { styles.participantNameContainer }>
<Text style = { styles.participantName }>
{ displayName }
</Text>
{ local ? <Text style = { styles.isLocal }>({t('chat.you')})</Text> : null }
</View>
{isModerator && <Text style = { styles.moderatorLabel }>{t('videothumbnail.moderator')}</Text>}
</View>
{
!isKnockingParticipant

View File

@ -7,10 +7,8 @@ import { Button } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { openDialog } from '../../../base/dialog';
import { Icon, IconHorizontalPoints } from '../../../base/icons';
import { JitsiModal } from '../../../base/modal';
import {
getParticipantCount,
isLocalParticipantModerator
} from '../../../base/participants';
import MuteEveryoneDialog
@ -18,6 +16,7 @@ import MuteEveryoneDialog
import { close } from '../../actions.native';
import { ContextMenuMore } from './ContextMenuMore';
import HorizontalDotsIcon from './HorizontalDotsIcon';
import LobbyParticipantList from './LobbyParticipantList';
import MeetingParticipantList from './MeetingParticipantList';
import styles from './styles';
@ -32,8 +31,6 @@ const ParticipantsPane = () => {
const openMoreMenu = useCallback(() => dispatch(openDialog(ContextMenuMore)), [ dispatch ]);
const closePane = useCallback(() => dispatch(close()), [ dispatch ]);
const isLocalModerator = useSelector(isLocalParticipantModerator);
const participantsCount = useSelector(getParticipantCount);
const showContextMenu = participantsCount > 2;
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
[ dispatch ]);
const { t } = useTranslation();
@ -55,21 +52,13 @@ const ParticipantsPane = () => {
labelStyle = { styles.muteAllLabel }
mode = 'contained'
onPress = { muteAll }
style = { showContextMenu ? styles.muteAllMoreButton : styles.muteAllButton } />
{
showContextMenu
&& <Button
/* eslint-disable-next-line react/jsx-no-bind */
icon = { () =>
(<Icon
size = { 20 }
src = { IconHorizontalPoints } />)
}
labelStyle = { styles.moreIcon }
mode = 'contained'
onPress = { openMoreMenu }
style = { styles.moreButton } />
}
style = { styles.muteAllMoreButton } />
<Button
icon = { HorizontalDotsIcon }
labelStyle = { styles.moreIcon }
mode = 'contained'
onPress = { openMoreMenu }
style = { styles.moreButton } />
</View>
}
</JitsiModal>

View File

@ -78,8 +78,7 @@ const contextMenuItem = {
display: 'flex',
flexDirection: 'row',
height: BaseTheme.spacing[7],
marginLeft: BaseTheme.spacing[3],
marginTop: BaseTheme.spacing[2]
marginLeft: BaseTheme.spacing[3]
};
/**
@ -137,12 +136,18 @@ export default {
width: '100%'
},
participantDetailsContainer: {
display: 'flex',
flexDirection: 'column',
width: '100%'
},
participantNameContainer: {
display: 'flex',
flexDirection: 'row',
overflow: 'hidden',
paddingLeft: BaseTheme.spacing[3],
width: '63%'
width: '100%'
},
participantName: {
@ -150,6 +155,13 @@ export default {
color: BaseTheme.palette.text01
},
moderatorLabel: {
color: BaseTheme.palette.text03,
alignSelf: 'flex-start',
paddingLeft: BaseTheme.spacing[3],
paddingTop: BaseTheme.spacing[1]
},
isLocal: {
alignSelf: 'center',
color: BaseTheme.palette.text01,
@ -249,11 +261,6 @@ export default {
marginLeft: 'auto'
},
contextMenuMore: {
backgroundColor: BaseTheme.palette.bottomSheet,
borderRadius: BaseTheme.shape.borderRadius
},
muteAllButton: {
...muteAllButton
},
@ -300,6 +307,11 @@ export default {
marginLeft: BaseTheme.spacing[3]
},
contextMenuItemTextNoIcon: {
...contextMenuItemText,
marginLeft: BaseTheme.spacing[6]
},
contextMenuItemName: {
color: BaseTheme.palette.text04,
flexShrink: 1,

View File

@ -71,7 +71,6 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
* @param {Object} participant - The participant.
* @param {boolean} muted - The mute state of the participant.
* @param {Object} state - The redux state.
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
* @returns {MediaState}
*/
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
@ -98,7 +97,6 @@ export function getParticipantAudioMediaState(participant: Object, muted: Boolea
* @param {Object} participant - The participant.
* @param {boolean} muted - The mute state of the participant.
* @param {Object} state - The redux state.
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
* @returns {MediaState}
*/
export function getParticipantVideoMediaState(participant: Object, muted: Boolean, state: Object) {

View File

@ -0,0 +1,65 @@
// @flow
import { approveParticipant } from '../../../av-moderation/actions';
import { translate } from '../../../base/i18n';
import { IconMicrophone } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getParticipantById, isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isForceMuted } from '../../../participants-pane/functions';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* 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<Props, *> {
accessibilityLabel = 'participantsPane.actions.askUnmute';
icon = IconMicrophone;
label = 'participantsPane.actions.askUnmute';
/**
* Handles clicking / pressing the button, and asks the participant to unmute.
*
* @private
* @returns {void}
*/
_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 {Props}
*/
function mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
const participant = getParticipantById(state, participantID);
return {
visible: isLocalParticipantModerator(state)
&& (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)
|| isForceMuted(participant, MEDIA_TYPE.VIDEO, state))
};
}
export default translate(connect(mapStateToProps)(AskUnmuteButton));

View File

@ -1,11 +1,12 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractGrantModeratorDialog
import AbstractGrantModeratorDialog, { abstractMapStateToProps }
from '../AbstractGrantModeratorDialog';
/**
@ -22,11 +23,15 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
return (
<ConfirmDialog
contentKey = 'dialog.grantModeratorDialog'
onSubmit = { this._onSubmit } />
onSubmit = { this._onSubmit }>
<Text>
{`${this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName })}`}
</Text>
</ConfirmDialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect()(GrantModeratorDialog));
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

View File

@ -1,7 +1,8 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { Text, View, Switch } from 'react-native';
import { Divider } from 'react-native-paper';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
@ -12,6 +13,8 @@ import AbstractMuteEveryoneDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
import styles from './styles';
type Props = AbstractProps & {
/**
@ -28,6 +31,22 @@ type Props = AbstractProps & {
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
/**
* Toggles advanced moderation switch.
*
* @returns {void}
*/
_onToggleModeration() {
this.setState(state => {
return {
audioModerationEnabled: !state.audioModerationEnabled,
content: this.props.t(state.audioModerationEnabled
? 'dialog.muteEveryoneDialog' : 'dialog.muteEveryoneDialogModerationOn'
)
};
});
}
/**
* Implements {@code Component#render}.
*
@ -39,8 +58,23 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
{ `${this.props.title} \n\n ${this.state.content}` }
</Text>
{this.props.exclude.length === 0 && <>
<Divider style = { styles.dividerWithSpacing } />
<View style = { styles.toggleContainer }>
<Text
style = {{
...this.props._dialogStyles.text,
...styles.toggleLabel
}}>
{this.props.t('dialog.moderationAudioLabel')}
</Text>
<Switch
onValueChange = { this._onToggleModeration }
value = { !this.state.audioModerationEnabled } />
</View>
</>}
</ConfirmDialog>
);
}

View File

@ -1,7 +1,8 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { Switch, Text, View } from 'react-native';
import { Divider } from 'react-native-paper';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
@ -12,6 +13,8 @@ import AbstractMuteEveryonesVideoDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryonesVideoDialog';
import styles from './styles';
type Props = AbstractProps & {
/**
@ -24,10 +27,26 @@ type Props = AbstractProps & {
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteEveryoneDialog
* @extends AbstractMuteEveryonesVideoDialog
*/
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
/**
* Toggles advanced moderation switch.
*
* @returns {void}
*/
_onToggleModeration() {
this.setState(state => {
return {
moderationEnabled: !state.moderationEnabled,
content: this.props.t(state.moderationEnabled
? 'dialog.muteEveryonesVideoDialog' : 'dialog.muteEveryonesVideoDialogModerationOn'
)
};
});
}
/**
* Implements {@code Component#render}.
*
@ -39,8 +58,21 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
okKey = 'dialog.muteEveryonesVideoDialogOk'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
{ `${this.props.title} \n\n ${this.state.content}` }
</Text>
{this.props.exclude.length === 0 && <>
<Divider style = { styles.dividerWithSpacing } />
<View style = { styles.toggleContainer }>
<Text
style = {{ ...this.props._dialogStyles.text,
...styles.toggleLabel }}>
{this.props.t('dialog.moderationVideoLabel')}
</Text>
<Switch
onValueChange = { this._onToggleModeration }
value = { !this.state.moderationEnabled } />
</View>
</>}
</ConfirmDialog>
);
}

View File

@ -18,6 +18,7 @@ import { PrivateMessageButton } from '../../../chat';
import { hideRemoteVideoMenu } from '../../actions.native';
import ConnectionStatusButton from '../native/ConnectionStatusButton';
import AskUnmuteButton from './AskUnmuteButton';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
@ -126,6 +127,7 @@ class RemoteVideoMenu extends PureComponent<Props> {
onCancel = { this._onCancel }
renderHeader = { this._renderMenuHeader }
showSlidingView = { _isParticipantAvailable }>
<AskUnmuteButton { ...buttonProps } />
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
<MuteEveryoneElseButton { ...buttonProps } />
{ !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }

View File

@ -67,5 +67,24 @@ export default createStyleSheet({
divider: {
backgroundColor: BaseTheme.palette.dividerColor
},
dividerWithSpacing: {
backgroundColor: BaseTheme.palette.dividerColor,
marginVertical: BaseTheme.spacing[3]
},
toggleContainer: {
display: 'flex',
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
width: '100%',
overflow: 'hidden'
},
toggleLabel: {
marginRight: BaseTheme.spacing[3],
maxWidth: '70%'
}
});