Compare commits

...

6 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
fa25ffdf24 fix(i18n) fix some country names 2022-02-28 08:44:28 -06:00
Robert Pintilii
b069a3ccf6 fix(thumbnails) Revert local tile ratio (#11009) 2022-02-23 09:08:11 +02:00
Дамян Минков
829100412e feat: Handles hidden-from-recorder from jwt. (#10973)
* feat: Handles hidden-from-recorder from jwt.

Hides the participant that has this flag in jwt from the recorder. A hidden meeting moderator.
Makes sure follows me works and no tracks are being added.

* squash: Skips showing notification when disabling
local audio and video.

* squash: Fixes comments.

* squash: Updates with ljm changes.
2022-02-18 07:59:44 -06:00
Robert Pintilii
e68b27dd29 fix(context-menu) Add max height (#10965) 2022-02-17 09:43:34 +02:00
Robert Pintilii
a05d677439 fix(breakout-rooms) Hide non-working options inside breakout rooms (#10959)
When the local participant is a moderator and is in a breakout room hide the following:
- advanced moderation
- Ask to Unmute
- Grant Moderator
2022-02-17 09:43:23 +02:00
Robert Pintilii
984401419e fix(video-quality-label) Open dialog also on audio-only mode (#10957) 2022-02-17 09:43:06 +02:00
17 changed files with 138 additions and 45 deletions

View File

@@ -148,6 +148,7 @@ import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/Au
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
import { endpointMessageReceived } from './react/features/subtitles';
import { muteLocal } from './react/features/video-menu/actions.any';
import UIEvents from './service/UI/UIEvents';
const logger = Logger.getLogger(__filename);
@@ -1145,7 +1146,8 @@ export default {
* Used by Jibri to detect when it's alone and the meeting should be terminated.
*/
get membersCount() {
return room.getParticipants().filter(p => !p.isHidden()).length + 1;
return room.getParticipants()
.filter(p => !p.isHidden() || !(config.iAmRecorder && p.isHiddenFromRecorder())).length + 1;
},
/**
@@ -2064,6 +2066,10 @@ export default {
APP.store.dispatch(updateRemoteParticipantFeatures(user));
});
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
if (config.iAmRecorder && user.isHiddenFromRecorder()) {
return;
}
// The logic shared between RN and web.
commonUserJoinedHandling(APP.store, room, user);
@@ -2113,6 +2119,14 @@ export default {
return;
}
if (config.iAmRecorder) {
const participant = room.getParticipantById(track.getParticipantId());
if (participant.isHiddenFromRecorder()) {
return;
}
}
APP.store.dispatch(trackAdded(track));
});
@@ -2600,13 +2614,24 @@ export default {
* @returns {void}
*/
_onConferenceJoined() {
const { dispatch } = APP.store;
APP.UI.initConference();
if (!config.disableShortcuts) {
APP.keyboardshortcut.init();
}
APP.store.dispatch(conferenceJoined(room));
dispatch(conferenceJoined(room));
const jwt = APP.store.getState()['features/base/jwt'];
if (jwt?.user?.hiddenFromRecorder) {
dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
dispatch(muteLocal(true, MEDIA_TYPE.VIDEO));
dispatch(setAudioUnmutePermissions(true, true));
dispatch(setVideoUnmutePermissions(true, true));
}
},
/**

View File

@@ -1167,6 +1167,7 @@ var config = {
forceJVB121Ratio
forceTurnRelay
hiddenDomain
hiddenFromRecorderFeatureEnabled
ignoreStartMuted
websocketKeepAlive
websocketKeepAliveUrl

View File

@@ -91,7 +91,7 @@ var interfaceConfig = {
LANG_DETECTION: true, // Allow i18n to detect the system language
LIVE_STREAMING_HELP_LINK: 'https://jitsi.org/live', // Documentation reference for the live streaming feature.
LOCAL_THUMBNAIL_RATIO: 1, // 1:1
LOCAL_THUMBNAIL_RATIO: 16 / 9, // 16:9
/**
* Maximum coefficient of the ratio of the large video to the visible area

11
package-lock.json generated
View File

@@ -67,7 +67,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/1348-bafd6c7b/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2e869c83b8c7f4d8c27896819bbf87e4ad287371",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@@ -12003,8 +12003,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/1348-bafd6c7b/lib-jitsi-meet.tgz",
"integrity": "sha512-YMxEW1TA9tcURejYEEAafFGBB7Hz1KfelyGIDuLVpzeQeIgh+chqcsaCxQSWd+Vja/agJdJZ6S6sY64XTdnVfA==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#2e869c83b8c7f4d8c27896819bbf87e4ad287371",
"integrity": "sha512-jL6MjGlC9BaLT+O+kab3zPc2ubfcos++CZ6OKcDl8OfR3olUM+VSq/jZjkLVmnFlEwysLEcvGJeXRjgVdsuUwg==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -29109,8 +29109,9 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/1348-bafd6c7b/lib-jitsi-meet.tgz",
"integrity": "sha512-YMxEW1TA9tcURejYEEAafFGBB7Hz1KfelyGIDuLVpzeQeIgh+chqcsaCxQSWd+Vja/agJdJZ6S6sY64XTdnVfA==",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#2e869c83b8c7f4d8c27896819bbf87e4ad287371",
"integrity": "sha512-jL6MjGlC9BaLT+O+kab3zPc2ubfcos++CZ6OKcDl8OfR3olUM+VSq/jZjkLVmnFlEwysLEcvGJeXRjgVdsuUwg==",
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#2e869c83b8c7f4d8c27896819bbf87e4ad287371",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@@ -72,7 +72,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/1348-bafd6c7b/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2e869c83b8c7f4d8c27896819bbf87e4ad287371",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",

View File

@@ -67,6 +67,8 @@ type Props = {
onMouseLeave?: Function
};
const MAX_HEIGHT = 400;
const useStyles = makeStyles(theme => {
return {
contextMenu: {
@@ -80,7 +82,9 @@ const useStyles = makeStyles(theme => {
position: 'absolute',
right: `${participantsPaneTheme.panePadding}px`,
top: 0,
zIndex: 2
zIndex: 2,
maxHeight: `${MAX_HEIGHT}px`,
overflowY: 'auto'
},
contextMenuHidden: {
@@ -136,8 +140,9 @@ const ContextMenu = ({
const { current: container } = containerRef;
const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget;
const outerHeight = getComputedOuterHeight(container);
const height = Math.min(MAX_HEIGHT, outerHeight);
container.style.top = offsetTop + outerHeight > offsetHeight + scrollTop
container.style.top = offsetTop + height > offsetHeight + scrollTop
? `${offsetTop - outerHeight}`
: `${offsetTop}`;

View File

@@ -5,6 +5,7 @@ declare var APP: Object;
import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json';
import i18next from 'i18next';
import I18nextXHRBackend from 'i18next-xhr-backend';
import _ from 'lodash';
import LANGUAGES_RESOURCES from '../../../../lang/languages.json';
import MAIN_RESOURCES from '../../../../lang/main.json';
@@ -12,6 +13,20 @@ import MAIN_RESOURCES from '../../../../lang/main.json';
import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes';
import languageDetector from './languageDetector';
/**
* Override certain country names.
*/
const COUNTRIES_RESOURCES_OVERRIDES = {
countries: {
TW: 'Taiwan'
}
};
/**
* Merged country names.
*/
const COUNTRIES = _.merge({}, COUNTRIES_RESOURCES, COUNTRIES_RESOURCES_OVERRIDES);
/**
* The available/supported languages.
*
@@ -68,7 +83,7 @@ i18next
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'countries',
COUNTRIES_RESOURCES,
COUNTRIES,
/* deep */ true,
/* overwrite */ true);
i18next.addResourceBundle(

View File

@@ -220,10 +220,11 @@ function _undoOverwriteLocalParticipant(
* avatarURL: ?string,
* email: ?string,
* id: ?string,
* name: ?string
* name: ?string,
* hidden-from-recorder: ?boolean
* }}
*/
function _user2participant({ avatar, avatarUrl, email, id, name }) {
function _user2participant({ avatar, avatarUrl, email, id, name, 'hidden-from-recorder': hiddenFromRecorder }) {
const participant = {};
if (typeof avatarUrl === 'string') {
@@ -241,5 +242,9 @@ function _user2participant({ avatar, avatarUrl, email, id, name }) {
participant.name = name.trim();
}
if (hiddenFromRecorder === 'true' || hiddenFromRecorder === true) {
participant.hiddenFromRecorder = true;
}
return Object.keys(participant).length ? participant : undefined;
}

View File

@@ -65,12 +65,14 @@ export function setAudioMuted(muted: boolean, ensureTrack: boolean = false) {
* Action to disable/enable the audio mute icon.
*
* @param {boolean} blocked - True if the audio mute icon needs to be disabled.
* @param {boolean|undefined} skipNotification - True if we want to skip showing the notification.
* @returns {Function}
*/
export function setAudioUnmutePermissions(blocked: boolean) {
export function setAudioUnmutePermissions(blocked: boolean, skipNotification: boolean = false) {
return {
type: SET_AUDIO_UNMUTE_PERMISSIONS,
blocked
blocked,
skipNotification
};
}
@@ -155,12 +157,14 @@ export function setVideoMuted(
* Action to disable/enable the video mute icon.
*
* @param {boolean} blocked - True if the video mute icon needs to be disabled.
* @param {boolean|undefined} skipNotification - True if we want to skip showing the notification.
* @returns {Function}
*/
export function setVideoUnmutePermissions(blocked: boolean) {
export function setVideoUnmutePermissions(blocked: boolean, skipNotification: boolean = false) {
return {
type: SET_VIDEO_UNMUTE_PERMISSIONS,
blocked
blocked,
skipNotification
};
}

View File

@@ -86,12 +86,12 @@ MiddlewareRegistry.register(store => next => action => {
}
case SET_AUDIO_UNMUTE_PERMISSIONS: {
const { blocked } = action;
const { blocked, skipNotification } = action;
const state = store.getState();
const tracks = state['features/base/tracks'];
const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
if (blocked && isAudioMuted) {
if (blocked && isAudioMuted && !skipNotification) {
store.dispatch(showWarningNotification({
descriptionKey: 'notify.audioUnmuteBlockedDescription',
titleKey: 'notify.audioUnmuteBlockedTitle'
@@ -111,13 +111,13 @@ MiddlewareRegistry.register(store => next => action => {
}
case SET_VIDEO_UNMUTE_PERMISSIONS: {
const { blocked } = action;
const { blocked, skipNotification } = action;
const state = store.getState();
const tracks = state['features/base/tracks'];
const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
const isMediaShared = isScreenMediaShared(state);
if (blocked && isVideoMuted && !isMediaShared) {
if (blocked && isVideoMuted && !isMediaShared && !skipNotification) {
store.dispatch(showWarningNotification({
descriptionKey: 'notify.videoUnmuteBlockedDescription',
titleKey: 'notify.videoUnmuteBlockedTitle'

View File

@@ -102,16 +102,32 @@ function _onFollowMeCommand(attributes = {}, id, store) {
const participantSendingCommand = getParticipantById(state, id);
// The Command(s) API will send us our own commands and we don't want
// to act upon them.
if (participantSendingCommand.local) {
return;
}
if (participantSendingCommand) {
// The Command(s) API will send us our own commands and we don't want
// to act upon them.
if (participantSendingCommand.local) {
return;
}
if (participantSendingCommand.role !== 'moderator') {
logger.warn('Received follow-me command not from moderator');
if (participantSendingCommand.role !== 'moderator') {
logger.warn('Received follow-me command not from moderator');
return;
return;
}
} else {
// This is the case of jibri receiving commands from a hidden participant.
const { iAmRecorder } = state['features/base/config'];
const { conference } = state['features/base/conference'];
// As this participant is not stored in redux store we do the checks on the JitsiParticipant from lib-jitsi-meet
const participant = conference.getParticipantById(id);
if (!iAmRecorder || !participant || participant.getRole() !== 'moderator'
|| !participant.isHiddenFromRecorder()) {
logger.warn('Something went wrong with follow-me command');
return;
}
}
if (!isFollowMeActive(state)) {

View File

@@ -27,6 +27,7 @@ import {
getParticipantCount,
isEveryoneModerator
} from '../../../base/participants';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import {
SETTINGS_TABS,
openSettingsDialog,
@@ -89,6 +90,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
const participantCount = useSelector(getParticipantCount);
const isAudioModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.AUDIO));
const isVideoModerationEnabled = useSelector(isAvModerationEnabled(MEDIA_TYPE.VIDEO));
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const { t } = useTranslation();
@@ -144,7 +146,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
onClick: muteAllVideo,
text: t('participantsPane.actions.stopEveryonesVideo')
} ] } />
{isModerationSupported && (participantCount === 1 || !allModerators) && (
{!isBreakoutRoom && isModerationSupported && (participantCount === 1 || !allModerators) && (
<ContextMenuItemGroup actions = { actions }>
<div className = { classes.text }>
<span>{t('participantsPane.actions.allow')}</span>

View File

@@ -116,6 +116,11 @@ type Props = {
*/
isHighlighted: boolean,
/**
* Whether or not the local participant is in a breakout room.
*/
isInBreakoutRoom: boolean,
/**
* Callback used to open a confirmation dialog for audio muting.
*/
@@ -189,6 +194,7 @@ function MeetingParticipantItem({
_videoMediaState,
askUnmuteText,
isHighlighted,
isInBreakoutRoom,
muteAudio,
muteParticipantButtonText,
onContextMenu,
@@ -261,13 +267,15 @@ function MeetingParticipantItem({
{!overflowDrawer && !_participant?.isFakeParticipant
&& <>
<ParticipantQuickAction
askUnmuteText = { askToUnmuteText }
buttonType = { _quickActionButtonType }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }
participantID = { _participantID }
participantName = { _displayName } />
{!isInBreakoutRoom && (
<ParticipantQuickAction
askUnmuteText = { askToUnmuteText }
buttonType = { _quickActionButtonType }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }
participantID = { _participantID }
participantName = { _displayName } />
)}
<ParticipantActionEllipsis
accessibilityLabel = { participantActionEllipsisLabel }
onClick = { onContextMenu } />

View File

@@ -11,6 +11,11 @@ type Props = {
*/
askUnmuteText: string,
/**
* Whether or not the local participant is in a breakout room.
*/
isInBreakoutRoom: boolean,
/**
* Callback for the mouse leaving this item.
*/
@@ -34,7 +39,7 @@ type Props = {
/**
* The meeting participants.
*/
participantIds: Array<string>,
participantIds: Array<string>,
/**
* Callback used to open an actions drawer for a participant.
@@ -74,6 +79,7 @@ type Props = {
*/
function MeetingParticipantItems({
askUnmuteText,
isInBreakoutRoom,
lowerMenu,
toggleMenu,
muteAudio,
@@ -90,6 +96,7 @@ function MeetingParticipantItems({
<MeetingParticipantItem
askUnmuteText = { askUnmuteText }
isHighlighted = { raiseContextId === id }
isInBreakoutRoom = { isInBreakoutRoom }
key = { id }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }

View File

@@ -3,7 +3,7 @@
import { makeStyles } from '@material-ui/styles';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { rejectParticipantAudio } from '../../../av-moderation/actions';
import useContextMenu from '../../../base/components/context-menu/useContextMenu';
@@ -15,7 +15,7 @@ import {
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { normalizeAccents } from '../../../base/util/strings';
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { showOverflowDrawer } from '../../../toolbox/functions';
import { muteRemote } from '../../../video-menu/actions.any';
import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
@@ -92,6 +92,7 @@ function MeetingParticipants({
const youText = t('chat.you');
const askUnmuteText = t('participantsPane.actions.askUnmute');
const muteParticipantButtonText = t('dialog.muteParticipantButton');
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const styles = useStyles();
@@ -112,6 +113,7 @@ function MeetingParticipants({
<div>
<MeetingParticipantItems
askUnmuteText = { askUnmuteText }
isInBreakoutRoom = { isBreakoutRoom }
lowerMenu = { lowerMenu }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }

View File

@@ -12,7 +12,7 @@ import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/u
import { IconShareVideo } from '../../../base/icons';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { setVolume } from '../../../filmstrip/actions.web';
import { isForceMuted } from '../../../participants-pane/functions';
import { requestRemoteControl, stopController } from '../../../remote-control';
@@ -139,6 +139,7 @@ const ParticipantContextMenu = ({
const { participantsVolume } = useSelector(state => state['features/filmstrip']);
const _volume = (participant?.local ?? true ? undefined
: participant?.id ? participantsVolume[participant?.id] : undefined) || 1;
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const _currentRoomId = useSelector(getCurrentRoomId);
const _rooms = Object.values(useSelector(getBreakoutRooms));
@@ -209,7 +210,7 @@ const ParticipantContextMenu = ({
);
}
if (!disableGrantModerator) {
if (!disableGrantModerator && !isBreakoutRoom) {
buttons2.push(
<GrantModeratorButton
key = 'grant-moderator'

View File

@@ -64,7 +64,7 @@ export class VideoQualityLabel extends AbstractVideoQualityLabel<Props> {
return null;
}
let className, icon, labelContent, onClick, tooltipKey;
let className, icon, labelContent, tooltipKey;
if (_audioOnly) {
className = 'audio-only';
@@ -73,10 +73,10 @@ export class VideoQualityLabel extends AbstractVideoQualityLabel<Props> {
} else {
className = 'current-video-quality';
icon = IconGauge;
onClick = () => dispatch(openDialog(VideoQualityDialog));
tooltipKey = 'videoStatus.performanceSettings';
}
const onClick = () => dispatch(openDialog(VideoQualityDialog));
return (
<Tooltip
@@ -86,6 +86,7 @@ export class VideoQualityLabel extends AbstractVideoQualityLabel<Props> {
className = { className }
icon = { icon }
id = 'videoResolutionLabel'
// eslint-disable-next-line react/jsx-no-bind
onClick = { onClick }
text = { labelContent } />
</Tooltip>