Compare commits

..

27 Commits

Author SHA1 Message Date
Gabriel Borlea
11f0ab9226 ref(screenshot-capture): move screenshot processing on a web worker (#14015)
Improvement for the screenshot capture feature by using a web worker to process the differences between the screenshots, and some code adjustments.
2023-11-09 10:01:02 +02:00
Hristo Terezov
0b6705610c chore(package.jsom): Update LJM 2023-11-08 10:26:55 -05:00
Hristo Terezov
9d9199ba3b fix(conference-hangup): Leave room in parallel.
Currently we are waiting for the user to submit feedback dialog in
order to leave the room. Now the leave and showing the dialog are
executed in parallel.
2023-11-08 09:16:58 -05:00
AHMAD KADRI
ff656a0625 ref(accessibility): add an aria-expended attribute if the the button open menu (#14023)
ref(accessibility): add an aria-expended attribute if the the button open menu
2023-11-08 15:59:05 +02:00
emrah
148fc103e3 fix(token): add jitsi_meet_context_room into the param list 2023-11-08 06:06:22 -06:00
Calinteodor
77abbee308 feat(base/modal): changed hasTabNavigator to hasExtraHeaderHeight (#14033)
* feat(base/modal): changed hasTabNavigator to hasExtraHeaderHeight
2023-11-08 11:44:10 +02:00
Calinteodor
83c4ce98b4 feat(filmstrip): fixed indicators container dissapear when not in tile view (#14031)
* feat(filmstrip): fixed indicators container dissapear when not in tile view
2023-11-07 23:33:28 +02:00
Calin-Teodor
898741e40d feat(filmstrip): fixed indicators container ui 2023-11-07 18:39:02 +02:00
Calin-Teodor
0c3e7395e7 feat(participants-pane): fixed visitors label position 2023-11-07 18:39:02 +02:00
Saúl Ibarra Corretgé
c530bdd107 feat(external_api) add event with transcription chunks 2023-11-07 13:10:00 +01:00
Horatiu Muresan
29dbcb309d fix(drawer-menu) Make drawer menu accessible on small height (#14026) 2023-11-07 13:06:03 +02:00
Calinteodor
8a4990d9ae sdk(react-native-sdk): rnsdk screenshare android fix (#13884)
sdk(react-native-sdk): rnsdk screenshare android fix
2023-11-07 12:22:02 +02:00
Дамян Минков
0e55cbbda6 Clean up prosody modules with some extra checks (#14020)
* fix: Adds check for jitsi_meet_room not being string.

Oct 20 12:22:50 mod_bosh        error   Traceback[bosh]: /usr/share/jitsi-meet/prosody-plugins/token/util.lib.lua:336: bad argument #1 to 'lower' (string expected, got userdata)
        stack traceback:
        [C]: in function 'lower'
        /usr/share/jitsi-meet/prosody-plugins/token/util.lib.lua:336: in function 'verify_room'
        ...re/jitsi-meet/prosody-plugins/mod_token_verification.lua:78: in function 'verify_user'

* fix: Adds check for missing speaker stats for occupant.

error   Traceback[c2s]: ...itsi-meet/prosody-plugins/mod_speakerstats_component.lua:124: attempt to index field '?' (a nil value)
        stack traceback:
        ...itsi-meet/prosody-plugins/mod_speakerstats_component.lua:124: in function '?'

* fix: Nil check for breakout_rooms.

c2saaaad95a16c0 error   Traceback[c2s]: ...re/jitsi-meet/prosody-plugins/mod_muc_breakout_rooms.lua:345: attempt to index local 'main_room' (a nil value)
        stack traceback:
        ...re/jitsi-meet/prosody-plugins/mod_muc_breakout_rooms.lua:345: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
        (...tail calls...)
        /usr/lib/prosody/modules/muc/muc.lib.lua:496: in function </usr/lib/prosody/modules/muc/muc.lib.lua:492>

* fix: Adds nil check in allowners.

c2saaaae3024810 error   Traceback[c2s]: /usr/share/jitsi-meet/prosody-plugins/mod_muc_allowners.lua:171: attempt to index local 'room' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_muc_allowners.lua:171: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>

* fix: Adds nil check in lobby.

mod_bosh        error   Traceback[bosh]: ...share/jitsi-meet/prosody-plugins/mod_muc_lobby_rooms.lua:168: attempt to index local 'lobby_room' (a nil value)
        stack traceback:
        ...share/jitsi-meet/prosody-plugins/mod_muc_lobby_rooms.lua:168: in function '?'
        /usr/share/lua/5.2/prosody/util/filters.lua:25: in function 'filter'
        /usr/lib/prosody/modules/mod_bosh.lua:361: in function 'send'
        /usr/lib/prosody/modules/muc/mod_muc.lua:495: in function '?'

* fix: Fixes nil error in fmuc.

s2sinaaaaf2817260       error   Traceback[s2s]: /usr/share/jitsi-meet/prosody-plugins/mod_fmuc.lua:295: attempt to index local 'occupant' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_fmuc.lua:295: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
        (...tail calls...)
        /usr/lib/prosody/modules/muc/muc.lib.lua:1201: in function </usr/lib/prosody/modules/muc/muc.lib.lua:1194>

* fix: Fixes nil occupant.

c2s55f4d5411dd0 error   Traceback[c2s]: /usr/share/jitsi-meet/prosody-plugins/mod_muc_flip.lua:120: attempt to index local 'kicked_occupant' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_muc_flip.lua:120: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
        (...tail calls...)
        /usr/lib/prosody/modules/muc/muc.lib.lua:791: in function </usr/lib/prosody/modules/muc/muc.lib.lua:616>

* fix: Fixes caching main room.

Objects should not be set in room._data as this field is being serialized and we see errors like.

error   Traceback[c2s]: /usr/share/lua/5.2/prosody/util/serialization.lua:34: Can't serialize userdata
        stack traceback:
        [C]: in function 'error'
        /usr/share/lua/5.2/prosody/util/serialization.lua:34: in function </usr/share/lua/5.2/prosody/util/serialization.lua:33>
        (...tail calls...)
        /usr/share/lua/5.2/prosody/util/serialization.lua:199: in function 'serialize_table'
        /usr/share/lua/5.2/prosody/util/serialization.lua:197: in function 'serialize_table'
        /usr/share/lua/5.2/prosody/util/serialization.lua:197: in function 'serialize_table'
        /usr/share/lua/5.2/prosody/util/serialization.lua:219: in function </usr/share/lua/5.2/prosody/util/serialization.lua:217>
        (...tail calls...)
        /usr/lib/prosody/modules/mod_storage_memory.lua:42: in function </usr/lib/prosody/modules/mod_storage_memory.lua:40>
        (...tail calls...)
        ...re/jitsi-meet/prosody-plugins/mod_muc_breakout_rooms.lua:207: in function 'create_breakout_room'

* fix: Fixes calling save_occupant after changing its role.

* squash: Fixed passed value to type.
2023-11-06 15:31:59 -06:00
damencho
6da94aecf2 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1718.0.0+30be4f6f...v1719.0.0+f8a18cf0
2023-11-06 09:40:48 -06:00
Calinteodor
2a3c962e88 feat(recent-list): fix undefined error that breaks visitor joining (#14024)
* feat(recent-list): fix undefined error that breaks visitor joining

* feat(recent-list): revert variable name change

* feat(recent-list): fixed linter
2023-11-06 09:40:28 -06:00
AHMAD KADRI
34f1eb60f4 Accessibility: add validation warning on room name (#14009)
feat(accessibility): add validation warning on room name
2023-11-06 10:59:51 +02:00
Jaya Allamsetty
4115ebe856 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1717.0.0+7b6ce949...v1718.0.0+30be4f6f
2023-11-02 15:36:11 -04:00
Horatiu Muresan
d7dadfc157 feat(facing-mode) add config for initial camera facing mode (#14013) 2023-11-02 16:20:38 +02:00
Erin Yuki Schlarb
2851eeeab3 fix: Make room_metadata Prosody module depend on the required jitsi_session module
Without this room_metadata will silently discard all room metadata client requests assuming that they didn’t come from Jitsi meet clients.

Fixes #14001
2023-11-01 17:06:26 -05:00
Muhammed Ajmal M
84d75f2ae8 fix(screen-sharing) Self view of SC sized correctly initially (#13992) 2023-11-01 18:32:34 +02:00
damencho
73b3309adf feat: Adds leave rate limit to muc_rate_limit. 2023-10-31 15:59:23 -05:00
Jaya Allamsetty
e2de06f60d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1716.0.0+93c167d3...v1717.0.0+7b6ce949
2023-10-31 14:35:31 -04:00
damencho
cdc7962d11 feat: Adds region parameter to dial out authorize requests. 2023-10-31 11:45:06 -05:00
Saúl Ibarra Corretgé
59242e1217 feat(external-api) introduce a "ready" event
It's fired when the API is ready, and it signals the embedder that they
can reveal the meeting from behind an overlay, for example.

The astute reader might notice we are currently sending a
'browser-support' event roughly at the same time. The reason for this
new event is plain simply semantics.

In addition the 'onload' handler is faked by calling it when the new
ready event fires. The original onload event is unreliable. It will be
called even when nothing was ever loaded (try loading a page without
internet and be amused).
2023-10-31 16:27:12 +01:00
Saúl Ibarra Corretgé
631e39d4fd feat(external-api) allow vh and vw values as parameters 2023-10-31 16:27:12 +01:00
Julian LADJANI
4290cdf53d fix(breakout-rooms, feature-flags): handle breakout button feature flag on participant pane footer component (#14003)
* fix(breakout-rooms, feature-flags): handle breakout button feature flag on participant pane footer component
2023-10-31 13:53:41 +02:00
damencho
84c1e20216 fix(moderated): Fixes moderators in moderated rooms without tenant. 2023-10-30 17:26:42 -05:00
58 changed files with 733 additions and 1030 deletions

View File

@@ -55,6 +55,8 @@ deploy-appbundle:
$(BUILD_DIR)/face-landmarks-worker.min.js.map \
$(BUILD_DIR)/noise-suppressor-worklet.min.js \
$(BUILD_DIR)/noise-suppressor-worklet.min.js.map \
$(BUILD_DIR)/screenshot-capture-worker.min.js \
$(BUILD_DIR)/screenshot-capture-worker.min.js.map \
$(DEPLOY_DIR)
cp \
$(BUILD_DIR)/close3.min.js \

View File

@@ -26,5 +26,5 @@ android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
appVersion=23.6.0
sdkVersion=8.6.0
appVersion=99.0.0
sdkVersion=99.0.0

View File

@@ -1061,13 +1061,6 @@ export default {
return room.getSpeakerStats();
},
/**
* Returns the connection times stored in the library.
*/
getConnectionTimes() {
return room.getConnectionTimes();
},
// used by torture currently
isJoined() {
return room && room.isJoined();
@@ -2445,7 +2438,7 @@ export default {
* @param {string} [hangupReason] the reason for leaving the meeting
* requested
*/
async hangup(requestFeedback = false, hangupReason) {
hangup(requestFeedback = false, hangupReason) {
APP.store.dispatch(disableReceiver());
this._stopProxyConnection();
@@ -2462,33 +2455,42 @@ export default {
APP.UI.removeAllListeners();
let feedbackResult = {};
let feedbackResultPromise = Promise.resolve({});
if (requestFeedback) {
try {
feedbackResult = await APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason));
} catch (err) { // eslint-disable-line no-empty
const feedbackDialogClosed = (feedbackResult = {}) => {
if (!feedbackResult.wasDialogShown && hangupReason) {
return APP.store.dispatch(
openLeaveReasonDialog(hangupReason)).then(() => feedbackResult);
}
return Promise.resolve(feedbackResult);
};
feedbackResultPromise
= APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason))
.then(feedbackDialogClosed, feedbackDialogClosed);
}
const leavePromise = this.leaveRoom().catch(() => Promise.resolve());
Promise.allSettled([ feedbackResultPromise, leavePromise ]).then(([ feedback, _ ]) => {
this._room = undefined;
room = undefined;
/**
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
* and let the page take care of sending the message, since there will be
* a redirect to the page anyway.
*/
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
APP.API.notifyReadyToClose();
}
}
if (!feedbackResult.wasDialogShown && hangupReason) {
await APP.store.dispatch(openLeaveReasonDialog(hangupReason));
}
APP.store.dispatch(maybeRedirectToWelcomePage(feedback.value ?? {}));
});
await this.leaveRoom();
this._room = undefined;
room = undefined;
/**
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
* and let the page take care of sending the message, since there will be
* a redirect to the page anyway.
*/
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
APP.API.notifyReadyToClose();
}
APP.store.dispatch(maybeRedirectToWelcomePage(feedbackResult));
},
/**
@@ -2498,7 +2500,7 @@ export default {
* @param {string} reason - reason for leaving the room.
* @returns {Promise}
*/
async leaveRoom(doDisconnect = true, reason = '') {
leaveRoom(doDisconnect = true, reason = '') {
APP.store.dispatch(conferenceWillLeave(room));
const maybeDisconnect = () => {

View File

@@ -218,6 +218,9 @@ var config = {
// Video
// Sets the default camera facing mode.
// cameraFacingMode: 'user',
// Sets the preferred resolution (height) for local video. Defaults to 720.
// resolution: 720,

View File

@@ -61,6 +61,35 @@ body.welcome-page {
}
.not-allow-title-character-div {
color: #f03e3e;
background-color: #fff;
font-size: 12px;
font-weight: 600;
margin: 10px 0px 5px 0px;
text-align: $welcomePageHeaderTextAlign;
border-radius: 5px;
padding: 5px;
.not-allow-title-character-text {
float: right;
line-height: 1.9;
};
.jitsi-icon {
margin-right: 9px;
float: left;
svg {
fill:#f03e3e;
& > *:first-child {
fill: none !important;
}
}
}
}
.insecure-room-name-warning {
align-items: center;
color: rgb(215, 121, 118);

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>23.6.0</string>
<string>99.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>23.6.0</string>
<string>99.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>23.6.0</string>
<string>99.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>23.6.0</string>
<string>99.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>8.6.0</string>
<string>99.0.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>8.6.0</string>
<string>99.0.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -1107,7 +1107,11 @@ class API {
this._enabled = true;
initCommands();
this.notifyBrowserSupport(isSupportedBrowser());
// Let the embedder know we are ready.
this._sendEvent({ name: 'ready' });
}
/**
@@ -1983,6 +1987,20 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the user received
* a transcription chunk.
*
* @param {Object} data - The event data.
* @returns {void}
*/
notifyTranscriptionChunkReceived(data) {
this._sendEvent({
name: 'transcription-chunk-received',
data
});
}
/**
* Notify external application (if API is enabled) whether the used browser is supported or not.
*

View File

@@ -145,6 +145,7 @@ const events = {
'prejoin-screen-loaded': 'prejoinScreenLoaded',
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
'ready': 'ready',
'recording-link-available': 'recordingLinkAvailable',
'recording-status-changed': 'recordingStatusChanged',
'participant-menu-button-clicked': 'participantMenuButtonClick',
@@ -159,6 +160,7 @@ const events = {
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged',
'toolbar-button-clicked': 'toolbarButtonClicked',
'transcription-chunk-received': 'transcriptionChunkReceived',
'whiteboard-status-changed': 'whiteboardStatusChanged'
};
@@ -273,10 +275,10 @@ function parseArguments(args) {
function parseSizeParam(value) {
let parsedValue;
// This regex parses values of the form 100px, 100em, 100pt or 100%.
// This regex parses values of the form 100px, 100em, 100pt, 100vh, 100vw or 100%.
// Values like 100 or 100px are handled outside of the regex, and
// invalid values will be ignored and the minimum will be used.
const re = /([0-9]*\.?[0-9]+)(em|pt|px|%)$/;
const re = /([0-9]*\.?[0-9]+)(em|pt|px|((d|l|s)?v)(h|w)|%)$/;
if (typeof value === 'string' && String(value).match(re) !== null) {
parsedValue = value;
@@ -365,7 +367,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
},
release
});
this._createIFrame(height, width, onload, sandbox);
this._createIFrame(height, width, sandbox);
this._transport = new Transport({
backend: new PostMessageTransportBackend({
postisOptions: {
@@ -375,9 +379,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
})
});
if (Array.isArray(invitees) && invitees.length > 0) {
this.invite(invitees);
}
this._onload = onload;
this._tmpE2EEKey = e2eeKey;
this._isLargeVideoVisible = false;
this._isPrejoinVideoVisible = false;
@@ -396,14 +403,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* parseSizeParam for format details.
* @param {number|string} width - The with of the iframe. Check
* parseSizeParam for format details.
* @param {Function} onload - The function that will listen
* for onload event.
* @param {string} sandbox - Sandbox directive for the created iframe, if desired.
* @returns {void}
*
* @private
*/
_createIFrame(height, width, onload, sandbox) {
_createIFrame(height, width, sandbox) {
const frameName = `jitsiConferenceFrame${id}`;
this._frame = document.createElement('iframe');
@@ -427,11 +432,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this._frame.sandbox = sandbox;
}
if (onload) {
// waits for iframe resources to load
// and fires event when it is done
this._frame.onload = onload;
}
this._frame.src = this._url;
this._frame = this._parentNode.appendChild(this._frame);
@@ -580,6 +580,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
const userID = data.id;
switch (name) {
case 'ready': {
// Fake the iframe onload event because it's not reliable.
this._onload?.();
break;
}
case 'video-conference-joined': {
if (typeof this._tmpE2EEKey !== 'undefined') {

687
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,12 +65,13 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1716.0.0+93c167d3/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1720.0.0+b3173832/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
"pixelmatch": "5.3.0",
"promise.allsettled": "1.0.4",
"punycode": "2.3.0",
"react": "18.2.0",
@@ -113,7 +114,6 @@
"react-youtube": "10.1.0",
"redux": "4.0.4",
"redux-thunk": "2.4.1",
"resemblejs": "4.0.0",
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"tss-react": "4.4.4",
@@ -136,6 +136,8 @@
"@types/dom-screen-wake-lock": "1.0.1",
"@types/js-md5": "0.4.3",
"@types/lodash": "4.14.182",
"@types/offscreencanvas": "2019.7.2",
"@types/pixelmatch": "5.2.5",
"@types/punycode": "2.1.0",
"@types/react": "17.0.14",
"@types/react-dom": "17.0.14",
@@ -145,7 +147,6 @@
"@types/react-native-video": "5.0.14",
"@types/react-redux": "7.1.24",
"@types/react-window": "1.8.5",
"@types/resemblejs": "^4.1.0",
"@types/unorm": "1.3.28",
"@types/uuid": "8.3.4",
"@types/w3c-image-capture": "1.0.6",

View File

@@ -19,8 +19,6 @@ import { setAudioMuted, setVideoMuted } from './react/features/base/media/action
interface IEventListeners {
onAudioMutedChanged?: Function;
onVideoMutedChanged?: Function;
onConferenceBlurred?: Function;
onConferenceFocused?: Function;
onConferenceJoined?: Function;
@@ -109,8 +107,6 @@ export const JitsiMeeting = forwardRef((props: IAppProps, ref) => {
setAppProps({
'flags': flags,
'rnSdkHandlers': {
onAudioMutedChanged: eventListeners?.onAudioMutedChanged,
onVideoMutedChanged: eventListeners?.onVideoMutedChanged,
onConferenceBlurred: eventListeners?.onConferenceBlurred,
onConferenceFocused: eventListeners?.onConferenceFocused,
onConferenceJoined: eventListeners?.onConferenceJoined,

View File

@@ -5,15 +5,12 @@ import { openTokenAuthUrl } from '../authentication/actions';
// @ts-ignore
import { getTokenAuthUrl, isTokenAuthEnabled } from '../authentication/functions';
import { getJwtExpirationDate } from '../base/jwt/functions';
import { MEDIA_TYPE } from '../base/media/constants';
import { isLocalTrackMuted } from '../base/tracks/functions.any';
import { getLocationContextRoot, parseURIString } from '../base/util/uri';
import { addTrackStateToURL } from './functions.any';
import logger from './logger';
import { IStore } from './types';
/**
* Redirects to another page generated by replacing the path in the original URL
* with the given path.
@@ -107,11 +104,7 @@ export function maybeRedirectToTokenAuthUrl(
dispatch: IStore['dispatch'], getState: IStore['getState'], failureCallback: Function) {
const state = getState();
const config = state['features/base/config'];
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
const { startAudioOnly } = config;
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
if (!isTokenAuthEnabled(config)) {
return false;
@@ -127,18 +120,7 @@ export function maybeRedirectToTokenAuthUrl(
const room = state['features/base/conference'].room;
const { tenant } = parseURIString(locationURL.href) || {};
getTokenAuthUrl(
config,
locationURL,
{
audioMuted,
audioOnlyEnabled: audioOnlyEnabled || startAudioOnly,
skipPrejoin: true,
videoMuted
},
room,
tenant
)
getTokenAuthUrl(config, room, tenant, true, locationURL)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');

View File

@@ -55,20 +55,8 @@ function _getWebConferenceRoute(state: IReduxState) {
&& !state['features/base/jwt'].jwt && room) {
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { startAudioOnly } = config;
return getTokenAuthUrl(
config,
locationURL,
{
audioMuted: false,
audioOnlyEnabled: startAudioOnly,
skipPrejoin: false,
videoMuted: false
},
room,
tenant
)
return getTokenAuthUrl(config, room, tenant, false, locationURL)
.then((url: string | undefined) => {
route.href = url;

View File

@@ -13,65 +13,29 @@ export const isTokenAuthEnabled = (config: IConfig): boolean =>
/**
* Returns the state that we can add as a parameter to the tokenAuthUrl.
*
* @param {URL} locationURL - The location URL.
* @param {Object} options: - Config options {
* audioMuted: boolean | undefined
* audioOnlyEnabled: boolean | undefined,
* skipPrejoin: boolean | undefined,
* videoMuted: boolean | undefined
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
*
* @param {boolean} skipPrejoin - Whether to skip pre-join page.
* @param {URL} locationURL - The location URL.
* @returns {Object} The state object.
*/
export const _getTokenAuthState = (
locationURL: URL,
options: {
audioMuted: boolean | undefined;
audioOnlyEnabled: boolean | undefined;
skipPrejoin: boolean | undefined;
videoMuted: boolean | undefined;
},
roomName: string | undefined,
tenant: string | undefined): object => {
tenant: string | undefined,
skipPrejoin: boolean | undefined = false,
locationURL: URL): object => {
const state = {
room: roomName,
roomSafe: getBackendSafeRoomName(roomName),
tenant
};
const {
audioMuted = false,
audioOnlyEnabled = false,
skipPrejoin = false,
videoMuted = false
} = options;
if (audioMuted) {
// @ts-ignore
state['config.startWithAudioMuted'] = true;
}
if (audioOnlyEnabled) {
// @ts-ignore
state['config.startAudioOnly'] = true;
}
if (skipPrejoin) {
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
// @ts-ignore
state['config.prejoinConfig.enabled'] = false;
}
if (videoMuted) {
// @ts-ignore
state['config.startWithVideoMuted'] = true;
}
const params = new URLSearchParams(locationURL.hash);
for (const [ key, value ] of params) {

View File

@@ -14,15 +14,10 @@ export * from './functions.any';
* argument to this method.
*
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
* @param {string} tenant - The name of the conference tenant.
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
* @param {URL} locationURL - The location URL.
* @param {Object} options: - Config options {
* audioMuted: boolean | undefined
* audioOnlyEnabled: boolean | undefined,
* skipPrejoin: boolean | undefined,
* videoMuted: boolean | undefined
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -30,23 +25,11 @@ export * from './functions.any';
*/
export const getTokenAuthUrl = (
config: IConfig,
locationURL: URL,
options: {
audioMuted: boolean | undefined;
audioOnlyEnabled: boolean | undefined;
skipPrejoin: boolean | undefined;
videoMuted: boolean | undefined;
},
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false,
// eslint-disable-next-line max-params
tenant: string | undefined): Promise<string | undefined> => {
const {
audioMuted = false,
audioOnlyEnabled = false,
skipPrejoin = false,
videoMuted = false
} = options;
locationURL: URL): Promise<string | undefined> => {
let url = config.tokenAuthUrl;
@@ -55,17 +38,7 @@ export const getTokenAuthUrl = (
}
if (url.indexOf('{state}')) {
const state = _getTokenAuthState(
locationURL,
{
audioMuted,
audioOnlyEnabled,
skipPrejoin,
videoMuted
},
roomName,
tenant
);
const state = _getTokenAuthState(roomName, tenant, skipPrejoin, locationURL);
// Append ios=true or android=true to the token URL.
// @ts-ignore

View File

@@ -32,15 +32,10 @@ function _cryptoRandom() {
* argument to this method.
*
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
* @param {URL} locationURL - The location URL.
* @param {Object} options: - Config options {
* audioMuted: boolean | undefined
* audioOnlyEnabled: boolean | undefined,
* skipPrejoin: boolean | undefined,
* videoMuted: boolean | undefined
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
* @param {string} tenant - The name of the conference tenant.
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
* @param {URL} locationURL - The current location URL.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -48,23 +43,11 @@ function _cryptoRandom() {
*/
export const getTokenAuthUrl = (
config: IConfig,
locationURL: URL,
options: {
audioMuted: boolean | undefined;
audioOnlyEnabled: boolean | undefined;
skipPrejoin: boolean | undefined;
videoMuted: boolean | undefined;
},
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false,
// eslint-disable-next-line max-params
tenant: string | undefined): Promise<string | undefined> => {
const {
audioMuted = false,
audioOnlyEnabled = false,
skipPrejoin = false,
videoMuted = false
} = options;
locationURL: URL): Promise<string | undefined> => {
let url = config.tokenAuthUrl;
@@ -73,17 +56,7 @@ export const getTokenAuthUrl = (
}
if (url.indexOf('{state}')) {
const state = _getTokenAuthState(
locationURL,
{
audioMuted,
audioOnlyEnabled,
skipPrejoin,
videoMuted
},
roomName,
tenant
);
const state = _getTokenAuthState(roomName, tenant, skipPrejoin, locationURL);
if (browser.isElectron()) {
// @ts-ignore

View File

@@ -13,9 +13,7 @@ import {
JitsiConferenceErrors,
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../base/media/constants';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { isLocalTrackMuted } from '../base/tracks/functions.any';
import { parseURIString } from '../base/util/uri';
import { openLogoutDialog } from '../settings/actions';
@@ -259,9 +257,6 @@ function _handleLogin({ dispatch, getState }: IStore) {
const room = state['features/base/conference'].room;
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
if (!room) {
logger.warn('Cannot handle login, room is undefined!');
@@ -275,18 +270,7 @@ function _handleLogin({ dispatch, getState }: IStore) {
return;
}
getTokenAuthUrl(
config,
locationURL,
{
audioMuted,
audioOnlyEnabled,
skipPrejoin: true,
videoMuted
},
room,
tenant
)
getTokenAuthUrl(config, room, tenant, true, locationURL)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');

View File

@@ -253,6 +253,7 @@ export interface IConfig {
callStatsID?: string;
callStatsSecret?: string;
callUUID?: string;
cameraFacingMode?: string;
channelLastN?: number;
chromeExtensionBanner?: {
chromeExtensionsInfo?: Array<{ id: string; path: string; }>;

View File

@@ -74,6 +74,7 @@ export default [
*/
'callUUID',
'cameraFacingMode',
'conferenceInfo',
'channelLastN',
'connectionIndicators',

View File

@@ -23,7 +23,12 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { getPropertyValue } from '../settings/functions.any';
import { TRACK_ADDED } from '../tracks/actionTypes';
import { destroyLocalTracks } from '../tracks/actions.any';
import { isLocalTrackMuted, isLocalVideoTrackDesktop, setTrackMuted } from '../tracks/functions.any';
import {
getCameraFacingMode,
isLocalTrackMuted,
isLocalVideoTrackDesktop,
setTrackMuted
} from '../tracks/functions.any';
import { ITrack } from '../tracks/types';
import {
@@ -40,7 +45,6 @@ import {
setVideoMuted
} from './actions';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY
@@ -233,7 +237,7 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setCameraFacingMode(getCameraFacingMode(state)));
dispatch(setVideoMuted(videoMuted));
}

View File

@@ -53,6 +53,11 @@ export interface IProps extends WithTranslation {
*/
handleClick?: Function;
/**
* Whether the button open a menu or not.
*/
isMenuButton?: boolean;
/**
* Notify mode for `toolbarButtonClicked` event -
* whether to only notify or to also prevent button click routine.
@@ -102,7 +107,7 @@ export const defaultDisabledButtonStyles = {
/**
* An abstract implementation of a button.
*/
export default class AbstractButton<P extends IProps, S=any> extends Component<P, S> {
export default class AbstractButton<P extends IProps, S = any> extends Component<P, S> {
static defaultProps = {
afterClick: undefined,
disabledStyles: defaultDisabledButtonStyles,
@@ -354,7 +359,7 @@ export default class AbstractButton<P extends IProps, S=any> extends Component<P
if (typeof APP !== 'undefined' && notifyMode) {
APP.API.notifyToolbarButtonClicked(
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
);
}

View File

@@ -19,6 +19,11 @@ interface IProps extends AbstractToolboxItemProps {
*/
contextMenu?: boolean;
/**
* Whether the button open a menu or not.
*/
isMenuButton?: boolean;
/**
* On key down handler.
*/
@@ -67,6 +72,7 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
const {
backgroundColor,
contextMenu,
isMenuButton,
disabled,
elementAfter,
icon,
@@ -77,8 +83,9 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
toggled
} = this.props;
const className = showLabel ? 'overflow-menu-item' : 'toolbox-button';
const buttonAttribute = isMenuButton ? 'aria-expanded' : 'aria-pressed';
const props = {
'aria-pressed': toggled,
[buttonAttribute]: toggled,
'aria-disabled': disabled,
'aria-label': this.accessibilityLabel,
className: className + (disabled ? ' disabled' : ''),

View File

@@ -35,6 +35,7 @@ import {
} from './actionTypes';
import {
createLocalTracksF,
getCameraFacingMode,
getLocalTrack,
getLocalTracks,
getLocalVideoTrack,
@@ -140,6 +141,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
getState
};
const promises = [];
const state = getState();
// The following executes on React Native only at the time of this
// writing. The effort to port Web's createInitialLocalTracks
@@ -153,7 +155,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
// device separately.
for (const device of devices) {
if (getLocalTrack(
getState()['features/base/tracks'],
state['features/base/tracks'],
device as MediaType,
/* includePending */ true)) {
throw new Error(`Local track for ${device} already exists`);
@@ -165,7 +167,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
cameraDeviceId: options.cameraDeviceId,
devices: [ device ],
facingMode:
options.facingMode || CAMERA_FACING_MODE.USER,
options.facingMode || getCameraFacingMode(state),
micDeviceId: options.micDeviceId
},
store)

View File

@@ -4,7 +4,7 @@ import {
} from '../config/functions.any';
import { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { gumPending } from '../media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { CAMERA_FACING_MODE, MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { IMediaState } from '../media/reducer';
import { IGUMPendingState } from '../media/types';
import {
@@ -447,3 +447,13 @@ export function logTracksForParticipant(tracksState: ITrack[], participantId: st
logger.debug(`${logStringPrefix}${reason ? `(reason: ${reason})` : ''}:${tracksLogMsg}`);
}
/**
* Gets the default camera facing mode.
*
* @param {Object} state - The redux state.
* @returns {string} - The camera facing mode.
*/
export function getCameraFacingMode(state: IReduxState) {
return state['features/base/config'].cameraFacingMode ?? CAMERA_FACING_MODE.USER;
}

View File

@@ -1,6 +1,7 @@
import { IStore } from '../../app/types';
import JitsiMeetJS from '../lib-jitsi-meet';
import { getCameraFacingMode } from './functions.any';
import { ITrackOptions } from './types';
export * from './functions.any';
@@ -39,6 +40,7 @@ export function createLocalTracksF(options: ITrackOptions = {}, store: IStore) {
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
facingMode: options.facingMode || getCameraFacingMode(state),
micDeviceId,
resolution
});

View File

@@ -12,6 +12,7 @@ import {
getUserSelectedMicDeviceId
} from '../settings/functions.web';
import { getCameraFacingMode } from './functions.any';
import loadEffects from './loadEffects';
import logger from './logger';
import { ITrackOptions } from './types';
@@ -82,6 +83,7 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore)
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
effects,
facingMode: options.facingMode || getCameraFacingMode(state),
firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
micDeviceId,

View File

@@ -11,6 +11,7 @@ const button = {
const buttonLabel = {
...BaseTheme.typography.bodyShortBold,
lineHeight: 14,
textTransform: 'capitalize'
};

View File

@@ -12,7 +12,7 @@ import {
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect, useDispatch } from 'react-redux';
import { appNavigate } from '../../../app/actions.native';
import { appNavigate } from '../../../app/actions';
import { IReduxState, IStore } from '../../../app/types';
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
import { FULLSCREEN_ENABLED, PIP_ENABLED } from '../../../base/flags/constants';
@@ -41,15 +41,15 @@ import { navigate } from '../../../mobile/navigation/components/conference/Confe
import { screen } from '../../../mobile/navigation/routes';
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
import Captions from '../../../subtitles/components/native/Captions';
import { setToolboxVisible } from '../../../toolbox/actions.native';
import { setToolboxVisible } from '../../../toolbox/actions';
import Toolbox from '../../../toolbox/components/native/Toolbox';
import { isToolboxVisible } from '../../../toolbox/functions.native';
import { isToolboxVisible } from '../../../toolbox/functions';
import {
AbstractConference,
abstractMapStateToProps
} from '../AbstractConference';
import type { AbstractProps } from '../AbstractConference';
import { isConnecting } from '../functions.native';
import { isConnecting } from '../functions';
import AlwaysOnLabels from './AlwaysOnLabels';
import ExpandedLabelPopup from './ExpandedLabelPopup';
@@ -230,9 +230,7 @@ class Conference extends AbstractConference<IProps, State> {
*/
componentDidUpdate(prevProps: IProps) {
const {
_audioOnlyEnabled,
_showLobby,
_startCarMode
_showLobby
} = this.props;
if (!prevProps._showLobby && _showLobby) {
@@ -240,10 +238,6 @@ class Conference extends AbstractConference<IProps, State> {
}
if (prevProps._showLobby && !_showLobby) {
if (_audioOnlyEnabled && _startCarMode) {
return;
}
navigate(screen.conference.main);
}
}

View File

@@ -3,6 +3,7 @@ import { Component } from 'react';
import { createInviteDialogEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState, IStore } from '../../../app/types';
import { getMeetingRegion } from '../../../base/config/functions.any';
import { showErrorNotification, showNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
import { INotificationProps } from '../../../notifications/types';
@@ -65,6 +66,11 @@ export interface IProps {
*/
_peopleSearchUrl: string;
/**
* The region where we connected to.
*/
_region: string;
/**
* Whether or not to allow sip invites.
*/
@@ -248,6 +254,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
_jwt: jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl,
_region: region,
_sipInviteEnabled: sipInviteEnabled
} = this.props;
const options = {
@@ -259,6 +266,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
jwt,
peopleSearchQueryTypes,
peopleSearchUrl,
region,
sipInviteEnabled
};
@@ -300,6 +308,7 @@ export function _mapStateToProps(state: IReduxState) {
_jwt: state['features/base/jwt'].jwt ?? '',
_peopleSearchQueryTypes: peopleSearchQueryTypes ?? [],
_peopleSearchUrl: peopleSearchUrl ?? '',
_region: getMeetingRegion(state),
_sipInviteEnabled: isSipInviteEnabled(state)
};
}

View File

@@ -41,13 +41,15 @@ export const sharingFeatures = {
*
* @param {string} dialNumber - The dial number to check for validity.
* @param {string} dialOutAuthUrl - The endpoint to use for checking validity.
* @param {string} region - The region we are connected to.
* @returns {Promise} - The promise created by the request.
*/
export function checkDialNumber(
dialNumber: string,
dialOutAuthUrl: string
dialOutAuthUrl: string,
region: string
): Promise<{ allow?: boolean; country?: string; phone?: string; }> {
const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}&region=${region}`;
return new Promise((resolve, reject) =>
fetch(fullUrl)
@@ -146,6 +148,11 @@ export type GetInviteResultsOptions = {
*/
peopleSearchUrl: string;
/**
* The region we are connected to.
*/
region: string;
/**
* Whether or not to check sip invites.
*/
@@ -174,6 +181,7 @@ export function getInviteResultsForQuery(
dialOutEnabled,
peopleSearchQueryTypes,
peopleSearchUrl,
region,
sipInviteEnabled,
jwt
} = options;
@@ -217,7 +225,7 @@ export function getInviteResultsForQuery(
// so ensure only digits get sent.
numberToVerify = getDigitsOnly(numberToVerify);
phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl);
phoneNumberPromise = checkDialNumber(numberToVerify, dialOutAuthUrl, region);
} else if (dialOutEnabled && !dialOutAuthUrl) {
// fake having a country code to hide the country code reminder
hasCountryCode = true;

View File

@@ -228,16 +228,17 @@ class LargeVideo extends Component<IProps> {
* another container for the background and the
* largeVideoWrapper in order to hide/show them.
*/}
{ _displayScreenSharingPlaceholder ? <ScreenSharePlaceholder /> : <></>}
<div
id = 'largeVideoWrapper'
onTouchEnd = { this._onDoubleTap }
ref = { this._wrapperRef }
role = 'figure' >
{ _displayScreenSharingPlaceholder ? <ScreenSharePlaceholder /> : <video
<video
autoPlay = { !_noAutoPlayVideo }
id = 'largeVideo'
muted = { true }
playsInline = { true } /* for Safari on iOS to work */ /> }
playsInline = { true } /* for Safari on iOS to work */ />
</div>
</div>
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES

View File

@@ -15,7 +15,10 @@ const useStyles = makeStyles()(theme => {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute'
position: 'absolute',
top: 0,
left: 0,
zIndex: 2
},
content: {
display: 'flex',

View File

@@ -8,7 +8,6 @@ import {
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN
} from '../../base/conference/actionTypes';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
import { PARTICIPANT_JOINED } from '../../base/participants/actionTypes';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
@@ -32,12 +31,6 @@ const { JMOngoingConference } = NativeModules;
const rnSdkHandlers = getAppProp(store, 'rnSdkHandlers');
switch (type) {
case SET_AUDIO_MUTED:
rnSdkHandlers?.onAudioMutedChanged && rnSdkHandlers?.onAudioMutedChanged(action.muted);
break;
case SET_VIDEO_MUTED:
rnSdkHandlers?.onVideoMutedChanged && rnSdkHandlers?.onVideoMutedChanged(Boolean(action.muted));
break;
case CONFERENCE_BLURRED:
rnSdkHandlers?.onConferenceBlurred && rnSdkHandlers?.onConferenceBlurred();
break;

View File

@@ -128,6 +128,7 @@ export const RoomParticipantContextMenu = ({
size = { AVATAR_SIZE } />,
text: entity?.participantName
} ] } />}
<ContextMenuItemGroup>
<div className = { styles.text }>
{t('breakoutRooms.actions.sendToBreakoutRoom')}

View File

@@ -6,6 +6,10 @@ import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { openDialog, openSheet } from '../../../base/dialog/actions';
import {
BREAKOUT_ROOMS_BUTTON_ENABLED
} from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconDotsHorizontal, IconRingGroup } from '../../../base/icons/svg';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
@@ -34,6 +38,9 @@ const ParticipantsPaneFooter = (): JSX.Element => {
const isBreakoutRoomsSupported = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getBreakoutRooms()?.isSupported()
);
const isBreakoutRoomsEnabled = useSelector((state: IReduxState) =>
getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true)
);
const openMoreMenu = useCallback(() => dispatch(openSheet(ContextMenuMore)), [ dispatch ]);
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
[ dispatch ]);
@@ -44,6 +51,7 @@ const ParticipantsPaneFooter = (): JSX.Element => {
<View style = { styles.participantsPaneFooterContainer as ViewStyle }>
{
isBreakoutRoomsSupported
&& isBreakoutRoomsEnabled
&& <Button
accessibilityLabel = 'participantsPane.actions.breakoutRooms'
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow

View File

@@ -1,6 +1,8 @@
import React, { useCallback } from 'react';
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import AbstractPollResults from '../AbstractPollResults';
@@ -18,7 +20,6 @@ const PollResults = (props: AbstractProps) => {
const {
answers,
changeVote,
creatorName,
haveVoted,
question,
showDetails,
@@ -87,6 +88,7 @@ const PollResults = (props: AbstractProps) => {
);
}, [ showDetails ]);
const localParticipant = useSelector(getLocalParticipant);
/* eslint-disable react/jsx-no-bind */
@@ -94,7 +96,7 @@ const PollResults = (props: AbstractProps) => {
<View>
<Text style = { dialogStyles.questionText as TextStyle } >{ question }</Text>
<Text style = { dialogStyles.questionOwnerText as TextStyle } >
{ t('polls.by', { name: creatorName }) }
{ t('polls.by', { name: localParticipant?.name }) }
</Text>
<FlatList
data = { answers }

View File

@@ -1,5 +1,5 @@
import resemble from 'resemblejs';
import 'image-capture';
import JitsiTrack from 'lib-jitsi-meet/types/auto/modules/RTC/JitsiTrack';
import './createImageBitmap';
import { createScreensharingCaptureTakenEvent } from '../analytics/AnalyticsEvents';
@@ -7,20 +7,20 @@ import { sendAnalytics } from '../analytics/functions';
import { IReduxState } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
import { ITrack } from '../base/tracks/types';
import { getBaseUrl } from '../base/util/helpers';
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
import {
CLEAR_INTERVAL,
INTERVAL_TIMEOUT,
PERCENTAGE_LOWER_BOUND,
CLEAR_TIMEOUT,
POLL_INTERVAL,
SET_INTERVAL
SCREENSHOT_QUEUE_LIMIT,
SET_TIMEOUT,
TIMEOUT_TICK
} from './constants';
import logger from './logger';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { processScreenshot } from './processScreenshot';
import { timerWorkerScript } from './worker';
declare let ImageCapture: any;
@@ -30,14 +30,10 @@ declare let ImageCapture: any;
*/
export default class ScreenshotCaptureSummary {
_state: IReduxState;
_currentCanvas: HTMLCanvasElement;
_currentCanvasContext: CanvasRenderingContext2D | null;
_initializedRegion: boolean;
_imageCapture: any;
_imageCapture: ImageCapture;
_streamWorker: Worker;
_streamHeight: any;
_streamWidth: any;
_storedImageData?: ImageData;
_queue: Blob[];
/**
* Initializes a new {@code ScreenshotCaptureEffect} instance.
@@ -46,16 +42,17 @@ export default class ScreenshotCaptureSummary {
*/
constructor(state: IReduxState) {
this._state = state;
this._currentCanvas = document.createElement('canvas');
this._currentCanvasContext = this._currentCanvas.getContext('2d');
// Bind handlers such that they access the same instance.
this._handleWorkerAction = this._handleWorkerAction.bind(this);
this._initScreenshotCapture = this._initScreenshotCapture.bind(this);
this._streamWorker = new Worker(timerWorkerScript, { name: 'Screenshot capture worker' });
const baseUrl = `${getBaseUrl()}libs/`;
const workerUrl = `${baseUrl}screenshot-capture-worker.min.js`;
this._streamWorker = new Worker(workerUrl, { name: 'Screenshot capture worker' });
this._streamWorker.onmessage = this._handleWorkerAction;
this._initializedRegion = false;
this._queue = [];
}
/**
@@ -77,10 +74,17 @@ export default class ScreenshotCaptureSummary {
...jwt && { 'Authorization': `Bearer ${jwt}` }
};
await fetch(`${_screenshotHistoryRegionUrl}/${sessionId}`, {
method: 'POST',
headers
});
try {
await fetch(`${_screenshotHistoryRegionUrl}/${sessionId}`, {
method: 'POST',
headers
});
} catch (err) {
logger.warn(`Could not create screenshot region: ${err}`);
return;
}
this._initializedRegion = true;
}
@@ -88,31 +92,27 @@ export default class ScreenshotCaptureSummary {
/**
* Starts the screenshot capture event on a loop.
*
* @param {Track} track - The track that contains the stream from which screenshots are to be sent.
* @param {JitsiTrack} jitsiTrack - The track that contains the stream from which screenshots are to be sent.
* @returns {Promise} - Promise that resolves once effect has started or rejects if the
* videoType parameter is not desktop.
*/
async start(track: ITrack) {
const { videoType } = track;
const stream = track.getOriginalStream();
async start(jitsiTrack: JitsiTrack) {
if (!window.OffscreenCanvas) {
logger.warn('Can\'t start screenshot capture, OffscreenCanvas is not available');
return;
}
const { videoType, track } = jitsiTrack;
if (videoType !== 'desktop') {
return;
}
const desktopTrack = stream.getVideoTracks()[0];
const { height, width }
= desktopTrack.getSettings() ?? desktopTrack.getConstraints();
this._streamHeight = height;
this._streamWidth = width;
this._currentCanvas.height = parseInt(height, 10);
this._currentCanvas.width = parseInt(width, 10);
this._imageCapture = new ImageCapture(desktopTrack);
this._imageCapture = new ImageCapture(track);
if (!this._initializedRegion) {
await this._initRegionSelection();
}
this._initScreenshotCapture();
this.sendTimeout();
}
/**
@@ -121,28 +121,34 @@ export default class ScreenshotCaptureSummary {
* @returns {void}
*/
stop() {
this._streamWorker.postMessage({ id: CLEAR_INTERVAL });
this._streamWorker.postMessage({ id: CLEAR_TIMEOUT });
}
/**
* Method that is called as soon as the first frame of the video loads from stream.
* The method is used to store the {@code ImageData} object from the first frames
* in order to use it for future comparisons based on which we can process only certain
* screenshots.
* Sends to worker the imageBitmap for the next timeout.
*
* @private
* @returns {void}
* @returns {Promise<void>}
*/
async _initScreenshotCapture() {
const imageBitmap = await this._imageCapture.grabFrame();
async sendTimeout() {
let imageBitmap: ImageBitmap | undefined;
this._currentCanvasContext?.drawImage(imageBitmap, 0, 0, this._streamWidth, this._streamHeight);
const imageData = this._currentCanvasContext?.getImageData(0, 0, this._streamWidth, this._streamHeight);
if (!this._imageCapture.track || this._imageCapture.track.readyState !== 'live') {
logger.warn('Track is in invalid state');
this.stop();
return;
}
try {
imageBitmap = await this._imageCapture.grabFrame();
} catch (e) {
// ignore error
}
this._storedImageData = imageData;
this._streamWorker.postMessage({
id: SET_INTERVAL,
timeMs: POLL_INTERVAL
id: SET_TIMEOUT,
timeMs: POLL_INTERVAL,
imageBitmap
});
}
@@ -153,18 +159,24 @@ export default class ScreenshotCaptureSummary {
* @param {EventHandler} message - Message received from the Worker.
* @returns {void}
*/
_handleWorkerAction(message: { data: { id: number; }; }) {
return message.data.id === INTERVAL_TIMEOUT && this._handleScreenshot();
_handleWorkerAction(message: { data: { id: number; imageBlob?: Blob; }; }) {
const { id, imageBlob } = message.data;
this.sendTimeout();
if (id === TIMEOUT_TICK && imageBlob && this._queue.length < SCREENSHOT_QUEUE_LIMIT) {
this._doProcessScreenshot(imageBlob);
}
}
/**
* Method that processes the screenshot.
*
* @private
* @param {ImageData} imageData - The image data of the new screenshot.
* @param {Blob} imageBlob - The blob for the current screenshot.
* @returns {void}
*/
_doProcessScreenshot(imageData?: ImageData) {
_doProcessScreenshot(imageBlob: Blob) {
this._queue.push(imageBlob);
sendAnalytics(createScreensharingCaptureTakenEvent());
const conference = getCurrentConference(this._state);
@@ -175,41 +187,24 @@ export default class ScreenshotCaptureSummary {
const { jwt } = this._state['features/base/jwt'];
const meetingFqn = extractFqnFromPath();
const remoteParticipants = getRemoteParticipants(this._state);
const participants = [];
const participants: Array<string | undefined> = [];
participants.push(getLocalParticipant(this._state)?.id);
remoteParticipants.forEach(p => participants.push(p.id));
this._storedImageData = imageData;
processScreenshot(this._currentCanvas, {
processScreenshot(imageBlob, {
jid,
jwt,
sessionId,
timestamp,
meetingFqn,
participants
}).then(() => {
const index = this._queue.indexOf(imageBlob);
if (index > -1) {
this._queue.splice(index, 1);
}
});
}
/**
* Screenshot handler.
*
* @private
* @returns {void}
*/
async _handleScreenshot() {
const imageBitmap = await this._imageCapture.grabFrame();
this._currentCanvasContext?.drawImage(imageBitmap, 0, 0, this._streamWidth, this._streamHeight);
const imageData = this._currentCanvasContext?.getImageData(0, 0, this._streamWidth, this._streamHeight);
resemble(imageData ?? '')
.compareTo(this._storedImageData ?? '')
.setReturnEarlyThreshold(PERCENTAGE_LOWER_BOUND)
.onComplete(resultData => {
if (resultData.rawMisMatchPercentage > PERCENTAGE_LOWER_BOUND) {
this._doProcessScreenshot(imageData);
}
});
}
}

View File

@@ -1,40 +1,44 @@
/**
* Percent of pixels that signal if two images should be considered different.
*/
export const PERCENTAGE_LOWER_BOUND = 5;
export const PERCENTAGE_LOWER_BOUND = 4;
/**
* Number of milliseconds that represent how often screenshots should be taken.
*/
export const POLL_INTERVAL = 4000;
export const POLL_INTERVAL = 2000;
/**
* SET_INTERVAL constant is used to set interval and it is set in
* SET_TIMEOUT constant is used to set interval and it is set in
* the id property of the request.data property. TimeMs property must
* also be set. Request.data example:
*
* {
* id: SET_INTERVAL,
* id: SET_TIMEOUT,
* timeMs: 33
* }.
*/
export const SET_INTERVAL = 1;
export const SET_TIMEOUT = 1;
/**
* CLEAR_INTERVAL constant is used to clear the interval and it is set in
* CLEAR_TIMEOUT constant is used to clear the interval and it is set in
* the id property of the request.data property.
*
* {
* id: CLEAR_INTERVAL
* id: CLEAR_TIMEOUT
* }.
*/
export const CLEAR_INTERVAL = 2;
export const CLEAR_TIMEOUT = 2;
/**
* INTERVAL_TIMEOUT constant is used as response and it is set in the id property.
* TIMEOUT_TICK constant is used as response and it is set in the id property.
*
* {
* id: INTERVAL_TIMEOUT
* id: TIMEOUT_TICK
* }.
*/
export const INTERVAL_TIMEOUT = 3;
export const TIMEOUT_TICK = 3;
export const SCREENSHOT_QUEUE_LIMIT = 3;
export const MAX_FILE_SIZE = 1000000;

View File

@@ -1,10 +1,10 @@
/**
* Helper method used to process screenshots captured by the {@code ScreenshotCaptureEffect}.
*
* @param {HTMLCanvasElement} canvas - The canvas containing a screenshot to be processed.
* @param {Blob} imageBlob - The blob of the screenshot that has to be processed.
* @param {Object} options - Custom options required for processing.
* @returns {void}
* @returns {Promise<void>}
*/
export function processScreenshot(canvas, options) { // eslint-disable-line no-unused-vars
export async function processScreenshot(imageBlob, options) { // eslint-disable-line no-unused-vars
return;
}

View File

@@ -1,29 +1,132 @@
import pixelmatch from 'pixelmatch';
import {
CLEAR_INTERVAL,
INTERVAL_TIMEOUT,
SET_INTERVAL
CLEAR_TIMEOUT,
MAX_FILE_SIZE,
PERCENTAGE_LOWER_BOUND,
SET_TIMEOUT,
TIMEOUT_TICK
} from './constants';
const code = `
var timer;
onmessage = function(request) {
switch (request.data.id) {
case ${SET_INTERVAL}: {
timer = setInterval(() => {
postMessage({ id: ${INTERVAL_TIMEOUT} });
}, request.data.timeMs);
break;
}
case ${CLEAR_INTERVAL}: {
if (timer) {
clearInterval(timer);
let timer: ReturnType<typeof setTimeout>;
const canvas = new OffscreenCanvas(0, 0);
const ctx = canvas.getContext('2d');
let storedImageData: ImageData | undefined;
/**
* Sends Blob with the screenshot to main thread.
*
* @param {ImageData} imageData - The image of the screenshot.
* @returns {void}
*/
async function sendBlob(imageData: ImageData) {
let imageBlob = await canvas.convertToBlob({ type: 'image/jpeg' });
if (imageBlob.size > MAX_FILE_SIZE) {
const quality = Number((MAX_FILE_SIZE / imageBlob.size).toFixed(2)) * 0.92;
imageBlob = await canvas.convertToBlob({ type: 'image/jpeg',
quality });
}
storedImageData = imageData;
postMessage({
id: TIMEOUT_TICK,
imageBlob
});
}
/**
* Sends empty message to main thread.
*
* @returns {void}
*/
function sendEmpty() {
postMessage({
id: TIMEOUT_TICK
});
}
/**
* Draws the image bitmap on the canvas and checks the difference percent with the previous image
* if there is no previous image the percentage is not calculated.
*
* @param {ImageBitmap} imageBitmap - The image bitmap that is drawn on canvas.
* @returns {void}
*/
function checkScreenshot(imageBitmap: ImageBitmap) {
const { height, width } = imageBitmap;
if (canvas.width !== width) {
canvas.width = width;
}
if (canvas.height !== height) {
canvas.height = height;
}
ctx?.drawImage(imageBitmap, 0, 0, width, height);
const imageData = ctx?.getImageData(0, 0, width, height);
imageBitmap.close();
if (!imageData) {
sendEmpty();
return;
}
if (!storedImageData || imageData.data.length !== storedImageData.data.length) {
sendBlob(imageData);
return;
}
let numOfPixels = 0;
try {
numOfPixels = pixelmatch(
imageData.data,
storedImageData.data,
null,
width,
height);
} catch {
sendEmpty();
return;
}
const percent = numOfPixels / imageData.data.length * 100;
if (percent >= PERCENTAGE_LOWER_BOUND) {
sendBlob(imageData);
} else {
sendEmpty();
}
}
onmessage = function(request) {
switch (request.data.id) {
case SET_TIMEOUT: {
timer = setTimeout(async () => {
const imageBitmap = request.data.imageBitmap;
if (imageBitmap) {
checkScreenshot(imageBitmap);
} else {
sendEmpty();
}
break;
}, request.data.timeMs);
break;
}
case CLEAR_TIMEOUT: {
if (timer) {
clearTimeout(timer);
}
}
};
`;
// @ts-ignore
export const timerWorkerScript = URL.createObjectURL(new Blob([ code ], { type: 'application/javascript' }));
break;
}
}
};

View File

@@ -153,10 +153,15 @@ function _endpointMessageReceived({ dispatch, getState }: IStore, next: Function
newTranscriptMessage.unstable = text;
}
dispatch(
updateTranscriptMessage(
transcriptMessageID,
newTranscriptMessage));
dispatch(updateTranscriptMessage(transcriptMessageID, newTranscriptMessage));
// Notify the external API too.
if (typeof APP !== 'undefined') {
APP.API.notifyTranscriptionChunkReceived({
messageID: transcriptMessageID,
...newTranscriptMessage
});
}
}
} catch (error) {
logger.error('Error occurred while updating transcriptions\n', error);

View File

@@ -73,7 +73,7 @@ const useStyles = makeStyles<{ overflowDrawer: boolean; reactionsMenuHeight: num
return {
overflowMenuDrawer: {
overflow: 'hidden',
height: `calc(${DRAWER_MAX_HEIGHT} - ${reactionsMenuHeight}px - 16px)`
height: `calc(${DRAWER_MAX_HEIGHT})`
},
contextMenu: {
position: 'relative' as const,
@@ -238,6 +238,7 @@ const OverflowMenuButton = ({
trigger = 'click'
visible = { isOpen }>
<OverflowToggleButton
isMenuButton = { true }
isOpen = { isOpen }
onKeyDown = { onEscClick } />
</Popover>

View File

@@ -35,6 +35,7 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
_additionalCardTemplate: HTMLTemplateElement | null;
_additionalContentTemplate: HTMLTemplateElement | null;
_additionalToolbarContentTemplate: HTMLTemplateElement | null;
_titleHasNotAllowCharacter: boolean;
/**
* Default values for {@code WelcomePage} component's properties.
@@ -61,6 +62,14 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
};
/**
* Used To display a warning massage if the title input has no allow character.
*
* @private
* @type {boolean}
*/
this._titleHasNotAllowCharacter = false;
/**
* The HTML Element used as the container for additional content. Used
* for directly appending the additional content template to the dom.
@@ -205,7 +214,7 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
<SettingsButton
defaultTab = { SETTINGS_TABS.CALENDAR }
isDisplayedOnWelcomePage = { true } />
{ showAdditionalToolbarContent
{showAdditionalToolbarContent
? <div
className = 'settings-toolbar-content'
ref = { this._setAdditionalToolbarContentRef } />
@@ -213,10 +222,10 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
}
</div>
<h1 className = 'header-text-title'>
{ t('welcomepage.headerTitle') }
{t('welcomepage.headerTitle')}
</h1>
<span className = 'header-text-subtitle'>
{ t('welcomepage.headerSubtitle')}
{t('welcomepage.headerSubtitle')}
</span>
<div id = 'enter_room'>
<div className = 'join-meeting-container'>
@@ -232,11 +241,11 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
pattern = { ROOM_NAME_VALIDATE_PATTERN_STR }
placeholder = { this.state.roomPlaceholder }
ref = { this._setRoomInputRef }
title = { t('welcomepage.roomNameAllowedChars') }
type = 'text'
value = { this.state.room } />
</form>
</div>
<button
aria-disabled = 'false'
aria-label = 'Start meeting'
@@ -245,17 +254,27 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
onClick = { this._onFormSubmit }
tabIndex = { 0 }
type = 'button'>
{ t('welcomepage.startMeeting') }
{t('welcomepage.startMeeting')}
</button>
</div>
</div>
{ this._renderInsecureRoomNameWarning() }
{this._titleHasNotAllowCharacter && (
<div
className = 'not-allow-title-character-div'
role = 'alert'>
<Icon src = { IconWarning } />
<span className = 'not-allow-title-character-text'>
{t('welcomepage.roomNameAllowedChars')}
</span>
</div>
)}
{this._renderInsecureRoomNameWarning()}
{ _moderatedRoomServiceUrl && (
{_moderatedRoomServiceUrl && (
<div id = 'moderated-meetings'>
{
translateToHTML(
t, 'welcomepage.moderatedMessage', { url: _moderatedRoomServiceUrl })
t, 'welcomepage.moderatedMessage', { url: _moderatedRoomServiceUrl })
}
</div>)}
</div>
@@ -264,22 +283,22 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
<div className = 'welcome-cards-container'>
<div className = 'welcome-card-column'>
<div className = 'welcome-tabs welcome-card welcome-card--blue'>
{ this._renderTabs() }
{this._renderTabs()}
</div>
{ showAdditionalCard
{showAdditionalCard
? <div
className = 'welcome-card welcome-card--dark'
ref = { this._setAdditionalCardRef } />
: null }
: null}
</div>
{ showAdditionalContent
{showAdditionalContent
? <div
className = 'welcome-page-content'
ref = { this._setAdditionalContentRef } />
: null }
: null}
</div>
{ DISPLAY_WELCOME_FOOTER && this._renderFooter()}
{DISPLAY_WELCOME_FOOTER && this._renderFooter()}
</div>
);
@@ -295,7 +314,7 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
<div className = 'insecure-room-name-warning'>
<Icon src = { IconWarning } />
<span>
{ getUnsafeRoomText(this.props.t, 'welcome') }
{getUnsafeRoomText(this.props.t, 'welcome')}
</span>
</div>
);
@@ -329,6 +348,9 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
// @ts-ignore
// eslint-disable-next-line require-jsdoc
_onRoomChange(event: React.ChangeEvent<HTMLInputElement>) {
const specialCharacters = [ '?', '&', ':', '\'', '"', '%', '#', '.' ];
this._titleHasNotAllowCharacter = specialCharacters.some(char => event.target.value.includes(char));
super._onRoomChange(event.target.value);
}
@@ -342,8 +364,10 @@ class WelcomePage extends AbstractWelcomePage<IProps> {
t,
_deeplinkingCfg: {
ios = { downloadLink: undefined },
android = { fDroidUrl: undefined,
downloadLink: undefined }
android = {
fDroidUrl: undefined,
downloadLink: undefined
}
}
} = this.props;

View File

@@ -292,10 +292,12 @@ end);
module:hook('muc-occupant-groupchat', function(event)
local occupant, room, stanza = event.occupant, event.room, event.stanza;
local from = stanza.attr.from;
local occupant_host = jid.host(occupant.bare_jid);
local occupant_host;
-- if there is no occupant this is a message from main, probably coming from other vnode
if occupant then
occupant_host = jid.host(occupant.bare_jid);
-- we manage nick only for visitors
if occupant_host ~= main_domain then
-- add to message stanza display name for the visitor

View File

@@ -50,9 +50,7 @@ module:hook("muc-occupant-pre-join", function (event)
return;
end
-- FIX ME: luacheck warning 581
-- not (x == y)' can be replaced by 'x ~= y' (if neither side is a table or NaN)
if not (subdomain == session.jitsi_meet_domain) then
if session.jitsi_meet_domain ~= '*' and subdomain ~= session.jitsi_meet_domain then
module:log('debug', 'skip allowners for auth user and non matching room subdomain: %s, jwt subdomain: %s',
subdomain, session.jitsi_meet_domain);
return;
@@ -138,7 +136,7 @@ function filter_admin_set_query(event)
local _aff = item.attr.affiliation;
-- if it is a moderated room we skip it
if is_moderated(room.jid) then
if room and is_moderated(room.jid) then
return nil;
end

View File

@@ -342,12 +342,11 @@ end
function on_breakout_room_pre_create(event)
local breakout_room = event.room;
local main_room, main_room_jid = get_main_room(breakout_room.jid);
local name = main_room._data.breakout_rooms[breakout_room.jid];
-- Only allow existent breakout rooms to be started.
-- Authorisation of breakout rooms is done by their random uuid name
if main_room and main_room._data.breakout_rooms and name then
breakout_room:set_subject(breakout_room.jid, name);
if main_room and main_room._data.breakout_rooms and main_room._data.breakout_rooms[breakout_room.jid] then
breakout_room:set_subject(breakout_room.jid, main_room._data.breakout_rooms[breakout_room.jid]);
else
module:log('debug', 'Invalid breakout room %s will not be created.', breakout_room.jid);
breakout_room:destroy(main_room_jid, 'Breakout room is invalid.');

View File

@@ -62,7 +62,7 @@ module:hook("muc-occupant-pre-join", function(event)
module:log("debug", "Bypass lobby invitee %s", occupant_jid)
occupant.role = "participant";
room:set_affiliation(true, jid_bare(occupant_jid), "member")
room:save();
room:save_occupant(occupant);
end
-- bypass password on the flip device
local join = stanza:get_child("x", MUC_NS);
@@ -111,12 +111,19 @@ end)
module:hook("muc-occupant-joined", function(event)
local room, occupant = event.room, event.occupant;
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
return ;
return;
end
if room._data.flip_participant_nick and occupant.nick == room._data.flip_participant_nick then
-- make joining participant from flip device have the same role and affiliation as for the previous device
local kicked_occupant = room:get_occupant_by_nick(room._data.kicked_participant_nick);
if not kicked_occupant then
module:log("info", "Kick participant not found, nick %s from main room jid %s",
room._data.kicked_participant_nick, room.jid)
return;
end
local initial_affiliation = room:get_affiliation(kicked_occupant.jid) or "member";
module:log("debug", "Transfer affiliation %s to occupant jid %s", initial_affiliation, occupant.jid)
room:set_affiliation(true, occupant.bare_jid, initial_affiliation)
@@ -129,7 +136,7 @@ module:hook("muc-occupant-joined", function(event)
local kicked_participant_node_jid = jid.split(kicked_occupant.jid);
module:log("info", "Kick participant jid %s nick %s from main room jid %s", kicked_occupant.jid, room._data.kicked_participant_nick, room.jid)
room:set_role(true, room._data.kicked_participant_nick, 'none')
room:save()
room:save_occupant(occupant);
-- Kick participant from the first device from the lobby room
if room._data.lobbyroom then
local lobby_room_jid = room._data.lobbyroom;

View File

@@ -165,6 +165,12 @@ function filter_stanza(stanza)
-- allow messages to or from moderator
local lobby_room_jid = jid_bare(stanza.attr.from);
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
if not lobby_room then
module:log('warn', 'No lobby room found %s', stanza.attr.from);
return nil;
end
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
@@ -396,7 +402,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
if not affiliation or affiliation == 'none' or affiliation == 'member' then
occupant.role = 'participant';
room:set_affiliation(true, invitee_bare_jid, 'member');
room:save();
room:save_occupant(occupant);
return;
end

View File

@@ -3,13 +3,15 @@
local queue = require "util.queue";
local new_throttle = require "util.throttle".create;
local timer = require "util.timer";
local st = require "util.stanza";
-- we max to 500 participants per meeting so this should be enough, we are not suppose to handle all
-- participants in one meeting
local PRESENCE_QUEUE_MAX_SIZE = 1000;
-- default to 5 participants per second
local join_rate_per_conference = module:get_option_number("muc_rate_joins", 5);
-- default to 3 participants per second
local join_rate_per_conference = module:get_option_number("muc_rate_joins", 3);
local leave_rate_per_conference = module:get_option_number("muc_rate_leaves", 5);
-- Measure/monitor the room rate limiting queue
local measure = require "core.statsmanager".measure;
@@ -31,16 +33,20 @@ local stat_longest_queue = 0;
-- Adds item to the queue
-- @returns false if queue is full and item was not added, true otherwise
local function add_item_to_queue(joining_queue, item, room, from)
if not joining_queue:push(item) then
module:log('error', 'Error pushing presence in queue for %s in %s', from, room.jid);
local function add_item_to_queue(queue, item, room, from, send_stats)
if not queue:push(item) then
module:log('error',
'Error pushing item in %s queue for %s in %s', send_stats and 'join' or 'leave', from, room.jid);
if send_stats then
measure_full_queue();
end
measure_full_queue();
return false;
else
-- check is this the longest queue and if so throws a stat
if joining_queue:count() > stat_longest_queue then
stat_longest_queue = joining_queue:count();
if send_stats and queue:count() > stat_longest_queue then
stat_longest_queue = queue:count();
measure_longest_queue(stat_longest_queue);
end
@@ -50,27 +56,23 @@ end
-- process join_rate_presence_queue in the room and pops element passing them to handle_normal_presence
-- returns 1 if we want to reschedule it after 1 second
local function timer_process_queue_elements (room)
local presence_queue = room.join_rate_presence_queue;
if not presence_queue or presence_queue:count() == 0 then
local function timer_process_queue_elements (rate, queue, process, queue_empty_cb)
if not queue or queue:count() == 0 then
return;
end
for _ = 1, join_rate_per_conference do
local ev = presence_queue:pop();
for _ = 1, rate do
local ev = queue:pop();
if ev then
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
ev.stanza.delayed_join_skip = true;
room:handle_normal_presence(ev.origin, ev.stanza);
process(ev);
end
end
-- if there are elements left, schedule an execution in a second
if presence_queue:count() > 0 then
if queue:count() > 0 then
return 1;
else
room.join_rate_queue_timer = false;
queue_empty_cb();
end
end
@@ -99,17 +101,28 @@ module:hook("muc-occupant-pre-join", function (event)
room.join_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
end
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from) then
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true) then
-- let's not stop processing the event
return nil;
end
if not room.join_rate_queue_timer then
timer.add_task(1, function ()
local status, result = pcall(timer_process_queue_elements, room);
local status, result = pcall(timer_process_queue_elements,
join_rate_per_conference,
room.join_rate_presence_queue,
function(ev)
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
ev.stanza.delayed_join_skip = true;
room:handle_normal_presence(ev.origin, ev.stanza);
end,
function() -- empty callback
room.join_rate_queue_timer = false;
end
);
if not status then
-- there was an error in the timer function
module:log('error', 'Error processing queue: %s', result);
module:log('error', 'Error processing join queue: %s', result);
measure_errors_processing_queue();
@@ -130,7 +143,7 @@ module:hook("muc-occupant-pre-join", function (event)
-- if add fails as queue is full we return false and the event will continue processing, we risk re-order
-- but not losing it
return add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from);
return add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true);
end
end, 9); -- as we will rate limit joins we need to be the first to execute
@@ -141,3 +154,73 @@ end, 9); -- as we will rate limit joins we need to be the first to execute
module:hook('muc-room-destroyed',function(event)
event.room.join_rate_presence_queue = nil;
end);
module:hook('muc-occupant-pre-leave', function (event)
local occupant, room, stanza = event.occupant, event.room, event.stanza;
local throttle = room.leave_rate_throttle;
if not throttle then
throttle = new_throttle(leave_rate_per_conference, 1); -- rate per one second
room.leave_rate_throttle = throttle;
end
if not throttle:poll(1) then
if not room.leave_rate_presence_queue then
room.leave_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
end
-- we need it later when processing the event
event.orig_role = occupant.role;
if not add_item_to_queue(room.leave_rate_presence_queue, event, room, stanza.attr.from, false) then
-- let's not stop processing the event
return nil;
end
-- set role to nil so the occupant will be removed from room occupants when we save it
-- we remove occupant from the list early on batches so we can spare sending few presences
occupant.role = nil;
room:save_occupant(occupant);
if not room.leave_rate_queue_timer then
timer.add_task(1, function ()
local status, result = pcall(timer_process_queue_elements,
leave_rate_per_conference,
room.leave_rate_presence_queue,
function(ev)
local occupant, orig_role, origin, room, stanza
= ev.occupant, ev.orig_role, ev.origin, ev.room, ev.stanza;
room:publicise_occupant_status(
occupant,
st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}),
nil, nil, nil, orig_role);
module:fire_event("muc-occupant-left", {
room = room;
nick = occupant.nick;
occupant = occupant;
origin = origin;
stanza = stanza;
});
end,
function() -- empty callback
room.leave_rate_queue_timer = false;
end
);
if not status then
-- there was an error in the timer function
module:log('error', 'Error processing leave queue: %s', result);
-- let's re-schedule timer so we do not lose the queue
return 1;
end
return result;
end);
room.leave_rate_queue_timer = true;
end
return true; -- we stop execution, so we do not process this leave at the moment
end
end);

View File

@@ -4,5 +4,7 @@
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
local room_metadata_component_host = module:get_option_string('room_metadata_component', 'metadata.'..module.host);
module:depends("jitsi_session");
-- Advertise the component so clients can pick up the address and use it
module:add_identity('component', COMPONENT_IDENTITY_TYPE, room_metadata_component_host);

View File

@@ -35,15 +35,16 @@ end
-- Searches all rooms in the main muc component that holds a breakout room
-- caches it if found so we don't search it again
-- we should not cache objects in _data as this is being serialized when calling room:save()
local function get_main_room(breakout_room)
if breakout_room._data and breakout_room._data.main_room then
return breakout_room._data.main_room;
if breakout_room.main_room then
return breakout_room.main_room;
end
-- let's search all rooms to find the main room
for room in main_muc_service.each_room() do
if room._data and room._data.breakout_rooms_active and room._data.breakout_rooms[breakout_room.jid] then
breakout_room._data.main_room = room;
breakout_room.main_room = room;
return room;
end
end
@@ -117,7 +118,7 @@ function on_message(event)
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
if not occupant or not room.speakerStats[occupant.jid] then
module:log("warn", "No occupant %s found for %s", from, roomAddress);
return false;
end

View File

@@ -17,6 +17,7 @@ local starts_with = main_util.starts_with;
local cjson_safe = require 'cjson.safe'
local timer = require "util.timer";
local async = require "util.async";
local inspect = require 'inspect';
local nr_retries = 3;
local ssl = require "ssl";
@@ -197,6 +198,7 @@ end
-- session.jitsi_meet_room - the room name value from the token
-- session.jitsi_meet_domain - the domain name value from the token
-- session.jitsi_meet_context_user - the user details from the token
-- session.jitsi_meet_context_room - the room details from the token
-- session.jitsi_meet_context_group - the group value from the token
-- session.jitsi_meet_context_features - the features value from the token
-- @param session the current session
@@ -344,7 +346,11 @@ function Util:verify_room(session, room_address)
local auth_room = session.jitsi_meet_room;
if auth_room then
auth_room = string.lower(auth_room);
if type(auth_room) == 'string' then
auth_room = string.lower(auth_room);
else
module:log('warn', 'session.jitsi_meet_room not string: %s', inspect(auth_room));
end
end
if not self.enableDomainVerification then
-- if auth_room is missing, this means user is anonymous (no token for

View File

@@ -383,6 +383,17 @@ module.exports = (_env, argv) => {
globalObject: 'AudioWorkletGlobalScope'
}
}),
Object.assign({}, config, {
entry: {
'screenshot-capture-worker': './react/features/screenshot-capture/worker.ts'
},
plugins: [
...config.plugins,
...getBundleAnalyzerPlugin(analyzeBundle, 'screenshot-capture-worker')
],
performance: getPerformanceHints(perfHintOptions, 4 * 1024)
})
];
};