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:
parent
703e43ecd7
commit
c3dae1f6e9
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
|
||||
import '../analytics/middleware';
|
||||
import '../av-moderation/middleware';
|
||||
import '../base/conference/middleware';
|
||||
import '../base/config/middleware';
|
||||
import '../base/jwt/middleware';
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import '../authentication/middleware';
|
||||
import '../av-moderation/middleware';
|
||||
import '../base/devices/middleware';
|
||||
import '../e2ee/middleware';
|
||||
import '../external-api/middleware';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import '../av-moderation/reducer';
|
||||
import '../base/devices/reducer';
|
||||
import '../e2ee/reducer';
|
||||
import '../feedback/reducer';
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,12 +94,15 @@ function ParticipantItem({
|
|||
className = 'participant-avatar'
|
||||
participantId = { participantID }
|
||||
size = { 32 } />
|
||||
<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
|
||||
&& <>
|
||||
|
|
|
@ -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 } />)
|
||||
}
|
||||
style = { styles.muteAllMoreButton } />
|
||||
<Button
|
||||
icon = { HorizontalDotsIcon }
|
||||
labelStyle = { styles.moreIcon }
|
||||
mode = 'contained'
|
||||
onPress = { openMoreMenu }
|
||||
style = { styles.moreButton } />
|
||||
}
|
||||
</View>
|
||||
}
|
||||
</JitsiModal>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 } /> }
|
||||
|
|
|
@ -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%'
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue