Compare commits

...

21 Commits

Author SHA1 Message Date
Avram Tudor
7e5ffdb390 fix(face-centering) fix face centering on browsers with no offscreencanvas support (#11234) 2022-03-28 14:13:00 +03:00
gpatel-fr
9ea2b5836a fix(lang): update french translation 2022-03-27 21:15:40 +02:00
Jaya Allamsetty
7208e781b3 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1403.0.0+665ab174...v1406.0.0+dcd5f13b
2022-03-25 15:42:54 -04:00
Saúl Ibarra Corretgé
dc96324d3b fix(rn,recording) fix recording dialog state not updating 2022-03-25 17:22:32 +01:00
Aditya Gupta
0b93569df2 Increase the visibility of 404 error message (#11108)
Changed the font-size and color of the h2 element of the 404 error page so that it can look more vibrant and stand out.
2022-03-25 13:45:40 +02:00
Avram Tudor
c731e2f8cb 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-25 13:44:05 +02:00
Avram Tudor
b1e3f2b50d fix(highlight) implement custom notification for highlight start recording (#11217) 2022-03-25 13:43:51 +02:00
Gabriel Borlea
7751060b63 fix(facial-expressions): base url for models (#11218) 2022-03-25 13:43:32 +02:00
JohnProv
25f53503e8 fix(lang) update Dutch translation 2022-03-25 11:11:34 +01:00
Calin Chitu
8d737adc65 feat(participants/native) - updated container styles 2022-03-25 11:46:04 +02:00
Calinteodor
3fcfb64338 fix(rn,recording) fix start button not being enabled 2022-03-25 10:31:37 +01:00
Shawn
f8628dfeef feat(reservations) add integration with mod_muc_max_occupants 2022-03-24 08:38:34 -05:00
Christoph Settgast
39793110b6 conf(nginx) add keepalive via upstream groups
Incorporate https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/
into the example nginx config
2022-03-24 13:20:50 +01:00
Saúl Ibarra Corretgé
e5e7aa87d3 fix(overlay) fix not showing the correct gUM helper text 2022-03-24 13:19:43 +01:00
Tudor D. Pop
18fa56ff1b fix(lobby) display the entire message in the reject notification 2022-03-24 12:15:30 +01:00
Calin Chitu
ccf7939316 fix(mobile/navigation) - fixed bottom color glitch 2022-03-23 16:37:24 +02:00
Jaya Allamsetty
a518498bff chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1402.0.0+66ff07ce...v1403.0.0+665ab174
2022-03-23 09:57:41 -04:00
Robert Pintilii
9a78011daf feat(external-api) Add grantModerator command (#11199) 2022-03-23 11:31:44 +02:00
Jaya Allamsetty
35d0fefa1e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1401.0.0+584a8680...v1402.0.0+66ff07ce
2022-03-22 17:35:27 -04:00
bgrozev
b6792d9287 Update config.js with new e2eping properties. (#11195) 2022-03-22 14:12:29 -05:00
Robert Pintilii
cfb480932a 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 14:56:18 +02:00
30 changed files with 472 additions and 163 deletions

View File

@@ -950,14 +950,18 @@ var config = {
// Options related to end-to-end (participant to participant) ping.
// e2eping: {
// // The interval in milliseconds at which pings will be sent.
// // Defaults to 10000, set to <= 0 to disable.
// pingInterval: 10000,
// // Whether ene-to-end pings should be enabled.
// enabled: false,
//
// // The interval in milliseconds at which analytics events
// // with the measured RTT will be sent. Defaults to 60000, set
// // to <= 0 to disable.
// analyticsInterval: 60000,
// // The number of responses to wait for.
// numRequests: 5,
//
// // The max conference size in which e2e pings will be sent.
// maxConferenceSize: 200,
//
// // The maximum number of e2e ping messages per second for the whole conference to aim for.
// // This is used to contol the pacing of messages in order to reduce the load on the backend.
// maxMessagesPerSecond: 250
// },
// If set, will attempt to use the provided video input device label when

View File

@@ -4,11 +4,12 @@
text-align: center;
h2 {
font-size: 36px;
font-size: 48px;
color : #f2f2f2;
}
&__message {
font-size: 24px;
margin-top: 20px;
}
}
}

View File

@@ -4,6 +4,16 @@ types {
# nginx's default mime.types doesn't include a mapping for wasm
application/wasm wasm;
}
upstream prosody {
zone upstreams 64K;
server 127.0.0.1:5280;
keepalive 2;
}
upstream jvb1 {
zone upstreams 64K;
server 127.0.0.1:9090;
keepalive 2;
}
server {
listen 80;
listen [::]:80;
@@ -77,14 +87,16 @@ server {
# BOSH
location = /http-bind {
proxy_pass http://127.0.0.1:5280/http-bind?prefix=$prefix&$args;
proxy_pass http://prosody/http-bind?prefix=$prefix&$args;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header Connection "";
}
# xmpp websockets
location = /xmpp-websocket {
proxy_pass http://127.0.0.1:5280/xmpp-websocket?prefix=$prefix&$args;
proxy_pass http://prosody/xmpp-websocket?prefix=$prefix&$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -94,7 +106,7 @@ server {
# colibri (JVB) websockets for jvb1
location ~ ^/colibri-ws/default-id/(.*) {
proxy_pass http://127.0.0.1:9090/colibri-ws/default-id/$1$is_args$args;
proxy_pass http://jvb1/colibri-ws/default-id/$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

View File

@@ -39,9 +39,6 @@
"audioOnly": {
"audioOnly": "Bande passante faible"
},
"blankPage": {
"meetingEnded": "Réunion terminée."
},
"breakoutRooms": {
"actions": {
"add": "Ajouter salle annexe",
@@ -559,6 +556,7 @@
"errorMissingPassword": "Veuillez saisir le mot de passe de la réunion",
"invalidPassword": "Mot de passe invalide",
"joinRejectedMessage": "Votre requête pour rejoindre une réunion a été refusée par un modérateur.",
"joinRejectedTitle": "Demande d'accès rejetée.",
"joinTitle": "Rejoindre une réunion",
"joinWithPasswordMessage": "Tentative de rejoindre avec mot de passe, patientez s'il vous plait ...",
"joiningMessage": "Vous allez rejoindre une réunion dès que quelqu'un aura accepté votre demande",
@@ -865,6 +863,11 @@
"expandedPending": "Démarrage de l'enregistrement ...",
"failedToStart": "L'enregistrement n'a pas réussi à démarrer",
"fileSharingdescription": "Partager l'enregistrement avec les participants de la réunion",
"highlight": "Souligner",
"highlightMoment": "Souligner un moment",
"highlightMomentDisabled": "Vous ne pouvez souligner des moments que pendant une réunion",
"highlightMomentSuccess": "Moment souligné",
"highlightMomentSucessDescription": "Votre moment souligné sera ajouté au résumé de la réunion.",
"inProgress": "Enregistrement ou diffusion en cours",
"limitNotificationDescriptionNative": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <3> {{app}} </3>.",
"limitNotificationDescriptionWeb": "En raison d'une forte demande, votre enregistrement sera limité à {{limit}} min. Pour des enregistrements illimités, essayez <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",

View File

@@ -497,6 +497,7 @@
"errorMissingPassword": "Voer alstublieft het wachtwoord van de vergadering in",
"invalidPassword": "Ongeldig wachtwoord",
"joinRejectedMessage": "Uw verzoek tot deelname is afgewezen door een moderator.",
"joinRejectedTitle": "Verzoek tot deelname afgewezen",
"joinTitle": "Deelnemen aan vergadering",
"joinWithPasswordMessage": "Poging tot deelname met wachtwoord, even geduld a.u.b...",
"joiningMessage": "U neemt deel aan de vergadering zodra iemand uw verzoek accepteert",
@@ -889,6 +890,7 @@
"mute": "Audio dempen in- of uitschakelen",
"muteEveryone": "Iedereen dempen",
"muteEveryoneElse": "Alle anderen dempen",
"participants": "Deelnemers",
"pip": "Picture-in-Picture-modus in- of uitschakelen",
"privateMessage": "Verstuur privébericht",
"profile": "Uw profiel bewerken",
@@ -954,6 +956,8 @@
"noisyAudioInputDesc": "Het klinkt alsof uw microfoon ruis veroorzaakt. Overweeg te dempen of van apparaat te wisselen.",
"noisyAudioInputTitle": "Uw microfoon lijkt ruis te veroorzaken!",
"openChat": "Chat openen",
"openReactionsMenu": "Reactiemenu openen",
"participants": "Deelnemers",
"pip": "Picture-in-Picture-modus activeren",
"privateMessage": "Verstuur privébericht",
"profile": "Uw profiel bewerken",
@@ -1051,6 +1055,7 @@
},
"virtualBackground": {
"addBackground": "Achtergrond toevoegen",
"apply": "Toepassen",
"blur": "Vervagen",
"none": "Geen",
"pleaseWait": "Even geduld a.u.b...",

View File

@@ -556,6 +556,7 @@
"errorMissingPassword": "Please enter the meeting password",
"invalidPassword": "Invalid password",
"joinRejectedMessage": "Your join request was rejected by a moderator.",
"joinRejectedTitle": "Join request rejected.",
"joinTitle": "Join Meeting",
"joinWithPasswordMessage": "Trying to join with password, please wait...",
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",

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',

24
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/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1406.0.0+dcd5f13b/lib-jitsi-meet.tgz",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@@ -3571,8 +3571,8 @@
},
"node_modules/@jitsi/sdp-interop": {
"version": "1.0.5",
"resolved": "git+https://git@github.com/jitsi/sdp-interop.git#3707993863eb6c5b6d66c4a025e9dba193775bfb",
"integrity": "sha512-gqp3Pne45vlrLUyBgfTCw58zMflNripWb2Eaj0mF++U5uk2oRoo1/GBZu2C6Z42ExHfHfhFUFR3N8/Ss2LwnVw==",
"resolved": "git+https://git@github.com/jitsi/sdp-interop.git#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"integrity": "sha512-80u69QNTBArnCd1CGbTTrl/8AsZOOMF82dQhrgXBQAnrimdpomX1fMZ82ZkxyWyYvRMPG167u43Tp8y1g2DLNA==",
"license": "Apache-2.0",
"dependencies": {
"lodash.clonedeep": "4.5.0",
@@ -11789,13 +11789,13 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
"integrity": "sha512-1xMOjZ2hAwNiDm3UDgfGNoT8RwhCQJBYH1VjGvwmbDJTH5PC9WX89uZpBKoyD012GK5MdJv3oLrEr3g9tndfWw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1406.0.0+dcd5f13b/lib-jitsi-meet.tgz",
"integrity": "sha512-QqCUVulUv8Gi4VDixLYaaqRxOeq5vFrVbptakKaR3EgtJZ6T6sU+Db6FID/Ji5+XuI0UMKfFX1ni9+oQU32rNw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3707993863eb6c5b6d66c4a025e9dba193775bfb",
"@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "0.9.0",
"base64-js": "1.3.1",
@@ -22406,9 +22406,9 @@
}
},
"@jitsi/sdp-interop": {
"version": "git+https://git@github.com/jitsi/sdp-interop.git#3707993863eb6c5b6d66c4a025e9dba193775bfb",
"integrity": "sha512-gqp3Pne45vlrLUyBgfTCw58zMflNripWb2Eaj0mF++U5uk2oRoo1/GBZu2C6Z42ExHfHfhFUFR3N8/Ss2LwnVw==",
"from": "@jitsi/sdp-interop@https://git@github.com/jitsi/sdp-interop#3707993863eb6c5b6d66c4a025e9dba193775bfb",
"version": "git+https://git@github.com/jitsi/sdp-interop.git#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"integrity": "sha512-80u69QNTBArnCd1CGbTTrl/8AsZOOMF82dQhrgXBQAnrimdpomX1fMZ82ZkxyWyYvRMPG167u43Tp8y1g2DLNA==",
"from": "@jitsi/sdp-interop@https://git@github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"requires": {
"lodash.clonedeep": "4.5.0",
"sdp-transform": "2.14.1"
@@ -28784,12 +28784,12 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
"integrity": "sha512-1xMOjZ2hAwNiDm3UDgfGNoT8RwhCQJBYH1VjGvwmbDJTH5PC9WX89uZpBKoyD012GK5MdJv3oLrEr3g9tndfWw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1406.0.0+dcd5f13b/lib-jitsi-meet.tgz",
"integrity": "sha512-QqCUVulUv8Gi4VDixLYaaqRxOeq5vFrVbptakKaR3EgtJZ6T6sU+Db6FID/Ji5+XuI0UMKfFX1ni9+oQU32rNw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3707993863eb6c5b6d66c4a025e9dba193775bfb",
"@jitsi/sdp-interop": "https://git@github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "0.9.0",
"base64-js": "1.3.1",

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/v1401.0.0+584a8680/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1406.0.0+dcd5f13b/lib-jitsi-meet.tgz",
"libflacjs": "https://git@github.com/mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",

View File

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

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

@@ -67,8 +67,8 @@ export function loadWorker() {
return;
}
const baseUrl = getBaseUrl();
let workerUrl = `${baseUrl}libs/facial-expressions-worker.min.js`;
const baseUrl = `${getBaseUrl()}/libs/`;
let workerUrl = `${baseUrl}facial-expressions-worker.min.js`;
const workerBlob = new Blob([ `importScripts("${workerUrl}");` ], { type: 'application/javascript' });

View File

@@ -307,7 +307,8 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
showNotification({
appearance: NOTIFICATION_TYPE.ERROR,
hideErrorSupportLink: true,
titleKey: 'lobby.joinRejectedMessage'
titleKey: 'lobby.joinRejectedTitle',
descriptionKey: 'lobby.joinRejectedMessage'
}, NOTIFICATION_TIMEOUT_TYPE.LONG)
);
}

View File

@@ -1,5 +1,3 @@
import { StyleSheet } from 'react-native';
import { BoxModel } from '../../../base/styles';
import BaseTheme from '../../../base/ui/components/BaseTheme';
@@ -11,13 +9,12 @@ export const TEXT_COLOR = BaseTheme.palette.text01;
*/
export const navigationStyles = {
connectingScreenContainer: {
backgroundColor: BaseTheme.palette.uiBackground,
flex: 1
},
connectingScreenContent: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
backgroundColor: BaseTheme.palette.uiBackground,
flex: 1,
flexDirection: 'column',
justifyContent: 'center'

View File

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

View File

@@ -31,7 +31,7 @@ type Props = {
}
const CollapsibleList = ({ children, containerStyle, onLongPress, title }: Props) => {
const [ collapsed, setCollapsed ] = useState(true);
const [ collapsed, setCollapsed ] = useState(false);
const _toggleCollapsed = useCallback(() => {
setCollapsed(!collapsed);
}, [ collapsed ]);

View File

@@ -50,11 +50,21 @@ type Props = {
*/
_sortedRemoteParticipants: Map<string, string>,
/**
* List of breakout rooms that were created.
*/
breakoutRooms: Array,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* List of participants waiting in lobby.
*/
lobbyParticipants: Array,
/**
* Participants search string.
*/
@@ -180,6 +190,8 @@ class MeetingParticipantList extends PureComponent<Props> {
_participantsCount,
_showInviteButton,
_sortedRemoteParticipants,
breakoutRooms,
lobbyParticipants,
t
} = this.props;
const title = _currentRoom?.name
@@ -192,12 +204,19 @@ class MeetingParticipantList extends PureComponent<Props> {
// Regarding the fact that we have 3 sections, we apply
// a certain height percentage for every section in order for all to fit
// inside the participants pane container
// If there are only meeting participants available,
// we take the full container height
const onlyMeetingParticipants
= breakoutRooms.length === 0 && lobbyParticipants.length === 0;
const containerStyle
= _participantsCount > 3 && styles.meetingListContainer;
= onlyMeetingParticipants
? styles.meetingListFullContainer : styles.meetingListContainer;
const finalContainerStyle
= _participantsCount > 3 && containerStyle;
return (
<CollapsibleList
containerStyle = { containerStyle }
containerStyle = { finalContainerStyle }
title = { title } >
{
_showInviteButton

View File

@@ -19,6 +19,10 @@ import {
getCurrentRoomId,
isInBreakoutRoom
} from '../../../breakout-rooms/functions';
import {
getKnockingParticipants,
getLobbyEnabled
} from '../../../lobby/functions';
import MuteEveryoneDialog
from '../../../video-menu/components/native/MuteEveryoneDialog';
import {
@@ -63,11 +67,15 @@ const ParticipantsPane = () => {
&& participantsCount > 2 && rooms.length > 1;
const addBreakoutRoom
= _isBreakoutRoomsSupported && !hideAddRoomButton && isLocalModerator;
const lobbyEnabled = useSelector(getLobbyEnabled);
const lobbyParticipants = useSelector(getKnockingParticipants);
return (
<JitsiScreen style = { styles.participantsPaneContainer }>
<LobbyParticipantList />
<MeetingParticipantList
breakoutRooms = { _isBreakoutRoomsSupported && rooms }
lobbyParticipants = { lobbyEnabled && lobbyParticipants }
searchString = { searchString }
setSearchString = { setSearchString } />
{

View File

@@ -196,7 +196,7 @@ export default {
},
lobbyListContent: {
height: '20%'
height: '16%'
},
lobbyListDescription: {
@@ -217,7 +217,11 @@ export default {
},
meetingListContainer: {
height: '60%'
height: '56%'
},
meetingListFullContainer: {
height: '82%'
},
meetingListDescription: {

View File

@@ -15,6 +15,7 @@ import {
import { highlightMeetingMoment } from '../../actions.any';
import { StartRecordingDialog } from '../../components';
import { PROMPT_RECORDING_NOTIFICATION_ID } from '../../constants';
import { getRecordButtonProps } from '../../functions';
export type Props = {
@@ -44,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.
@@ -106,9 +107,14 @@ export function _abstractMapStateToProps(state: Object) {
const isButtonDisabled = isHighlightMeetingMomentDisabled(state);
const { webhookProxyUrl } = state['features/base/config'];
const {
disabled: isRecordButtonDisabled,
visible: isRecordButtonVisible
} = getRecordButtonProps(state);
return {
_disabled: !isRecordingRunning,
_isHighlightInProgress: isButtonDisabled,
_visible: Boolean(webhookProxyUrl)
_visible: isRecordButtonVisible && !isRecordButtonDisabled && Boolean(webhookProxyUrl)
};
}

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

@@ -379,10 +379,7 @@ class StartRecordingDialogContent extends Component<Props> {
<Container>
<Container
className = 'recording-header recording-header-line'
style = { [
styles.headerIntegrations,
_dialogStyles.topBorderContainer
] }>
style = { styles.headerIntegrations }>
<Container
className = 'recording-icon-container'>
<Image

View File

@@ -9,6 +9,7 @@ import HeaderNavigationButton
from '../../../../mobile/navigation/components/HeaderNavigationButton';
import { goBack } from
'../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { RECORDING_TYPES } from '../../../constants';
import AbstractStartRecordingDialog, {
type Props,
mapStateToProps
@@ -43,30 +44,38 @@ class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
* @returns {void}
*/
componentDidMount() {
const {
_fileRecordingsServiceEnabled,
_isDropboxEnabled,
navigation,
t
} = this.props;
super.componentDidMount();
const {
isTokenValid,
isValidating
} = this.state;
// disable start button id recording service is shown only, when
// validating dropbox token, if that is not enabled we either always
// show the start button or if just dropbox is enabled start is available
// when there is token
const isStartDisabled
= _fileRecordingsServiceEnabled ? isValidating
: _isDropboxEnabled ? !isTokenValid : false;
const { navigation, t } = this.props;
navigation.setOptions({
headerRight: () => (
<HeaderNavigationButton
disabled = { isStartDisabled }
disabled = { this.isStartRecordingDisabled() }
label = { t('dialog.start') }
onPress = { this._onStartPress }
twoActions = { true } />
)
});
}
/**
* Implements React's {@link Component#componentDidUpdate()}. Invoked
* immediately after this component is updated.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps) {
super.componentDidUpdate(prevProps);
const { navigation, t } = this.props;
navigation.setOptions({
// eslint-disable-next-line react/no-multi-comp
headerRight: () => (
<HeaderNavigationButton
disabled = { this.isStartRecordingDisabled() }
label = { t('dialog.start') }
onPress = { this._onStartPress }
twoActions = { true } />
@@ -85,6 +94,30 @@ class StartRecordingDialog extends AbstractStartRecordingDialog<Props> {
this._onSubmit() && goBack();
}
isStartRecordingDisabled: () => boolean;
/**
* Disables start recording button.
*
* @returns {boolean}
*/
isStartRecordingDisabled() {
const { isTokenValid, selectedRecordingService } = this.state;
// Start button is disabled if recording service is only shown;
// When validating dropbox token, if that is not enabled, we either always
// show the start button or, if just dropbox is enabled, start button
// is available when there is token.
if (selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE) {
return false;
} else if (selectedRecordingService === RECORDING_TYPES.DROPBOX) {
return !isTokenValid;
}
return true;
}
/**
* Implements React's {@link Component#render()}.
*

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

@@ -7,6 +7,7 @@ import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { toggleScreenshotCaptureSummary } from '../../../../screenshot-capture';
import { isScreenshotCaptureEnabled } from '../../../../screenshot-capture/functions';
import { RECORDING_TYPES } from '../../../constants';
import AbstractStartRecordingDialog, {
mapStateToProps as abstractMapStateToProps
} from '../AbstractStartRecordingDialog';
@@ -19,6 +20,30 @@ import StartRecordingDialogContent from '../StartRecordingDialogContent';
* @augments Component
*/
class StartRecordingDialog extends AbstractStartRecordingDialog {
isStartRecordingDisabled: () => boolean;
/**
* Disables start recording button.
*
* @returns {boolean}
*/
isStartRecordingDisabled() {
const { isTokenValid, selectedRecordingService } = this.state;
// Start button is disabled if recording service is only shown;
// When validating dropbox token, if that is not enabled, we either always
// show the start button or, if just dropbox is enabled, start button
// is available when there is token.
if (selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE) {
return false;
} else if (selectedRecordingService === RECORDING_TYPES.DROPBOX) {
return !isTokenValid;
}
return true;
}
/**
* Implements React's {@link Component#render()}.
*
@@ -33,19 +58,14 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
spaceLeft,
userName
} = this.state;
const { _fileRecordingsServiceEnabled, _fileRecordingsServiceSharingEnabled, _isDropboxEnabled } = this.props;
// disable ok button id recording service is shown only, when
// validating dropbox token, if that is not enabled we either always
// show the ok button or if just dropbox is enabled ok is available
// when there is token
const isOkDisabled
= _fileRecordingsServiceEnabled ? isValidating
: _isDropboxEnabled ? !isTokenValid : false;
const {
_fileRecordingsServiceEnabled,
_fileRecordingsServiceSharingEnabled
} = this.props;
return (
<Dialog
okDisabled = { isOkDisabled }
okDisabled = { this.isStartRecordingDisabled() }
okKey = 'dialog.startRecording'
onSubmit = { this._onSubmit }
titleKey = 'dialog.startRecording'

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

@@ -32,6 +32,9 @@
-- * set "reservations_api_should_retry_for_code" to a function that takes an HTTP response code and
-- returns true if API call should be retried. By default, retries are done for 5XX
-- responses. Timeouts are never retried, and HTTP call failures are always retried.
-- * set "reservations_enable_max_occupants" to true to enable integration with
-- mod_muc_max_occupants. Setting thia will allow optional "max_occupants"
-- payload from API to influence max occupants allowed for a given room.
--
--
-- Example config:
@@ -71,6 +74,7 @@ local api_headers = module:get_option("reservations_api_headers");
local api_timeout = module:get_option("reservations_api_timeout", 20);
local api_retry_count = tonumber(module:get_option("reservations_api_retry_count", 3));
local api_retry_delay = tonumber(module:get_option("reservations_api_retry_delay", 3));
local max_occupants_enabled = module:get_option("reservations_enable_max_occupants", false);
-- Option for user to control HTTP response codes that will result in a retry.
@@ -244,7 +248,7 @@ function RoomReservation:enqueue_or_route_event(event)
end
--- Updates status and initiates event routing. Called internally when API call complete.
function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id)
function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id, max_occupants)
module:log("info", "Reservation created successfully for %s", self.room_jid);
self.meta = {
status = STATUS.SUCCESS;
@@ -255,6 +259,9 @@ function RoomReservation:set_status_success(start_time, duration, mail_owner, co
error_text = nil;
error_code = nil;
}
if max_occupants_enabled and max_occupants then
self.meta.max_occupants = max_occupants
end
self:route_pending_events()
end
@@ -362,7 +369,7 @@ end
--- Parses and validates HTTP response body for conference payload
-- Ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
-- @return nil if invalid, or table with keys "id", "name", "mail_owner", "start_time", "duration".
-- @return nil if invalid, or table with payload parsed from JSON response
function RoomReservation:parse_conference_response(response_body)
local data = json.decode(response_body);
@@ -372,7 +379,7 @@ function RoomReservation:parse_conference_response(response_body)
end
if data.name == nil or data.name:lower() ~= self:get_room_name() then
module:log("error", "Missing or mismathing room name - %s", data.name);
module:log("error", "Missing or mismatching room name - %s", data.name);
return;
end
@@ -393,6 +400,17 @@ function RoomReservation:parse_conference_response(response_body)
end
data.duration = duration;
-- if optional max_occupants field set, cast to number
if data.max_occupants ~= nil then
local max_occupants = tonumber(data.max_occupants)
if max_occupants == nil or max_occupants < 1 then
-- N.B. invalid max_occupants rejected even if max_occupants_enabled=false
module:log("error", "Invalid value for max_occupants - %s", data.max_occupants);
return;
end
data.max_occupants = max_occupants
end
local start_time = datetime.parse(data.start_time); -- N.B. we lose milliseconds portion of the date
if start_time == nil then
module:log("error", "Missing or invalid start_time - %s", data.start_time);
@@ -440,7 +458,7 @@ function RoomReservation:handler_conference_data_returned_from_api(response_body
module:log("error", "API returned success code but invalid payload");
self:set_status_failed(500, 'Invalid response from reservation server');
else
self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id)
self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id, data.max_occupants)
end
end
@@ -573,11 +591,30 @@ local function room_destroyed(event)
end
end
--- If max_occupants_enabled, update room max_occupants if returned by API
local function room_created(event)
local res;
local room = event.room
if max_occupants_enabled and not is_healthcheck_room(room.jid) then
res = reservations[room.jid]
if res and res.meta.max_occupants ~= nil then
module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid);
room._data.max_occupants = res.meta.max_occupants
end
end
end
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("info", "Hook to muc events on %s", host);
module:log("info", "Hook to muc-room-destroyed on %s", host);
module:context(host):hook("muc-room-destroyed", room_destroyed, -1);
if max_occupants_enabled then
module:log("info", "Hook to muc-room-created on %s (mod_muc_max_occupants integration enabled)", host);
module:context(host):hook("muc-room-created", room_created);
end
end
end