Compare commits

...

18 Commits

Author SHA1 Message Date
Horatiu Muresan 738437776d fix(background-alpha) Fix setting background opacity 2022-04-04 16:33:50 +03:00
Avram Tudor 43beb627a2 fix(highlights) allow highlighting moments if recording is running (#11301) 2022-04-04 12:08:47 +03:00
Mihaela Dumitru bd994f2103
fix(salesforce) use salesforce only in the main room (#11286)
* fix(salesforce) use salesforce only in the main room (#11245)

* chore(deps) lib-jitsi-meet@latest (#11284)

https://github.com/jitsi/lib-jitsi-meet/compare/v1408.0.0+7e997fb7...v1409.0.0+88378583

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2022-04-04 08:24:41 +03:00
Robert Pintilii 6a3755f5d6 fix(thumbnails) Fix recalculate tile dimensions on client resize (#11267)
Recalculate after new dimensions are in the store
Fixes issue where on participant pane toggle the tiles would not recalculate correctly
# Conflicts:
#	react/features/filmstrip/middleware.web.js
2022-03-31 12:16:22 +03:00
Avram Tudor 97f355ec22 fix(highlight) set highlight button visibility based on record button… (#11215)
* fix(highlight) set highlight button visibility based on record button props

* code review

* code review

* code review

* code review
2022-03-31 10:32:18 +03:00
Avram Tudor 79fe929eb7 fix(highlight) implement custom notification for highlight start recording (#11217) 2022-03-31 10:32:07 +03:00
Avram Tudor ef2a490961 fix(face-centering) fix face centering on browsers with no offscreencanvas support (#11234) 2022-03-31 10:31:19 +03:00
Robert Pintilii 3bbba6f305 fix(context-menu) Don't overwrite hidden prop (#11265)
Fixes issue where the menu would disappear on dominant speaker change
2022-03-31 10:26:29 +03:00
Robert Pintilii 7a573fd580 feat(external-api) Add grantModerator command (#11199) 2022-03-31 10:25:11 +03:00
Jaya Allamsetty 1484e0fd0b Revert "fix(screenshare) Add and then mute the camera track after SS stops instead of not adding the track."
This workaround is not needed anymore since P2P is disabled between plan-b and unified-plan clients.
Fixes https://github.com/jitsi/jitsi-meet/issues/11131

This reverts commit c30038236a.
2022-03-29 11:14:19 -04:00
Saúl Ibarra Corretgé 2909176a73 fix(overlay) fix not showing the correct gUM helper text 2022-03-24 15:13:43 +01:00
Avram Tudor 64d3faa52a fix(highlight) fix notifications not disappearing (#11183) 2022-03-23 16:35:21 +02:00
Avram Tudor 842755674a fix(highlight) display option to start recording (#11146)
Fix incorrect handling of error case when highlighting moments
Allow users to start recording when trying to highlight while recording not started
2022-03-23 16:35:13 +02:00
Robert Pintilii c007a6194e fix(chat) Fix iOS web chat (#11193)
Fixes: on iOS web the chat input would move from the bottom when the keyboard was open
2022-03-22 11:08:16 -05:00
Robert Pintilii 71a8d7937c fix(settings-dialog) Fix crash (#11191)
Fixes crash when the participant becomes moderator while the dialog is open
2022-03-22 11:08:16 -05:00
Hristo Terezov 10785cb1c7 fix(shared-video): Can't click controls issue
The Dominant speaker name badge was overlaping the shared video
controls  (audio level, play/pause, etc).
2022-03-21 17:23:01 -05:00
Avram Tudor 5cdee7d989 fix(recording) fix incorrect condition for recording notification message (#11167) 2022-03-21 14:04:33 -05:00
Robert Pintilii b2d8a6115a fix(video-constraints) Fix calculations (#11161)
Only calculate for Large Video on the web (otherwise native breaks)
Take preferred max into calculations
2022-03-21 14:04:02 -05:00
34 changed files with 439 additions and 189 deletions

View File

@ -1637,29 +1637,32 @@ export default {
APP.store.dispatch(setScreenAudioShareState(false));
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
.then(([ stream ]) => {
logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`);
if (didHaveVideo && !ignoreDidHaveVideo) {
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
.then(([ stream ]) => {
logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`);
return this.useVideoStream(stream);
})
.catch(error => {
logger.error('failed to switch back to local video', error);
return this.useVideoStream(stream);
})
.catch(error => {
logger.error('failed to switch back to local video', error);
return this.useVideoStream(null).then(() =>
return this.useVideoStream(null).then(() =>
// Still fail with the original err
Promise.reject(error)
);
// Still fail with the original err
Promise.reject(error)
);
});
} else {
promise = promise.then(() => {
logger.debug('_turnScreenSharingOff using null for useVideoStream');
return this.useVideoStream(null);
});
}
return promise.then(
() => {
// Mute the video if camera video needs to be ignored or if video was muted before switching to screen
// share.
if (ignoreDidHaveVideo || !didHaveVideo) {
APP.store.dispatch(setVideoMuted(true, MEDIA_TYPE.VIDEO));
}
this.videoSwitchInProgress = false;
sendAnalytics(createScreenSharingEvent('stopped',
duration === 0 ? null : duration));

View File

@ -841,7 +841,7 @@
"raisedHandsLabel": "Number of raised hands",
"record": {
"already": {
"linked": "Record is already linked to this session."
"linked": "The meeting is already linked to this Salesforce object."
},
"type": {
"account": "Account",

View File

@ -39,7 +39,8 @@ import {
raiseHand,
isParticipantModerator,
isLocalParticipantModerator,
hasRaisedHand
hasRaisedHand,
grantModerator
} from '../../react/features/base/participants';
import { updateSettings } from '../../react/features/base/settings';
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
@ -164,6 +165,14 @@ function initCommands() {
}
APP.store.dispatch(autoAssignToBreakoutRooms());
},
'grant-moderator': participantId => {
if (!isLocalParticipantModerator(APP.store.getState())) {
logger.error('Missing moderator rights to grant moderator right to another participant');
return;
}
APP.store.dispatch(grantModerator(participantId));
},
'display-name': displayName => {
sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName);

View File

@ -38,7 +38,7 @@ const commands = {
displayName: 'display-name',
e2eeKey: 'e2ee-key',
email: 'email',
toggleLobby: 'toggle-lobby',
grantModerator: 'grant-moderator',
hangup: 'video-hangup',
initiatePrivateChat: 'initiate-private-chat',
joinBreakoutRoom: 'join-breakout-room',
@ -73,6 +73,7 @@ const commands = {
toggleChat: 'toggle-chat',
toggleE2EE: 'toggle-e2ee',
toggleFilmStrip: 'toggle-film-strip',
toggleLobby: 'toggle-lobby',
toggleModeration: 'toggle-moderation',
toggleParticipantsPane: 'toggle-participants-pane',
toggleRaiseHand: 'toggle-raise-hand',

View File

@ -108,13 +108,13 @@ UI.start = function() {
$('body').addClass('mobile-browser');
} else {
$('body').addClass('desktop-browser');
}
if (config.backgroundAlpha !== undefined) {
const backgroundColor = $('body').css('background-color');
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
if (config.backgroundAlpha !== undefined) {
const backgroundColor = $('body').css('background-color');
const alphaColor = setColorAlpha(backgroundColor, config.backgroundAlpha);
$('body').css('background-color', alphaColor);
}
$('body').css('background-color', alphaColor);
}
if (config.iAmRecorder) {

10
package-lock.json generated
View File

@ -73,7 +73,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/v1399.0.0+1a98d919/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1409.0.0+88378583/lib-jitsi-meet.tgz",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@ -11789,8 +11789,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1399.0.0+1a98d919/lib-jitsi-meet.tgz",
"integrity": "sha512-aIWaPY62nEZ9x13JDvv92UFhAvCSdC0ogCv4KpR1+Bwb6YmOPks+GHWpirZGk2RzgOL3cry43SDTVL4Tc0VyNA==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1409.0.0+88378583/lib-jitsi-meet.tgz",
"integrity": "sha512-c6WfdRVZl1cLl+oCKggMXk7yoYVTnvMU6kL8q9g3TtiNCocpBlh6HPLz9/A2DzLzCcyNio5KeNucyX7Ir3EXbA==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@ -28784,8 +28784,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1399.0.0+1a98d919/lib-jitsi-meet.tgz",
"integrity": "sha512-aIWaPY62nEZ9x13JDvv92UFhAvCSdC0ogCv4KpR1+Bwb6YmOPks+GHWpirZGk2RzgOL3cry43SDTVL4Tc0VyNA==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1409.0.0+88378583/lib-jitsi-meet.tgz",
"integrity": "sha512-c6WfdRVZl1cLl+oCKggMXk7yoYVTnvMU6kL8q9g3TtiNCocpBlh6HPLz9/A2DzLzCcyNio5KeNucyX7Ir3EXbA==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@ -78,7 +78,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/v1399.0.0+1a98d919/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1409.0.0+88378583/lib-jitsi-meet.tgz",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",

View File

@ -148,7 +148,7 @@ const ContextMenu = ({
setIsHidden(false);
} else {
setIsHidden(true);
hidden === undefined && setIsHidden(true);
}
}, [ entity, offsetTarget, _overflowDrawer ]);

View File

@ -15,7 +15,6 @@ import {
NOTIFICATION_TIMEOUT_TYPE,
showErrorNotification
} from '../../notifications';
import { showSalesforceNotification } from '../../salesforce';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
import { validateJwt } from '../jwt';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
@ -240,9 +239,6 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
dispatch(openDisplayNamePrompt(undefined));
}
dispatch(showSalesforceNotification());
return result;
}

View File

@ -199,7 +199,6 @@ class Chat extends AbstractChat<Props> {
<ChatInput
onResize = { this._onChatInputResize }
onSend = { this._onSendMessage } />
<KeyboardAvoider />
</div>
</>
);

View File

@ -222,12 +222,12 @@ class Conference extends AbstractConference<Props, *> {
id = 'layout_wrapper'
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove } >
onMouseMove = { this._onMouseMove }
ref = { this._setBackground }>
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }
ref = { this._setBackground }>
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
<ConferenceInfo />
<Notice />

View File

@ -12,6 +12,7 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { SET_REDUCED_UI } from '../base/responsive-ui';
import { FeedbackDialog } from '../feedback';
import { setFilmstripEnabled } from '../filmstrip';
import { showSalesforceNotification } from '../salesforce/actions';
import { setToolboxEnabled } from '../toolbox/actions';
import { notifyKickedOut } from './actions';
@ -21,13 +22,12 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED:
case SET_REDUCED_UI: {
const { dispatch, getState } = store;
const state = getState();
const { reducedUI } = state['features/base/responsive-ui'];
_conferenceJoined(store);
dispatch(setToolboxEnabled(!reducedUI));
dispatch(setFilmstripEnabled(!reducedUI));
break;
case SET_REDUCED_UI: {
_setReducedUI(store);
break;
}
@ -80,3 +80,37 @@ StateListenerRegistry.register(
}
}
});
/**
* Configures the UI. In reduced UI mode some components will
* be hidden if there is no space to render them.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @private
* @returns {void}
*/
function _setReducedUI({ dispatch, getState }) {
const { reducedUI } = getState()['features/base/responsive-ui'];
dispatch(setToolboxEnabled(!reducedUI));
dispatch(setFilmstripEnabled(!reducedUI));
}
/**
* Does extra sync up on properties that may need to be updated after the
* conference was joined.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @private
* @returns {void}
*/
function _conferenceJoined({ dispatch, getState }) {
_setReducedUI({
dispatch,
getState
});
dispatch(showSalesforceNotification());
}

View File

@ -22,6 +22,7 @@ const useStyles = makeStyles(theme => {
justifyContent: 'center',
marginBottom: theme.spacing(7),
transition: 'margin-bottom 0.3s',
pointerEvents: 'none',
position: 'absolute',
bottom: 0,
left: 0,

View File

@ -35,7 +35,7 @@ const queue = [];
let lastValidFaceBox;
const detect = async message => {
const { baseUrl, imageBitmap, isHorizontallyFlipped, threshold } = message.data;
const { baseUrl, image, isHorizontallyFlipped, threshold } = message.data;
if (initInProgress || initError) {
return;
@ -70,8 +70,8 @@ const detect = async message => {
tf.engine().startScope();
const image = tf.browser.fromPixels(imageBitmap);
const detections = await model.estimateFaces(image, false, isHorizontallyFlipped, false);
const imageTensor = tf.browser.fromPixels(image);
const detections = await model.estimateFaces(imageTensor, false, isHorizontallyFlipped, false);
tf.engine().endScope();
@ -80,10 +80,10 @@ const detect = async message => {
if (detections.length) {
faceBox = {
// normalize to percentage based
left: Math.round(Math.min(...detections.map(d => d.topLeft[0])) * 100 / imageBitmap.width),
right: Math.round(Math.max(...detections.map(d => d.bottomRight[0])) * 100 / imageBitmap.width),
top: Math.round(Math.min(...detections.map(d => d.topLeft[1])) * 100 / imageBitmap.height),
bottom: Math.round(Math.max(...detections.map(d => d.bottomRight[1])) * 100 / imageBitmap.height)
left: Math.round(Math.min(...detections.map(d => d.topLeft[0])) * 100 / image.width),
right: Math.round(Math.max(...detections.map(d => d.bottomRight[0])) * 100 / image.width),
top: Math.round(Math.min(...detections.map(d => d.topLeft[1])) * 100 / image.height),
bottom: Math.round(Math.max(...detections.map(d => d.bottomRight[1])) * 100 / image.height)
};
if (lastValidFaceBox && Math.abs(lastValidFaceBox.left - faceBox.left) < threshold) {

View File

@ -44,6 +44,7 @@ export async function sendDataToWorker(
}
let imageBitmap;
let image;
try {
imageBitmap = await imageCapture.grabFrame();
@ -53,13 +54,28 @@ export async function sendDataToWorker(
return;
}
if (typeof OffscreenCanvas === 'undefined') {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
context.drawImage(imageBitmap, 0, 0);
image = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
} else {
image = imageBitmap;
}
worker.postMessage({
id: DETECT_FACE_BOX,
baseUrl: getBaseUrl(),
imageBitmap,
image,
threshold,
isHorizontallyFlipped
});
imageBitmap.close();
}
/**

View File

@ -5,17 +5,10 @@ import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { CLIENT_RESIZED } from '../base/responsive-ui';
import { SETTINGS_UPDATED } from '../base/settings';
import {
getCurrentLayout,
LAYOUTS
} from '../video-layout';
import { SET_USER_FILMSTRIP_WIDTH } from './actionTypes';
import {
setFilmstripWidth,
setHorizontalViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
setFilmstripWidth
} from './actions';
import { DEFAULT_FILMSTRIP_WIDTH, MIN_STAGE_VIEW_WIDTH } from './constants';
import { updateRemoteParticipants, updateRemoteParticipantsOnLeave } from './functions';
@ -40,21 +33,6 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CLIENT_RESIZED: {
const state = store.getState();
const layout = getCurrentLayout(state);
switch (layout) {
case LAYOUTS.TILE_VIEW: {
store.dispatch(setTileViewDimensions());
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
store.dispatch(setHorizontalViewDimensions());
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
store.dispatch(setVerticalViewDimensions());
break;
}
if (isFilmstripResizable(state)) {
const { width: filmstripWidth } = state['features/filmstrip'];

View File

@ -51,8 +51,11 @@ StateListenerRegistry.register(
* Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view.
*/
StateListenerRegistry.register(
/* selector */ state => getCurrentLayout(state),
/* listener */ (layout, store) => {
/* selector */ state => {
return { layout: getCurrentLayout(state),
width: state['features/base/responsive-ui'].clientWidth };
},
/* listener */ ({ layout }, store) => {
switch (layout) {
case LAYOUTS.TILE_VIEW:
store.dispatch(setTileViewDimensions());
@ -64,6 +67,8 @@ StateListenerRegistry.register(
store.dispatch(setVerticalViewDimensions());
break;
}
}, {
deepEquals: true
});
/**

View File

@ -100,7 +100,7 @@ function mapStateToProps(state): Object {
const { premeetingBackground } = state['features/dynamic-branding'];
return {
...abstractMapStateToProps,
...abstractMapStateToProps(state),
_premeetingBackground: premeetingBackground
};
}

View File

@ -132,14 +132,13 @@ export function highlightMeetingMoment() {
return async (dispatch: Function, getState: Function) => {
dispatch(setHighlightMomentButtonState(true));
try {
await sendMeetingHighlight(getState());
const success = await sendMeetingHighlight(getState());
if (success) {
dispatch(showNotification({
descriptionKey: 'recording.highlightMomentSucessDescription',
titleKey: 'recording.highlightMomentSuccess'
}));
} catch (err) {
logger.error('Could not highlight meeting moment', err);
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
dispatch(setHighlightMomentButtonState(false));

View File

@ -1,17 +1,38 @@
// @flow
import { Component } from 'react';
import { batch } from 'react-redux';
import { getActiveSession, isHighlightMeetingMomentDisabled } from '../..';
import { openDialog } from '../../../base/dialog';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import {
hideNotification,
NOTIFICATION_TIMEOUT_TYPE,
NOTIFICATION_TYPE,
showNotification
} from '../../../notifications';
import { highlightMeetingMoment } from '../../actions.any';
import { StartRecordingDialog } from '../../components';
import { PROMPT_RECORDING_NOTIFICATION_ID } from '../../constants';
import { getRecordButtonProps } from '../../functions';
export type Props = {
/**
* Whether or not the conference is in audio only mode.
* Indicates whether or not the button is disabled.
*/
_audioOnly: boolean,
_disabled: boolean,
/**
* Indicates whether or not a highlight request is in progress.
*/
_isHighlightInProgress: boolean,
/**
* Indicates whether or not the button should be visible.
*/
_visible: boolean,
/**
* Invoked to obtain translated strings.
@ -24,7 +45,7 @@ export type Props = {
*/
export default class AbstractHighlightButton<P: Props> extends Component<P> {
/**
* Initializes a new AbstractVideoTrack instance.
* Initializes a new AbstractHighlightButton instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
@ -43,9 +64,27 @@ export default class AbstractHighlightButton<P: Props> extends Component<P> {
* @returns {void}
*/
_onClick() {
const { _disabled, dispatch } = this.props;
const { _disabled, _isHighlightInProgress, dispatch } = this.props;
if (!_disabled) {
if (_isHighlightInProgress) {
return;
}
if (_disabled) {
dispatch(showNotification({
descriptionKey: 'recording.highlightMomentDisabled',
titleKey: 'recording.highlightMoment',
uid: PROMPT_RECORDING_NOTIFICATION_ID,
customActionNameKey: [ 'localRecording.start' ],
customActionHandler: [ () => {
batch(() => {
dispatch(hideNotification(PROMPT_RECORDING_NOTIFICATION_ID));
dispatch(openDialog(StartRecordingDialog));
});
} ],
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
} else {
dispatch(highlightMeetingMoment());
}
}
@ -53,12 +92,14 @@ export default class AbstractHighlightButton<P: Props> extends Component<P> {
/**
* Maps (parts of) the Redux state to the associated
* {@code AbstractVideoQualityLabel}'s props.
* {@code AbstractHighlightButton}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean
* _disabled: boolean,
* _isHighlightInProgress: boolean,
* _visible: boolean
* }}
*/
export function _abstractMapStateToProps(state: Object) {
@ -66,8 +107,17 @@ export function _abstractMapStateToProps(state: Object) {
const isButtonDisabled = isHighlightMeetingMomentDisabled(state);
const { webhookProxyUrl } = state['features/base/config'];
const {
disabled: isRecordButtonDisabled,
visible: isRecordButtonVisible
} = getRecordButtonProps(state);
const canStartRecording = isRecordButtonVisible && !isRecordButtonDisabled;
const _visible = (canStartRecording || isRecordingRunning) && Boolean(webhookProxyUrl);
return {
_disabled: !isRecordingRunning || isButtonDisabled,
_visible: Boolean(webhookProxyUrl)
_disabled: !isRecordingRunning,
_isHighlightInProgress: isButtonDisabled,
_visible
};
}

View File

@ -6,16 +6,10 @@ import {
} from '../../../analytics';
import { IconToggleRecording } from '../../../base/icons';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
isLocalParticipantModerator
} from '../../../base/participants';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
import { FEATURES } from '../../../jaas/constants';
import { getActiveSession } from '../../functions';
import { getActiveSession, getRecordButtonProps } from '../../functions';
/**
* The type of the React {@code Component} props of
@ -131,57 +125,20 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
* {@code RecordButton} component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the Component.
* @private
* @returns {{
* _disabled: boolean,
* _isRecordingRunning: boolean,
* _tooltip: string,
* visible: boolean
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props): Object {
let { visible } = ownProps;
// a button can be disabled/enabled if enableFeaturesBasedOnToken
// is on or if the livestreaming is running.
let _disabled;
let _tooltip = '';
if (typeof visible === 'undefined') {
// If the containing component provides the visible prop, that is one
// above all, but if not, the button should be autonomus and decide on
// its own to be visible or not.
const isModerator = isLocalParticipantModerator(state);
const {
enableFeaturesBasedOnToken,
fileRecordingsEnabled
} = state['features/base/config'];
const { features = {} } = getLocalParticipant(state);
visible = isModerator && fileRecordingsEnabled;
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.recording) === 'true';
_disabled = String(features.recording) === 'disabled';
if (!visible && !_disabled) {
_disabled = true;
visible = true;
_tooltip = 'dialog.recordingDisabledTooltip';
}
}
}
// disable the button if the livestreaming is running.
if (getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
_disabled = true;
_tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
}
// disable the button if we are in a breakout room.
if (isInBreakoutRoom(state)) {
_disabled = true;
visible = false;
}
export function _mapStateToProps(state: Object): Object {
const {
disabled: _disabled,
tooltip: _tooltip,
visible
} = getRecordButtonProps(state);
return {
_disabled,

View File

@ -231,8 +231,7 @@ class StartRecordingDialogContent extends Component<Props> {
t
} = this.props;
if (isVpaas
|| selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE) {
if (!(isVpaas && selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE)) {
return null;
}

View File

@ -3,12 +3,14 @@
import { withStyles } from '@material-ui/core';
import React from 'react';
import { openDialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { IconHighlight } from '../../../../base/icons';
import { Label } from '../../../../base/label';
import { connect } from '../../../../base/redux';
import { Tooltip } from '../../../../base/tooltip';
import BaseTheme from '../../../../base/ui/components/BaseTheme';
import { StartRecordingDialog } from '../../../components';
import AbstractHighlightButton, {
_abstractMapStateToProps,
type Props as AbstractProps
@ -28,6 +30,17 @@ type Props = AbstractProps & {
_visible: boolean,
};
/**
* The type of the React {@code Component} state of {@link HighlightButton}.
*/
type State = {
/**
* Whether the notification which prompts for starting recording is open is not.
*/
isNotificationOpen: boolean
};
/**
* Creates the styles for the component.
*
@ -37,13 +50,36 @@ type Props = AbstractProps & {
*/
const styles = theme => {
return {
regular: {
background: theme.palette.field02,
margin: '0 4px 4px 4px'
container: {
position: 'relative'
},
disabled: {
background: theme.palette.text02,
margin: '0 4px 4px 4px'
},
regular: {
background: theme.palette.field02,
margin: '0 4px 4px 4px'
},
highlightNotification: {
backgroundColor: theme.palette.field02,
borderRadius: '6px',
boxShadow: '0px 6px 20px rgba(0, 0, 0, 0.25)',
boxSizing: 'border-box',
color: theme.palette.uiBackground,
fontSize: '14px',
fontWeight: '400',
left: '4px',
padding: '16px',
position: 'absolute',
top: '32px',
width: 320
},
highlightNotificationButton: {
color: theme.palette.field01Focus,
cursor: 'pointer',
fontWeight: '600',
marginTop: '8px'
}
};
};
@ -52,7 +88,82 @@ const styles = theme => {
* React {@code Component} responsible for displaying an action that
* allows users to highlight a meeting moment.
*/
export class HighlightButton extends AbstractHighlightButton<Props> {
export class HighlightButton extends AbstractHighlightButton<Props, State> {
/**
* Initializes a new HighlightButton instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
isNotificationOpen: false
};
this._onOpenDialog = this._onOpenDialog.bind(this);
this._onWindowClickListener = this._onWindowClickListener.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}.
*
* @inheritdoc
*/
componentDidMount() {
window.addEventListener('click', this._onWindowClickListener);
}
/**
* Implements React's {@link Component#componentWillUnmount()}.
*
* @inheritdoc
*/
componentWillUnmount() {
window.removeEventListener('click', this._onWindowClickListener);
}
/**
* Handles clicking / pressing the start recording button.
*
* @returns {void}
*/
_onOpenDialog() {
this.props.dispatch(openDialog(StartRecordingDialog));
}
/**
* Handles clicking / pressing the highlight button.
*
* @override
* @param {Event} e - The click event.
* @returns {void}
*/
_onClick(e) {
e.stopPropagation();
const { _disabled } = this.props;
if (_disabled) {
this.setState({
isNotificationOpen: true
});
} else {
super._onClick();
}
}
/**
* Window click event listener.
*
* @returns {void}
*/
_onWindowClickListener() {
this.setState({
isNotificationOpen: false
});
}
/**
* Implements React's {@link Component#render()}.
@ -77,16 +188,28 @@ export class HighlightButton extends AbstractHighlightButton<Props> {
const tooltipKey = _disabled ? 'recording.highlightMomentDisabled' : 'recording.highlightMoment';
return (
<Tooltip
content = { t(tooltipKey) }
position = { 'bottom' }>
<Label
className = { className }
icon = { IconHighlight }
iconColor = { _disabled ? BaseTheme.palette.text03 : BaseTheme.palette.field01 }
id = 'highlightMeetingLabel'
onClick = { this._onClick } />
</Tooltip>
<div className = { classes.container }>
<Tooltip
content = { t(tooltipKey) }
position = { 'bottom' }>
<Label
className = { className }
icon = { IconHighlight }
iconColor = { _disabled ? BaseTheme.palette.text03 : BaseTheme.palette.field01 }
id = 'highlightMeetingLabel'
onClick = { this._onClick } />
</Tooltip>
{this.state.isNotificationOpen && (
<div className = { classes.highlightNotification }>
{t('recording.highlightMomentDisabled')}
<div
className = { classes.highlightNotificationButton }
onClick = { this._onOpenDialog }>
{t('localRecording.start')}
</div>
</div>
)}
</div>
);
}
}

View File

@ -50,11 +50,7 @@ class RecordingButton extends AbstractRecordButton<Props> {
export function _mapStateToProps(state: Object, ownProps: Props): Object {
const abstractProps = _abstractMapStateToProps(state, ownProps);
const toolbarButtons = getToolbarButtons(state);
let { visible } = ownProps;
if (typeof visible === 'undefined') {
visible = toolbarButtons.includes('recording') && abstractProps.visible;
}
const visible = toolbarButtons.includes('recording') && abstractProps.visible;
return {
...abstractProps,

View File

@ -17,6 +17,13 @@ export const LIVE_STREAMING_OFF_SOUND_ID = 'LIVE_STREAMING_OFF_SOUND';
*/
export const LIVE_STREAMING_ON_SOUND_ID = 'LIVE_STREAMING_ON_SOUND';
/**
* The identifier of the prompt to start recording notification.
*
* @type {string}
*/
export const PROMPT_RECORDING_NOTIFICATION_ID = 'PROMPT_RECORDING_NOTIFICATION_ID';
/**
* The identifier of the sound to be played when a recording session is stopped.
*

View File

@ -1,7 +1,8 @@
// @flow
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant } from '../base/participants';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { isEnabled as isDropboxEnabled } from '../dropbox';
import { extractFqnFromPath } from '../dynamic-branding';
@ -119,6 +120,65 @@ export function getSessionStatusToShow(state: Object, mode: string): ?string {
return status;
}
/**
* Returns the recording button props.
*
* @param {Object} state - The redux state to search in.
*
* @returns {{
* disabled: boolean,
* tooltip: string,
* visible: boolean
* }}
*/
export function getRecordButtonProps(state: Object): ?string {
let visible;
// a button can be disabled/enabled if enableFeaturesBasedOnToken
// is on or if the livestreaming is running.
let disabled;
let tooltip = '';
// If the containing component provides the visible prop, that is one
// above all, but if not, the button should be autonomus and decide on
// its own to be visible or not.
const isModerator = isLocalParticipantModerator(state);
const {
enableFeaturesBasedOnToken,
fileRecordingsEnabled
} = state['features/base/config'];
const { features = {} } = getLocalParticipant(state);
visible = isModerator && fileRecordingsEnabled;
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.recording) === 'true';
disabled = String(features.recording) === 'disabled';
if (!visible && !disabled) {
disabled = true;
visible = true;
tooltip = 'dialog.recordingDisabledTooltip';
}
}
// disable the button if the livestreaming is running.
if (getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
disabled = true;
tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
}
// disable the button if we are in a breakout room.
if (isInBreakoutRoom(state)) {
disabled = true;
visible = false;
}
return {
disabled,
tooltip,
visible
};
}
/**
* Returns the resource id.

View File

@ -10,6 +10,7 @@ import {
} from '../notifications';
import { SalesforceLinkDialog } from './components';
import { isSalesforceEnabled } from './functions';
/**
* Displays the notification for linking the meeting to Salesforce.
@ -18,9 +19,7 @@ import { SalesforceLinkDialog } from './components';
*/
export function showSalesforceNotification() {
return (dispatch: Object, getState: Function) => {
const { salesforceUrl } = getState()['features/base/config'];
if (!salesforceUrl) {
if (!isSalesforceEnabled(getState())) {
return;
}

View File

@ -1,6 +1,21 @@
// @flow
import { doGetJSON } from '../base/util';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
/**
* Determines whether Salesforce is enabled for the current conference.
*
* @param {Function|Object} state - The redux store, the redux
* {@code getState} function, or the redux state itself.
* @returns {boolean}
*/
export const isSalesforceEnabled = (state: Function | Object) => {
const { salesforceUrl } = state['features/base/config'];
const isBreakoutRoom = isInBreakoutRoom(state);
return Boolean(salesforceUrl) && !isBreakoutRoom;
};
/**
* Fetches the Salesforce records that were most recently interacted with.

View File

@ -1,2 +0,0 @@
export * from './components';
export * from './actions';

View File

@ -200,10 +200,10 @@ function _mapStateToProps(state) {
return {
...newProps,
followMeEnabled: tabState.followMeEnabled,
startAudioMuted: tabState.startAudioMuted,
startVideoMuted: tabState.startVideoMuted,
startReactionsMuted: tabState.startReactionsMuted
followMeEnabled: tabState?.followMeEnabled,
startAudioMuted: tabState?.startAudioMuted,
startVideoMuted: tabState?.startVideoMuted,
startReactionsMuted: tabState?.startReactionsMuted
};
},
styles: 'settings-pane moderator-pane',
@ -242,11 +242,11 @@ function _mapStateToProps(state) {
return {
...newProps,
currentFramerate: tabState.currentFramerate,
currentLanguage: tabState.currentLanguage,
hideSelfView: tabState.hideSelfView,
showPrejoinPage: tabState.showPrejoinPage,
enabledNotifications: tabState.enabledNotifications
currentFramerate: tabState?.currentFramerate,
currentLanguage: tabState?.currentLanguage,
hideSelfView: tabState?.hideSelfView,
showPrejoinPage: tabState?.showPrejoinPage,
enabledNotifications: tabState?.enabledNotifications
};
},
styles: 'settings-pane more-pane',

View File

@ -8,6 +8,7 @@ import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
@ -39,7 +40,7 @@ class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps, *> {
*/
function mapStateToProps(state) {
return {
visible: Boolean(state['features/base/config'].salesforceUrl)
visible: isSalesforceEnabled(state)
};
}

View File

@ -6,7 +6,7 @@ import { translate } from '../../../base/i18n';
import { IconSalesforce } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { SalesforceLinkDialog } from '../../../salesforce';
import { SalesforceLinkDialog } from '../../../salesforce/components';
/**
* The type of the React {@code Component} props of {@link LinkToSalesforce}.

View File

@ -52,6 +52,7 @@ import {
LiveStreamButton,
RecordButton
} from '../../../recording';
import { isSalesforceEnabled } from '../../../salesforce/functions';
import {
isScreenAudioSupported,
isScreenVideoShared,
@ -1391,8 +1392,7 @@ function _mapStateToProps(state, ownProps) {
disableProfile,
enableFeaturesBasedOnToken,
iAmRecorder,
iAmSipGateway,
salesforceUrl
iAmSipGateway
} = state['features/base/config'];
const {
fullScreen,
@ -1441,7 +1441,7 @@ function _mapStateToProps(state, ownProps) {
_isIosMobile: isIosMobileBrowser(),
_isMobile: isMobileBrowser(),
_isVpaasMeeting: isVpaasMeeting(state),
_hasSalesforce: Boolean(salesforceUrl),
_hasSalesforce: isSalesforceEnabled(state),
_localParticipantID: localParticipant?.id,
_localVideo: localVideo,
_overflowMenuVisible: overflowMenuVisible,

View File

@ -279,14 +279,16 @@ function _updateReceiverVideoConstraints({ getState }) {
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
visibleRemoteTrackSourceNames.forEach(sourceName => {
receiverConstraints.constraints[sourceName] = { 'maxHeight': qualityLevel };
receiverConstraints.constraints[sourceName] = { 'maxHeight': Math.min(qualityLevel,
maxFrameHeight) };
});
}
if (largeVideoSourceName) {
let quality = maxFrameHeight;
if (!remoteScreenShares.find(id => id === largeVideoParticipantId)) {
if (navigator.product !== 'ReactNative'
&& !remoteScreenShares.find(id => id === largeVideoParticipantId)) {
quality = getVideoQualityForLargeVideo();
}
receiverConstraints.constraints[largeVideoSourceName] = { 'maxHeight': quality };
@ -326,14 +328,16 @@ function _updateReceiverVideoConstraints({ getState }) {
const qualityLevel = getVideoQualityForResizableFilmstripThumbnails(state);
visibleRemoteParticipants.forEach(participantId => {
receiverConstraints.constraints[participantId] = { 'maxHeight': qualityLevel };
receiverConstraints.constraints[participantId] = { 'maxHeight': Math.min(qualityLevel,
maxFrameHeight) };
});
}
if (largeVideoParticipantId) {
let quality = maxFrameHeight;
if (!remoteScreenShares.find(id => id === largeVideoParticipantId)) {
if (navigator.product !== 'ReactNative'
&& !remoteScreenShares.find(id => id === largeVideoParticipantId)) {
quality = getVideoQualityForLargeVideo();
}
receiverConstraints.constraints[largeVideoParticipantId] = { 'maxHeight': quality };