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 // @flow
import '../analytics/middleware'; import '../analytics/middleware';
import '../av-moderation/middleware';
import '../base/conference/middleware'; import '../base/conference/middleware';
import '../base/config/middleware'; import '../base/config/middleware';
import '../base/jwt/middleware'; import '../base/jwt/middleware';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,20 +2,29 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TouchableOpacity } from 'react-native'; import { TouchableOpacity, View } from 'react-native';
import { Text } from 'react-native-paper'; import { Divider, Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; 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 { openDialog, hideDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet'; import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import { import {
Icon, Icon,
IconCheck,
IconVideoOff IconVideoOff
} from '../../../base/icons'; } from '../../../base/icons';
import { import { MEDIA_TYPE } from '../../../base/media';
getLocalParticipant, import { getParticipantCount, isEveryoneModerator } from '../../../base/participants';
getParticipantCount
} from '../../../base/participants';
import MuteEveryonesVideoDialog import MuteEveryonesVideoDialog
from '../../../video-menu/components/native/MuteEveryonesVideoDialog'; from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
@ -24,21 +33,29 @@ import styles from './styles';
export const ContextMenuMore = () => { export const ContextMenuMore = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]); const cancel = useCallback(() => dispatch(hideDialog()), [ dispatch ]);
const { id } = useSelector(getLocalParticipant);
const participantsCount = useSelector(getParticipantCount);
const showSlidingView = participantsCount > 2;
const muteAllVideo = useCallback(() => const muteAllVideo = useCallback(() =>
dispatch(openDialog(MuteEveryonesVideoDialog, dispatch(openDialog(MuteEveryonesVideoDialog)),
{ exclude: [ id ] })),
[ dispatch ]); [ dispatch ]);
const { t } = useTranslation(); 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 ( return (
<BottomSheet <BottomSheet
addScrollViewPadding = { false } addScrollViewPadding = { false }
onCancel = { cancel } onCancel = { cancel }
showSlidingView = { showSlidingView } showSlidingView = { true }>
style = { styles.contextMenuMore }>
<TouchableOpacity <TouchableOpacity
onPress = { muteAllVideo } onPress = { muteAllVideo }
style = { styles.contextMenuItem }> style = { styles.contextMenuItem }>
@ -47,6 +64,48 @@ export const ContextMenuMore = () => {
src = { IconVideoOff } /> src = { IconVideoOff } />
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text> <Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
</TouchableOpacity> </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> </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 { import {
getLocalParticipant, getLocalParticipant,
getParticipantByIdOrUndefined, getParticipantByIdOrUndefined,
getParticipantDisplayName getParticipantDisplayName,
isParticipantModerator
} from '../../../base/participants'; } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { import {
@ -14,9 +15,8 @@ import {
isParticipantVideoMuted isParticipantVideoMuted
} from '../../../base/tracks'; } from '../../../base/tracks';
import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native'; import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
import { MEDIA_STATE } from '../../constants';
import type { MediaState } from '../../constants'; import type { MediaState } from '../../constants';
import { getParticipantAudioMediaState } from '../../functions'; import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
import ParticipantItem from './ParticipantItem'; import ParticipantItem from './ParticipantItem';
@ -39,9 +39,9 @@ type Props = {
_isFakeParticipant: boolean, _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. * True if the participant is the local participant.
@ -63,6 +63,11 @@ type Props = {
*/ */
_raisedHand: boolean, _raisedHand: boolean,
/**
* Media state for video.
*/
_videoMediaState: MediaState,
/** /**
* The redux dispatch function. * The redux dispatch function.
*/ */
@ -127,10 +132,11 @@ class MeetingParticipantItem extends PureComponent<Props> {
const { const {
_audioMediaState, _audioMediaState,
_displayName, _displayName,
_isVideoMuted, _isModerator,
_local, _local,
_participantID, _participantID,
_raisedHand _raisedHand,
_videoMediaState
} = this.props; } = this.props;
return ( return (
@ -138,11 +144,12 @@ class MeetingParticipantItem extends PureComponent<Props> {
audioMediaState = { _audioMediaState } audioMediaState = { _audioMediaState }
displayName = { _displayName } displayName = { _displayName }
isKnockingParticipant = { false } isKnockingParticipant = { false }
isModerator = { _isModerator }
local = { _local } local = { _local }
onPress = { this._onPress } onPress = { this._onPress }
participantID = { _participantID } participantID = { _participantID }
raisedHand = { _raisedHand } 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 localParticipantId = getLocalParticipant(state).id;
const participant = getParticipantByIdOrUndefined(state, participantID); const participant = getParticipantByIdOrUndefined(state, participantID);
const _isAudioMuted = isParticipantAudioMuted(participant, state); const _isAudioMuted = isParticipantAudioMuted(participant, state);
const isVideoMuted = isParticipantVideoMuted(participant, state); const _isVideoMuted = isParticipantVideoMuted(participant, state);
const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state); const audioMediaState = getParticipantAudioMediaState(participant, _isAudioMuted, state);
const videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
return { return {
_audioMediaState: audioMediaState, _audioMediaState: audioMediaState,
_displayName: getParticipantDisplayName(state, participant?.id), _displayName: getParticipantDisplayName(state, participant?.id),
_isAudioMuted, _isAudioMuted,
_isFakeParticipant: Boolean(participant?.isFakeParticipant), _isFakeParticipant: Boolean(participant?.isFakeParticipant),
_isVideoMuted: isVideoMuted, _isModerator: isParticipantModerator(participant),
_local: Boolean(participant?.local), _local: Boolean(participant?.local),
_localVideoOwner: Boolean(ownerId === localParticipantId), _localVideoOwner: Boolean(ownerId === localParticipantId),
_participantID: participant?.id, _participantID: participant?.id,
_raisedHand: Boolean(participant?.raisedHand) _raisedHand: Boolean(participant?.raisedHand),
_videoMediaState: videoMediaState
}; };
} }

View File

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

View File

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

View File

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

View File

@ -71,7 +71,6 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
* @param {Object} participant - The participant. * @param {Object} participant - The participant.
* @param {boolean} muted - The mute state of the participant. * @param {boolean} muted - The mute state of the participant.
* @param {Object} state - The redux state. * @param {Object} state - The redux state.
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
* @returns {MediaState} * @returns {MediaState}
*/ */
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) { 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 {Object} participant - The participant.
* @param {boolean} muted - The mute state of the participant. * @param {boolean} muted - The mute state of the participant.
* @param {Object} state - The redux state. * @param {Object} state - The redux state.
* @param {boolean} ignoreDominantSpeaker - Whether to ignore the dominant speaker state.
* @returns {MediaState} * @returns {MediaState}
*/ */
export function getParticipantVideoMediaState(participant: Object, muted: Boolean, state: Object) { 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 // @flow
import React from 'react'; import React from 'react';
import { Text } from 'react-native';
import { ConfirmDialog } from '../../../base/dialog'; import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n'; import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import AbstractGrantModeratorDialog import AbstractGrantModeratorDialog, { abstractMapStateToProps }
from '../AbstractGrantModeratorDialog'; from '../AbstractGrantModeratorDialog';
/** /**
@ -22,11 +23,15 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
return ( return (
<ConfirmDialog <ConfirmDialog
contentKey = 'dialog.grantModeratorDialog' contentKey = 'dialog.grantModeratorDialog'
onSubmit = { this._onSubmit } /> onSubmit = { this._onSubmit }>
<Text>
{`${this.props.t('dialog.grantModeratorDialog', { participantName: this.props.participantName })}`}
</Text>
</ConfirmDialog>
); );
} }
_onSubmit: () => boolean; _onSubmit: () => boolean;
} }
export default translate(connect()(GrantModeratorDialog)); export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

View File

@ -1,7 +1,8 @@
// @flow // @flow
import React from 'react'; 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 { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog'; import { ConfirmDialog } from '../../../base/dialog';
@ -12,6 +13,8 @@ import AbstractMuteEveryoneDialog, {
abstractMapStateToProps, abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog'; type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
import styles from './styles';
type Props = AbstractProps & { type Props = AbstractProps & {
/** /**
@ -28,6 +31,22 @@ type Props = AbstractProps & {
*/ */
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> { 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}. * Implements {@code Component#render}.
* *
@ -39,8 +58,23 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
okKey = 'dialog.muteParticipantButton' okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit } > onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }> <Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` } { `${this.props.title} \n\n ${this.state.content}` }
</Text> </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> </ConfirmDialog>
); );
} }

View File

@ -1,7 +1,8 @@
// @flow // @flow
import React from 'react'; 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 { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog'; import { ConfirmDialog } from '../../../base/dialog';
@ -12,6 +13,8 @@ import AbstractMuteEveryonesVideoDialog, {
abstractMapStateToProps, abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryonesVideoDialog'; type Props as AbstractProps } from '../AbstractMuteEveryonesVideoDialog';
import styles from './styles';
type Props = AbstractProps & { type Props = AbstractProps & {
/** /**
@ -24,10 +27,26 @@ type Props = AbstractProps & {
* A React Component with the contents for a dialog that asks for confirmation * A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants. * from the user before muting all remote participants.
* *
* @extends AbstractMuteEveryoneDialog * @extends AbstractMuteEveryonesVideoDialog
*/ */
class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> { 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}. * Implements {@code Component#render}.
* *
@ -39,8 +58,21 @@ class MuteEveryonesVideoDialog extends AbstractMuteEveryonesVideoDialog<Props> {
okKey = 'dialog.muteEveryonesVideoDialogOk' okKey = 'dialog.muteEveryonesVideoDialogOk'
onSubmit = { this._onSubmit } > onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }> <Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` } { `${this.props.title} \n\n ${this.state.content}` }
</Text> </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> </ConfirmDialog>
); );
} }

View File

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

View File

@ -67,5 +67,24 @@ export default createStyleSheet({
divider: { divider: {
backgroundColor: BaseTheme.palette.dividerColor 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%'
} }
}); });