diff --git a/react/features/app/middlewares.any.js b/react/features/app/middlewares.any.js index 1065721545..1c29fb7597 100644 --- a/react/features/app/middlewares.any.js +++ b/react/features/app/middlewares.any.js @@ -1,6 +1,7 @@ // @flow import '../analytics/middleware'; +import '../av-moderation/middleware'; import '../base/conference/middleware'; import '../base/config/middleware'; import '../base/jwt/middleware'; diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index e178a0eb4f..74702332ae 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -1,7 +1,6 @@ // @flow import '../authentication/middleware'; -import '../av-moderation/middleware'; import '../base/devices/middleware'; import '../e2ee/middleware'; import '../external-api/middleware'; diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js index 9eacb65caf..427e64a637 100644 --- a/react/features/app/reducers.any.js +++ b/react/features/app/reducers.any.js @@ -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'; diff --git a/react/features/app/reducers.web.js b/react/features/app/reducers.web.js index c934280001..5e65cd398b 100644 --- a/react/features/app/reducers.web.js +++ b/react/features/app/reducers.web.js @@ -1,6 +1,5 @@ // @flow -import '../av-moderation/reducer'; import '../base/devices/reducer'; import '../e2ee/reducer'; import '../feedback/reducer'; diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index f567cfdf3b..198a6202e6 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -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)); diff --git a/react/features/filmstrip/components/native/Thumbnail.js b/react/features/filmstrip/components/native/Thumbnail.js index 0dc5795b71..7046d9ae0e 100644 --- a/react/features/filmstrip/components/native/Thumbnail.js +++ b/react/features/filmstrip/components/native/Thumbnail.js @@ -217,7 +217,9 @@ class Thumbnail extends PureComponent { const indicators = []; if (renderModeratorIndicator) { - indicators.push( + indicators.push( ); } diff --git a/react/features/notifications/components/AbstractNotification.js b/react/features/notifications/components/AbstractNotification.js index 2dc4b898bb..a4d049853f 100644 --- a/react/features/notifications/components/AbstractNotification.js +++ b/react/features/notifications/components/AbstractNotification.js @@ -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. */ diff --git a/react/features/notifications/components/native/Notification.js b/react/features/notifications/components/native/Notification.js index 4e12cab5ce..6988d911f8 100644 --- a/react/features/notifications/components/native/Notification.js +++ b/react/features/notifications/components/native/Notification.js @@ -66,12 +66,17 @@ class Notification extends AbstractNotification { * @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) => ( { 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 ( + showSlidingView = { true }> @@ -47,6 +64,48 @@ export const ContextMenuMore = () => { src = { IconVideoOff } /> {t('participantsPane.actions.stopEveryonesVideo')} + {isModerationSupported && ((participantCount === 1 || !allModerators)) && <> + + + {t('participantsPane.actions.allow')} + + {isAudioModerationEnabled + ? + + {t('participantsPane.actions.audioModeration')} + + + : + + + {t('participantsPane.actions.audioModeration')} + + } + {isVideoModerationEnabled + ? + + {t('participantsPane.actions.videoModeration')} + + + : + + + {t('participantsPane.actions.videoModeration')} + + } + } ); }; diff --git a/react/features/participants-pane/components/native/HorizontalDotsIcon.js b/react/features/participants-pane/components/native/HorizontalDotsIcon.js new file mode 100644 index 0000000000..6da5e16bd7 --- /dev/null +++ b/react/features/participants-pane/components/native/HorizontalDotsIcon.js @@ -0,0 +1,10 @@ +// @flow +import React from 'react'; + +import { Icon, IconHorizontalPoints } from '../../../base/icons'; + +const HorizontalDotsIcon = () => (); + +export default HorizontalDotsIcon; diff --git a/react/features/participants-pane/components/native/MeetingParticipantItem.js b/react/features/participants-pane/components/native/MeetingParticipantItem.js index f90ee2f7f2..400ad954c2 100644 --- a/react/features/participants-pane/components/native/MeetingParticipantItem.js +++ b/react/features/participants-pane/components/native/MeetingParticipantItem.js @@ -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 { const { _audioMediaState, _displayName, - _isVideoMuted, + _isModerator, _local, _participantID, - _raisedHand + _raisedHand, + _videoMediaState } = this.props; return ( @@ -138,11 +144,12 @@ class MeetingParticipantItem extends PureComponent { 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 }; } diff --git a/react/features/participants-pane/components/native/ParticipantItem.js b/react/features/participants-pane/components/native/ParticipantItem.js index 2b852aec41..7a53624873 100644 --- a/react/features/participants-pane/components/native/ParticipantItem.js +++ b/react/features/participants-pane/components/native/ParticipantItem.js @@ -34,6 +34,11 @@ type Props = { */ isKnockingParticipant: boolean, + /** + * Whether or not the user is a moderator. + */ + isModerator?: boolean, + /** * True if the participant is local. */ @@ -69,6 +74,7 @@ function ParticipantItem({ children, displayName, isKnockingParticipant, + isModerator, local, onPress, participantID, @@ -88,11 +94,14 @@ function ParticipantItem({ className = 'participant-avatar' participantId = { participantID } size = { 32 } /> - - - { displayName } - - { local ? ({t('chat.you')}) : null } + + + + { displayName } + + { local ? ({t('chat.you')}) : null } + + {isModerator && {t('videothumbnail.moderator')}} { !isKnockingParticipant diff --git a/react/features/participants-pane/components/native/ParticipantsPane.js b/react/features/participants-pane/components/native/ParticipantsPane.js index 106a245ad9..3f85cd27c5 100644 --- a/react/features/participants-pane/components/native/ParticipantsPane.js +++ b/react/features/participants-pane/components/native/ParticipantsPane.js @@ -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 - &&