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
|
// @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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
&& <>
|
&& <>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
// @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));
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 } /> }
|
||||||
|
|
|
@ -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%'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue