Compare commits

...

33 Commits

Author SHA1 Message Date
damencho
895afbab65 fix: Fixes polls parsing. 2024-02-12 13:21:06 -06:00
Saúl Ibarra Corretgé
1d6529af65 feat(transcript) add ability to dump transcript for debugging 2024-02-12 13:07:19 +01:00
Saúl Ibarra Corretgé
4d5fb719d2 fix(whiteboard) handle metadata being null 2024-02-12 13:05:53 +01:00
Jaya Allamsetty
4061a77af8 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1770.0.0+2d430a18...v1773.0.0+cc9b0e60
2024-02-09 11:20:51 -05:00
Mihaela Dumitru
7f889b2028 feat(prejoin) display recording warning (#14347) 2024-02-09 11:00:28 +02:00
bgrozev
6f49041d80 feat: Notify jicofo when room_metadata changes the config form. (#14346) 2024-02-08 14:58:02 -08:00
Avram Tudor
e73c3b6697 feat(recordings) send recordings metadata
* ref: centralise conference metadata updates

* feat(recordings) send recordings metadata
2024-02-08 21:38:06 +02:00
Avram Tudor
5a6b1d0b47 code review 2024-02-08 17:41:55 +02:00
Avram Tudor
da9cded75b fix(recording) ensure recording label is shown when transcription is on 2024-02-08 17:41:55 +02:00
Avram Tudor
9b61ad3616 fix(recording) do not show advanced options for users with local recording rights 2024-02-08 17:41:55 +02:00
Avram Tudor
583725bf31 fix(recording) do not show stop recording option in some wrong cases 2024-02-08 17:41:55 +02:00
Avram Tudor
9e2244210d ref: add selector to check if meeting is transcribed 2024-02-08 17:41:55 +02:00
Avram Tudor
361b82a1ed ref: add selector for checking cloud recording running status 2024-02-08 17:41:55 +02:00
Saúl Ibarra Corretgé
0ed25cda7e fix(ci) use the right CocoaPods version 2024-02-08 15:08:15 +01:00
Calinteodor
73ee1205eb feat(premeeting/prejoin): Control room name visibility (#14339)
feat(premeeting/prejoin): Control room name visibility (#14339)
2024-02-08 15:56:05 +02:00
damencho
21f2c60638 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1769.0.0+ff9f57c5...v1770.0.0+2d430a18
2024-02-07 14:53:46 -06:00
damencho
fd062c40fb fix: Use same format for anonymous jids as token module. 2024-02-07 14:53:36 -06:00
damencho
b87e6abc11 feat: Sends event on not allowed error.
Updates amplitude to include the update about sending events on exiting the page.
2024-02-07 13:08:35 -06:00
Avram Tudor
9f25726706 fix(recording) fix recording suggestion not being shown in some cases (#14333)
Initial implementation did not account for cases where participants become moderators
2024-02-07 15:36:43 +02:00
Calinteodor
d5ee7f3069 feat(mobile/polyfills): strophe.js for react native (#14329)
* chore(deps, abab): polyfill using dependency for strophe in react native
2024-02-06 18:28:35 +02:00
damencho
ba1102100a feat(visitors): Adds conference duration for visitors. 2024-02-06 10:16:30 -06:00
damencho
e1ce83d0c3 feat: Use room data form for conference duration.
Deprecates current component for conference duration. Will wait a bit for mobile to catch up and then we can delete the code.

chore(deps) lib-jitsi-meet@latest
jitsi/lib-jitsi-meet@v1767.0.0+178f9bbf...v1769.0.0+ff9f57c5
2024-02-06 10:16:30 -06:00
damencho
78cf510c0b feat: Moves common process_host_module to util. 2024-02-06 10:16:30 -06:00
Mihaela Dumitru
3f657c3ded feat(whiteboard) add native implementation (#14327) 2024-02-06 16:22:53 +02:00
Saúl Ibarra Corretgé
2035cd7e62 fix(recording) avoid having duplicate notification sounds 2024-02-06 11:14:12 +01:00
Saúl Ibarra Corretgé
6207e95cad fix(recording) fix default value to auto-transcribe-on-record 2024-02-06 11:14:12 +01:00
Calinteodor
40b63a187a feat(conference): remove one2one check for diplaying participants name (#14324)
* feat(conference): remove one2one check for diplaying participants name
2024-02-06 10:54:27 +02:00
Saúl Ibarra Corretgé
6c40250e18 fix(conference) avoid adding extra endpoint message listeners 2024-02-06 07:40:40 +01:00
Saúl Ibarra Corretgé
0268374b88 fix(conference) move endpointMessageReceived action
It's used for a myrias of things, not just subtitles.
2024-02-06 07:40:40 +01:00
Jaya Allamsetty
b1b60ec143 fix(config): Fix the typo in Av1 support flag. 2024-02-05 13:27:29 -05:00
Дамян Минков
82c58178b3 feat(visitors): Adds notification for promotion requests. (#14316)
* feat(visitors): Adds notification for promotion requests.

* squash: fixes lint.
2024-02-02 14:58:24 -06:00
Jaya Allamsetty
46d15a9c5c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1764.0.0+ba21a236...v1767.0.0+178f9bbf
2024-02-02 14:58:09 -06:00
Hristo Terezov
248908f476 fix(lobby): Hide enable option if there is a visitor 2024-02-02 14:20:16 -06:00
118 changed files with 2186 additions and 961 deletions

View File

@@ -73,6 +73,10 @@ jobs:
node-version: 16
cache: 'npm'
- run: npm install
- name: setup-cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
with:
podfile-path: ios/Podfile.lock
- name: Install Pods
run: |
pod --version

View File

@@ -38,6 +38,7 @@ import {
dataChannelClosed,
dataChannelOpened,
e2eRttChanged,
endpointMessageReceived,
kickedOut,
lockStateChanged,
nonParticipantMessageReceived,
@@ -162,7 +163,6 @@ import { isScreenAudioShared } from './react/features/screen-share/functions';
import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture/actions';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
import { endpointMessageReceived } from './react/features/subtitles/actions.any';
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
import { muteLocal } from './react/features/video-menu/actions.any';
import { iAmVisitor } from './react/features/visitors/functions';
@@ -1824,28 +1824,24 @@ export default {
room.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args) => {
APP.store.dispatch(endpointMessageReceived(...args));
if (args && args.length >= 2) {
const [ sender, eventData ] = args;
if (eventData.name === ENDPOINT_TEXT_MESSAGE_NAME) {
APP.API.notifyEndpointTextMessageReceived({
senderInfo: {
jid: sender._jid,
id: sender._id
},
eventData
});
}
(participant, data) => {
APP.store.dispatch(endpointMessageReceived(participant, data));
if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
APP.API.notifyEndpointTextMessageReceived({
senderInfo: {
jid: participant.getJid(),
id: participant.getId()
},
eventData: data
});
}
});
room.on(
JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
(...args) => {
APP.store.dispatch(nonParticipantMessageReceived(...args));
APP.API.notifyNonParticipantMessageReceived(...args);
(id, data) => {
APP.store.dispatch(nonParticipantMessageReceived(id, data));
APP.API.notifyNonParticipantMessageReceived(id, data);
});
room.on(

View File

@@ -85,7 +85,7 @@ var config = {
// disableE2EE: false,
// Enables supports for AV1 codec.
// enableAv1: false,
// enableAv1Support: false,
// Enables XMPP WebSocket (as opposed to BOSH) for the given amount of users.
// mobileXmppWsThreshold: 10, // enable XMPP WebSockets on mobile for 10% of the users
@@ -103,6 +103,9 @@ var config = {
// Experiment: Whether to skip interim transcriptions.
// skipInterimTranscriptions: false,
// Dump transcripts to a <transcript> element for debugging.
// dumpTranscript: false,
},
// Disables moderator indicators.
@@ -329,6 +332,9 @@ var config = {
// // If true, shows a notification at the start of the meeting with a call to action button
// // to start recording (for users who can do so).
// // suggestRecording: true,
// // If true, shows a warning label in the prejoin screen to point out the possibility that
// // the call you're joining might be recorded.
// // showPrejoinWarning: true,
// },
// recordingService: {

View File

@@ -36,6 +36,7 @@ $flagsImagePath: "../images/";
@import 'modals/invite/info';
@import 'modals/screen-share/share-audio';
@import 'modals/screen-share/share-screen-warning';
@import 'modals/whiteboard';
@import 'videolayout_default';
@import 'subject';
@import 'popup_menu';

View File

@@ -0,0 +1,7 @@
.whiteboard {
.excalidraw-wrapper {
height: 100vh;
width: 100vw;
}
}

View File

@@ -51,7 +51,6 @@ VirtualHost "jitmeet.example.com"
}
av_moderation_component = "avmoderation.jitmeet.example.com"
speakerstats_component = "speakerstats.jitmeet.example.com"
conference_duration_component = "conferenceduration.jitmeet.example.com"
end_conference_component = "endconference.jitmeet.example.com"
-- we need bosh
modules_enabled = {
@@ -130,9 +129,6 @@ Component "focus.jitmeet.example.com" "client_proxy"
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
muc_component = "conference.jitmeet.example.com"
Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
muc_component = "conference.jitmeet.example.com"
Component "endconference.jitmeet.example.com" "end_conference"
muc_component = "conference.jitmeet.example.com"

1
globals.d.ts vendored
View File

@@ -21,6 +21,7 @@ declare global {
JitsiMeetElectron?: any;
PressureObserver?: any;
PressureRecord?: any;
ReactNativeWebView?: any;
// selenium tests handler
_sharedVideoPlayer: any;
alwaysOnTop: { api: any };

3
globals.native.d.ts vendored
View File

@@ -19,6 +19,9 @@ interface IWindow {
location: ILocation;
PressureObserver?: any;
PressureRecord?: any;
ReactNativeWebView?: any;
TextDecoder?: any;
TextEncoder?: any;
self: any;
top: any;

View File

@@ -560,6 +560,7 @@
"noNumbers": "No dial-in numbers.",
"noPassword": "None",
"noRoom": "No room was specified to dial-in into.",
"noWhiteboard": "Could not load the whiteboard.",
"numbers": "Dial-in Numbers",
"password": "$t(lockRoomPasswordUppercase): ",
"reachedLimit": "You have reached the limit of your plan.",
@@ -567,7 +568,8 @@
"sipAudioOnly": "SIP audio only address",
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting",
"upgradeOptions": "Please check the upgrade options on"
"upgradeOptions": "Please check the upgrade options on",
"whiteboardError": "Error loading the whiteboard. Please try again later."
},
"inlineDialogFailure": {
"msg": "We stumbled a bit.",
@@ -937,6 +939,7 @@
"or": "or",
"premeeting": "Pre meeting",
"proceedAnyway": "Proceed anyway",
"recordingWarning": "This call might be recorded",
"screenSharingError": "Screen sharing error:",
"showScreen": "Enable pre meeting screen",
"startWithPhone": "Start with phone audio",
@@ -1530,6 +1533,7 @@
"whiteboard": {
"accessibilityLabel": {
"heading": "Whiteboard"
}
},
"screenTitle": "Whiteboard"
}
}

View File

@@ -113,7 +113,7 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
import { setTileView, toggleTileView } from '../../react/features/video-layout/actions.any';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality/actions';
import { toggleWhiteboard } from '../../react/features/whiteboard/actions.any';
import { toggleWhiteboard } from '../../react/features/whiteboard/actions.web';
import { getJitsiMeetTransport } from '../transport';
import {

230
package-lock.json generated
View File

@@ -42,7 +42,8 @@
"@vladmandic/human": "2.6.5",
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.8.7",
"amplitude-js": "8.2.1",
"abab": "2.0.6",
"amplitude-js": "8.21.9",
"base64-js": "1.5.1",
"bc-css-flags": "3.0.0",
"clipboard-copy": "4.0.1",
@@ -60,7 +61,7 @@
"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/v1764.0.0+ba21a236/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1773.0.0+cc9b0e60/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -110,6 +111,7 @@
"redux-thunk": "2.4.1",
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"text-encoding": "0.7.0",
"tss-react": "4.4.4",
"util": "0.12.1",
"uuid": "8.3.2",
@@ -125,7 +127,7 @@
"@babel/preset-env": "7.21.5",
"@babel/preset-react": "7.16.0",
"@jitsi/eslint-config": "4.1.5",
"@types/amplitude-js": "8.16.2",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
"@types/dom-screen-wake-lock": "1.0.1",
"@types/js-md5": "0.4.3",
@@ -236,6 +238,11 @@
"extraneous": true,
"devDependencies": {}
},
"node_modules/@amplitude/analytics-connector": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
},
"node_modules/@amplitude/react-native": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@amplitude/react-native/-/react-native-2.7.0.tgz",
@@ -246,38 +253,43 @@
}
},
"node_modules/@amplitude/types": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.9.2.tgz",
"integrity": "sha512-s+Q/O8kNfocZiyGvVdtM5T4JGPwLRZ4Q26wtEF5xJhyCtJglGMe0ixe6u/6iW9s4JHIq+LlPlUu5095pVsdtNA==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
"integrity": "sha512-I8qenRI7uU6wKNb9LiZrAosSHVoNHziXouKY81CrqxH9xhVTEIJFXeuCV0hbtBr0Al/8ejnGjQRx+S2SvU/pPg==",
"engines": {
"node": ">=10"
}
},
"node_modules/@amplitude/ua-parser-js": {
"version": "0.7.24",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
"integrity": "sha512-VbQuJymJ20WEw0HtI2np7EdC3NJGUWi8+Xdbc7uk8WfMIF308T0howpzkQ3JFMN7ejnrcSM/OyNGveeE3TP3TA==",
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/@amplitude/utils": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.9.2.tgz",
"integrity": "sha512-hGOIoIjmZ0pq/3b2gBrr17TaEarlR+qzFGu5npm76+scd/51F0eNvjd0vgV6WbJT1cxhyH/5Z8kihGWOU3vS3Q==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.2.tgz",
"integrity": "sha512-tVsHXu61jITEtRjB7NugQ5cVDd4QDzne8T3ifmZye7TiJeUfVRvqe44gDtf55A+7VqhDhyEIIXTA1iVcDGqlEw==",
"dependencies": {
"@amplitude/types": "^1.9.2",
"tslib": "^1.9.3"
"@amplitude/types": "^1.10.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@amplitude/utils/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -6497,9 +6509,9 @@
}
},
"node_modules/@types/amplitude-js": {
"version": "8.16.2",
"resolved": "https://registry.npmjs.org/@types/amplitude-js/-/amplitude-js-8.16.2.tgz",
"integrity": "sha512-a+tb/CEQOlrHRvEvAuYNOcoUy1POERANnAhfKgiTmsy0eACj3eukGP0ucA9t115QOPzVUhbnUfZqtyHp99IZyA==",
"version": "8.16.5",
"resolved": "https://registry.npmjs.org/@types/amplitude-js/-/amplitude-js-8.16.5.tgz",
"integrity": "sha512-W73JfDpwDH4VijOGo+nVuQOqUCiqyEGGVdajU4ziWTLn27cn+QtFuFuBdlhCraIIrO52fDRO4NSOGkawtn77Jw==",
"dev": true
},
"node_modules/@types/audioworklet": {
@@ -7516,7 +7528,8 @@
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="
"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
"deprecated": "Use your platform's native atob() and btoa() methods instead"
},
"node_modules/abort-controller": {
"version": "3.0.0",
@@ -7655,36 +7668,62 @@
}
},
"node_modules/amplitude-js": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.2.1.tgz",
"integrity": "sha512-jp8lm/koTNRceO16RCTlQg9+gUbxip1esod+d0oApBCJYpxuABec2bLHXv/OkVYICvnUWoiz17AZLxiaX/aK4Q==",
"deprecated": "Excessive logging into console at default levels",
"version": "8.21.9",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
"integrity": "sha512-d0jJH00wbXu7sxKtVwkdSXtVffjqdUrxuACKlnzP7jU5qt9wriXXMgHifdH5Oq+buKmyF8wKL9S02gAykysURA==",
"dependencies": {
"@amplitude/ua-parser-js": "0.7.24",
"@amplitude/utils": "^1.0.5",
"blueimp-md5": "^2.10.0",
"query-string": "5"
"@amplitude/analytics-connector": "^1.4.6",
"@amplitude/ua-parser-js": "0.7.33",
"@amplitude/utils": "^1.10.2",
"@babel/runtime": "^7.21.0",
"blueimp-md5": "^2.19.0",
"query-string": "8.1.0"
}
},
"node_modules/amplitude-js/node_modules/decode-uri-component": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
"engines": {
"node": ">=14.16"
}
},
"node_modules/amplitude-js/node_modules/filter-obj": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
"integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/amplitude-js/node_modules/query-string": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz",
"integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
"dependencies": {
"decode-uri-component": "^0.2.0",
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
"decode-uri-component": "^0.4.1",
"filter-obj": "^5.1.0",
"split-on-first": "^3.0.0"
},
"engines": {
"node": ">=0.10.0"
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/amplitude-js/node_modules/strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
"node_modules/amplitude-js/node_modules/split-on-first": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz",
"integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==",
"engines": {
"node": ">=0.10.0"
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/anser": {
@@ -12904,8 +12943,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1764.0.0+ba21a236/lib-jitsi-meet.tgz",
"integrity": "sha512-k5S3TSB3LqDMxfZfzf4PKhHpVH2pxSBpU2yaomw4XjqBCxsBCIwxthRvfEg2ZF3XWjKaCgH2eUgIhMq7T+1gYg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1773.0.0+cc9b0e60/lib-jitsi-meet.tgz",
"integrity": "sha512-i1ygVANxENUVAuWpo/KPcgk1LW0w6/LVnib9+xfcvZs7f1o/58eolG0k/rCJEUEpUWivHgdY7tKTbhHj8yPVDQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -18625,6 +18664,12 @@
"node": ">=0.10.0"
}
},
"node_modules/text-encoding": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==",
"deprecated": "no longer maintained"
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -20133,35 +20178,33 @@
}
},
"dependencies": {
"@amplitude/analytics-connector": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
},
"@amplitude/react-native": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@amplitude/react-native/-/react-native-2.7.0.tgz",
"integrity": "sha512-2dMxCVgRPwReHRDm9JKbL+sZGyozJlcdr5Jokv8TQR7idNxxGmm4YSYkjhGjSWkoEyGEyy+lh9kRJQL/DcUWJQ=="
},
"@amplitude/types": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.9.2.tgz",
"integrity": "sha512-s+Q/O8kNfocZiyGvVdtM5T4JGPwLRZ4Q26wtEF5xJhyCtJglGMe0ixe6u/6iW9s4JHIq+LlPlUu5095pVsdtNA=="
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
"integrity": "sha512-I8qenRI7uU6wKNb9LiZrAosSHVoNHziXouKY81CrqxH9xhVTEIJFXeuCV0hbtBr0Al/8ejnGjQRx+S2SvU/pPg=="
},
"@amplitude/ua-parser-js": {
"version": "0.7.24",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.24.tgz",
"integrity": "sha512-VbQuJymJ20WEw0HtI2np7EdC3NJGUWi8+Xdbc7uk8WfMIF308T0howpzkQ3JFMN7ejnrcSM/OyNGveeE3TP3TA=="
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA=="
},
"@amplitude/utils": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.9.2.tgz",
"integrity": "sha512-hGOIoIjmZ0pq/3b2gBrr17TaEarlR+qzFGu5npm76+scd/51F0eNvjd0vgV6WbJT1cxhyH/5Z8kihGWOU3vS3Q==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.2.tgz",
"integrity": "sha512-tVsHXu61jITEtRjB7NugQ5cVDd4QDzne8T3ifmZye7TiJeUfVRvqe44gDtf55A+7VqhDhyEIIXTA1iVcDGqlEw==",
"requires": {
"@amplitude/types": "^1.9.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
"@amplitude/types": "^1.10.2",
"tslib": "^2.0.0"
}
},
"@ampproject/remapping": {
@@ -24602,9 +24645,9 @@
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
},
"@types/amplitude-js": {
"version": "8.16.2",
"resolved": "https://registry.npmjs.org/@types/amplitude-js/-/amplitude-js-8.16.2.tgz",
"integrity": "sha512-a+tb/CEQOlrHRvEvAuYNOcoUy1POERANnAhfKgiTmsy0eACj3eukGP0ucA9t115QOPzVUhbnUfZqtyHp99IZyA==",
"version": "8.16.5",
"resolved": "https://registry.npmjs.org/@types/amplitude-js/-/amplitude-js-8.16.5.tgz",
"integrity": "sha512-W73JfDpwDH4VijOGo+nVuQOqUCiqyEGGVdajU4ziWTLn27cn+QtFuFuBdlhCraIIrO52fDRO4NSOGkawtn77Jw==",
"dev": true
},
"@types/audioworklet": {
@@ -25551,30 +25594,42 @@
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
},
"amplitude-js": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.2.1.tgz",
"integrity": "sha512-jp8lm/koTNRceO16RCTlQg9+gUbxip1esod+d0oApBCJYpxuABec2bLHXv/OkVYICvnUWoiz17AZLxiaX/aK4Q==",
"version": "8.21.9",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
"integrity": "sha512-d0jJH00wbXu7sxKtVwkdSXtVffjqdUrxuACKlnzP7jU5qt9wriXXMgHifdH5Oq+buKmyF8wKL9S02gAykysURA==",
"requires": {
"@amplitude/ua-parser-js": "0.7.24",
"@amplitude/utils": "^1.0.5",
"blueimp-md5": "^2.10.0",
"query-string": "5"
"@amplitude/analytics-connector": "^1.4.6",
"@amplitude/ua-parser-js": "0.7.33",
"@amplitude/utils": "^1.10.2",
"@babel/runtime": "^7.21.0",
"blueimp-md5": "^2.19.0",
"query-string": "8.1.0"
},
"dependencies": {
"decode-uri-component": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="
},
"filter-obj": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
"integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="
},
"query-string": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz",
"integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz",
"integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
"requires": {
"decode-uri-component": "^0.2.0",
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
"decode-uri-component": "^0.4.1",
"filter-obj": "^5.1.0",
"split-on-first": "^3.0.0"
}
},
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="
"split-on-first": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz",
"integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="
}
}
},
@@ -29472,8 +29527,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1764.0.0+ba21a236/lib-jitsi-meet.tgz",
"integrity": "sha512-k5S3TSB3LqDMxfZfzf4PKhHpVH2pxSBpU2yaomw4XjqBCxsBCIwxthRvfEg2ZF3XWjKaCgH2eUgIhMq7T+1gYg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1773.0.0+cc9b0e60/lib-jitsi-meet.tgz",
"integrity": "sha512-i1ygVANxENUVAuWpo/KPcgk1LW0w6/LVnib9+xfcvZs7f1o/58eolG0k/rCJEUEpUWivHgdY7tKTbhHj8yPVDQ==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
@@ -33629,6 +33684,11 @@
}
}
},
"text-encoding": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA=="
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

View File

@@ -48,7 +48,8 @@
"@vladmandic/human": "2.6.5",
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.8.7",
"amplitude-js": "8.2.1",
"abab": "2.0.6",
"amplitude-js": "8.21.9",
"base64-js": "1.5.1",
"bc-css-flags": "3.0.0",
"clipboard-copy": "4.0.1",
@@ -66,7 +67,7 @@
"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/v1764.0.0+ba21a236/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1773.0.0+cc9b0e60/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -116,6 +117,7 @@
"redux-thunk": "2.4.1",
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"text-encoding": "0.7.0",
"tss-react": "4.4.4",
"util": "0.12.1",
"uuid": "8.3.2",
@@ -131,7 +133,7 @@
"@babel/preset-env": "7.21.5",
"@babel/preset-react": "7.16.0",
"@jitsi/eslint-config": "4.1.5",
"@types/amplitude-js": "8.16.2",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
"@types/dom-screen-wake-lock": "1.0.1",
"@types/js-md5": "0.4.3",

View File

@@ -328,6 +328,22 @@ export function createNetworkInfoEvent({ isOnline, networkType, details }:
};
}
/**
* Creates a "not allowed error" event.
*
* @param {string} reason - The reason for the error.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createNotAllowedErrorEvent(reason: string) {
return {
action: 'not.allowed.error',
attributes: {
reason
}
};
}
/**
* Creates an "offer/answer failure" event.
*

View File

@@ -35,6 +35,12 @@ export default class AmplitudeHandler extends AbstractHandler {
this._enabled = false;
};
// Forces sending all events on exit (flushing) via sendBeacon
const onExitPage = () => {
// @ts-ignore
amplitude.getInstance().sendEvents();
};
if (navigator.product === 'ReactNative') {
amplitude.getInstance().init(amplitudeAPPKey);
fixDeviceID(amplitude.getInstance()).then(() => {
@@ -50,7 +56,8 @@ export default class AmplitudeHandler extends AbstractHandler {
includeReferrer: true,
includeUtm,
saveParamsReferrerOncePerSession: false,
onError
onError,
onExitPage
};
// @ts-ignore

View File

@@ -51,5 +51,6 @@ import '../video-layout/middleware';
import '../video-quality/middleware';
import '../videosipgw/middleware';
import '../visitors/middleware';
import '../whiteboard/middleware.any';
import './middleware';

View File

@@ -13,5 +13,6 @@ import '../mobile/react-native-sdk/middleware';
import '../mobile/watchos/middleware';
import '../share-room/middleware';
import '../shared-video/middleware';
import '../whiteboard/middleware.native';
import './middlewares.any';

View File

@@ -22,6 +22,6 @@ import '../talk-while-muted/middleware';
import '../toolbox/middleware';
import '../face-landmarks/middleware';
import '../gifs/middleware';
import '../whiteboard/middleware';
import '../whiteboard/middleware.web';
import './middlewares.any';

View File

@@ -55,3 +55,4 @@ import '../video-layout/reducer';
import '../video-quality/reducer';
import '../videosipgw/reducer';
import '../visitors/reducer';
import '../whiteboard/reducer';

View File

@@ -16,7 +16,6 @@ import '../noise-suppression/reducer';
import '../screenshot-capture/reducer';
import '../talk-while-muted/reducer';
import '../virtual-background/reducer';
import '../whiteboard/reducer';
import '../web-hid/reducer';
import './reducers.any';

View File

@@ -178,6 +178,18 @@ export const DATA_CHANNEL_OPENED = 'DATA_CHANNEL_OPENED';
*/
export const DATA_CHANNEL_CLOSED = 'DATA_CHANNEL_CLOSED';
/**
* The type of (redux) action which indicates that an endpoint message
* sent by another participant to the data channel is received.
*
* {
* type: ENDPOINT_MESSAGE_RECEIVED,
* participant: Object,
* data: Object
* }
*/
export const ENDPOINT_MESSAGE_RECEIVED = 'ENDPOINT_MESSAGE_RECEIVED';
/**
* The type of action which signals that the user has been kicked out from
* the conference.
@@ -333,3 +345,13 @@ export const SET_START_MUTED_POLICY = 'SET_START_MUTED_POLICY';
* }
*/
export const SET_ASSUMED_BANDWIDTH_BPS = 'SET_ASSUMED_BANDWIDTH_BPS';
/**
* The type of (redux) action which updated the conference metadata.
*
* {
* type: UPDATE_CONFERENCE_METADATA,
* metadata: Object
* }
*/
export const UPDATE_CONFERENCE_METADATA = 'UPDATE_CONFERENCE_METADATA';

View File

@@ -1,7 +1,6 @@
import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState, IStore } from '../../app/types';
import { endpointMessageReceived } from '../../subtitles/actions.any';
import { setIAmVisitor } from '../../visitors/actions';
import { iAmVisitor } from '../../visitors/functions';
import { overwriteConfig } from '../config/actions';
@@ -48,6 +47,7 @@ import {
DATA_CHANNEL_CLOSED,
DATA_CHANNEL_OPENED,
E2E_RTT_CHANGED,
ENDPOINT_MESSAGE_RECEIVED,
KICKED_OUT,
LOCK_STATE_CHANGED,
NON_PARTICIPANT_MESSAGE_RECEIVED,
@@ -61,7 +61,8 @@ import {
SET_PENDING_SUBJECT_CHANGE,
SET_ROOM,
SET_START_MUTED_POLICY,
SET_START_REACTIONS_MUTED
SET_START_REACTIONS_MUTED,
UPDATE_CONFERENCE_METADATA
} from './actionTypes';
import {
AVATAR_URL_COMMAND,
@@ -79,7 +80,7 @@ import {
sendLocalParticipant
} from './functions';
import logger from './logger';
import { IJitsiConference } from './reducer';
import { IConferenceMetadata, IJitsiConference } from './reducer';
/**
* Adds conference (event) listeners.
@@ -275,6 +276,21 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
})));
}
/**
* Action for updating the conference metadata.
*
* @param {IConferenceMetadata} metadata - The metadata object.
* @returns {{
* type: UPDATE_CONFERENCE_METADATA,
* metadata: IConferenceMetadata
* }}
*/
export function updateConferenceMetadata(metadata: IConferenceMetadata | null) {
return {
type: UPDATE_CONFERENCE_METADATA,
metadata
};
}
/**
* Create an action for when the end-to-end RTT against a specific remote participant has changed.
@@ -630,6 +646,25 @@ export function dataChannelClosed(code: number, reason: string) {
};
}
/**
* Signals that a participant sent an endpoint message on the data channel.
*
* @param {Object} participant - The participant details sending the message.
* @param {Object} data - The data carried by the endpoint message.
* @returns {{
* type: ENDPOINT_MESSAGE_RECEIVED,
* participant: Object,
* data: Object
* }}
*/
export function endpointMessageReceived(participant: Object, data: Object) {
return {
type: ENDPOINT_MESSAGE_RECEIVED,
participant,
data
};
}
/**
* Action to end a conference for all participants.
*

View File

@@ -6,6 +6,7 @@ import { MIN_ASSUMED_BANDWIDTH_BPS } from '../../../../modules/API/constants';
import {
ACTION_PINNED,
ACTION_UNPINNED,
createNotAllowedErrorEvent,
createOfferAnswerFailedEvent,
createPinnedEvent
} from '../../analytics/AnalyticsEvents';
@@ -25,7 +26,7 @@ import { overwriteConfig } from '../config/actions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
import { connect, connectionDisconnected, disconnect } from '../connection/actions';
import { validateJwt } from '../jwt/functions';
import { JitsiConferenceErrors, JitsiConnectionErrors } from '../lib-jitsi-meet';
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
import { PARTICIPANT_ROLE } from '../participants/constants';
import {
@@ -34,6 +35,7 @@ import {
getPinnedParticipant
} from '../participants/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
import { getLocalTracks } from '../tracks/functions.any';
@@ -54,7 +56,8 @@ import {
conferenceWillLeave,
createConference,
setLocalSubject,
setSubject
setSubject,
updateConferenceMetadata
} from './actions';
import { CONFERENCE_LEAVE_REASONS } from './constants';
import {
@@ -65,6 +68,7 @@ import {
restoreConferenceOptions
} from './functions';
import logger from './logger';
import { IConferenceMetadata } from './reducer';
/**
* Handler for before unload event.
@@ -124,6 +128,24 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch }, previousConference): void => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: IConferenceMetadata) => {
dispatch(updateConferenceMetadata(metadata));
});
}
if (conference !== previousConference) {
dispatch(updateConferenceMetadata(null));
}
});
/**
* Makes sure to leave a failed conference in order to release any allocated
* resources like peer connections, emit participant left events, etc.
@@ -197,6 +219,12 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
break;
}
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
const [ msg ] = error.params;
sendAnalytics(createNotAllowedErrorEvent(msg));
break;
}
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
sendAnalytics(createOfferAnswerFailedEvent());
break;

View File

@@ -27,7 +27,8 @@ import {
SET_PENDING_SUBJECT_CHANGE,
SET_ROOM,
SET_START_MUTED_POLICY,
SET_START_REACTIONS_MUTED
SET_START_REACTIONS_MUTED,
UPDATE_CONFERENCE_METADATA
} from './actionTypes';
import { isRoomValid } from './functions';
@@ -39,10 +40,23 @@ const DEFAULT_STATE = {
leaving: undefined,
locked: undefined,
membersOnly: undefined,
metadata: undefined,
password: undefined,
passwordRequired: undefined
};
export interface IConferenceMetadata {
recording?: {
isTranscribingEnabled: boolean;
};
whiteboard?: {
collabDetails: {
roomId: string;
roomKey: string;
};
};
}
export interface IJitsiConference {
addCommandListener: Function;
addLobbyMessageListener: Function;
@@ -141,6 +155,7 @@ export interface IConferenceState {
localSubject?: string;
locked?: string;
membersOnly?: IJitsiConference;
metadata?: IConferenceMetadata;
obfuscatedRoom?: string;
obfuscatedRoomSource?: string;
p2p?: Object;
@@ -247,6 +262,12 @@ ReducerRegistry.register<IConferenceState>('features/base/conference',
startAudioMutedPolicy: action.startAudioMutedPolicy,
startVideoMutedPolicy: action.startVideoMutedPolicy
};
case UPDATE_CONFERENCE_METADATA:
return {
...state,
metadata: action.metadata
};
}
return state;

View File

@@ -531,6 +531,7 @@ export interface IConfig {
};
recordingSharingUrl?: string;
recordings?: {
showPrejoinWarning?: boolean;
suggestRecording?: boolean;
};
remoteVideoMenu?: {
@@ -573,6 +574,7 @@ export interface IConfig {
testing?: {
assumeBandwidth?: boolean;
disableE2EE?: boolean;
dumpTranscript?: boolean;
mobileXmppWsThreshold?: number;
noAutoPlayVideo?: boolean;
p2pTestMode?: boolean;

View File

@@ -201,6 +201,7 @@ export default [
'remoteVideoMenu',
'roomPasswordNumberOfDigits',
'readOnlyName',
'recordings',
'replaceParticipant',
'resolution',
'salesforceUrl',

View File

@@ -5,6 +5,7 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../../app/types';
import DeviceStatus from '../../../../prejoin/components/web/preview/DeviceStatus';
import { isRoomNameEnabled } from '../../../../prejoin/functions';
import Toolbox from '../../../../toolbox/components/web/Toolbox';
import { getConferenceName } from '../../../conference/functions';
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
@@ -13,6 +14,7 @@ import { withPixelLineHeight } from '../../../styles/functions.web';
import ConnectionStatus from './ConnectionStatus';
import Preview from './Preview';
import RecordingWarning from './RecordingWarning';
import UnsafeRoomWarning from './UnsafeRoomWarning';
interface IProps {
@@ -57,6 +59,11 @@ interface IProps {
*/
showDeviceStatus: boolean;
/**
* Indicates whether to display the recording warning.
*/
showRecordingWarning?: boolean;
/**
* If should show unsafe room warning when joining.
*/
@@ -167,6 +174,7 @@ const PreMeetingScreen = ({
children,
className,
showDeviceStatus,
showRecordingWarning,
showUnsafeRoomWarning,
skipPrejoinButton,
title,
@@ -198,6 +206,7 @@ const PreMeetingScreen = ({
{children}
{_buttons.length && <Toolbox toolbarButtons = { _buttons } />}
{skipPrejoinButton}
{showRecordingWarning && <RecordingWarning />}
{showUnsafeRoomWarning && <UnsafeRoomWarning />}
{showDeviceStatus && <DeviceStatus />}
</div>
@@ -219,7 +228,7 @@ const PreMeetingScreen = ({
* @returns {Object}
*/
function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
const { hiddenPremeetingButtons, hideConferenceSubject } = state['features/base/config'];
const { hiddenPremeetingButtons } = state['features/base/config'];
const toolbarButtons = getToolbarButtons(state);
const premeetingButtons = (ownProps.thirdParty
? THIRD_PARTY_PREJOIN_BUTTONS
@@ -237,7 +246,7 @@ function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
? premeetingButtons
: premeetingButtons.filter(b => isToolbarButtonEnabled(b, toolbarButtons)),
_premeetingBackground: premeetingBackground,
_roomName: (hideConferenceSubject ? undefined : getConferenceName(state)) ?? ''
_roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
};
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { IconRecord } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
const useStyles = makeStyles()(theme => {
return {
container: {
backgroundColor: theme.palette.warning01,
borderRadius: theme.shape.borderRadius,
color: theme.palette.text04,
display: 'flex',
justifyContent: 'center',
...withPixelLineHeight(theme.typography.bodyShortRegular),
marginBottom: theme.spacing(3),
marginTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
paddingTop: theme.spacing(2),
width: '100%'
},
warning: {
alignItems: 'center',
display: 'flex',
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3)
},
text: {
fontWeight: 600,
paddingLeft: theme.spacing(2)
}
};
});
const RecordingWarning = () => {
const { t } = useTranslation();
const { classes, theme } = useStyles();
const color = theme.palette.icon04;
return (
<div className = { classes.container }>
<div className = { classes.warning }>
<Icon
color = { color }
src = { IconRecord } />
<span className = { classes.text }>
{t('prejoin.recordingWarning')}
</span>
</div>
</div>
);
};
export default RecordingWarning;

View File

@@ -1,3 +1,5 @@
import base64js from 'base64-js';
import { timeoutPromise } from './timeoutPromise';
/**
@@ -43,3 +45,37 @@ export function doGetJSON(url: string, retry?: boolean, options?: Object) {
return fetchPromise;
}
/**
* Encodes strings to Base64URL.
*
* @param {any} data - The byte array to encode.
* @returns {string}
*/
export const encodeToBase64URL = (data: string): string => base64js
.fromByteArray(new window.TextEncoder().encode(data))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
/**
* Decodes strings from Base64URL.
*
* @param {string} data - The byte array to decode.
* @returns {string}
*/
export const decodeFromBase64URL = (data: string): string => {
let s = data;
// Convert from Base64URL to Base64.
if (s.length % 4 === 2) {
s += '==';
} else if (s.length % 4 === 3) {
s += '=';
}
s = s.replace(/-/g, '+').replace(/_/g, '/');
// Convert Base64 to a byte array.
return new window.TextDecoder().decode(base64js.toByteArray(s));
};

View File

@@ -2,7 +2,7 @@ import { AnyAction } from 'redux';
import { IReduxState, IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { CONFERENCE_JOINED, ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { IJitsiConference } from '../base/conference/reducer';
import { openDialog } from '../base/dialog/actions';
@@ -92,14 +92,6 @@ MiddlewareRegistry.register(store => next => action => {
_addChatMsgListener(action.conference, store);
break;
case OPEN_CHAT:
unreadCount = 0;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
break;
case CLOSE_CHAT: {
const isPollTabOpen = getState()['features/chat'].isPollsTabFocused;
@@ -115,6 +107,38 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case ENDPOINT_MESSAGE_RECEIVED: {
const state = store.getState();
if (!isReactionsEnabled(state)) {
return;
}
const { participant, data } = action;
if (data?.name === ENDPOINT_REACTION_NAME) {
store.dispatch(pushReactions(data.reactions));
_handleReceivedMessage(store, {
id: participant.getId(),
message: getReactionMessageFromBuffer(data.reactions),
privateMessage: false,
lobbyChat: false,
timestamp: data.timestamp
}, false, true);
}
break;
}
case OPEN_CHAT:
unreadCount = 0;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
break;
case SET_IS_POLL_TAB_FOCUSED: {
dispatch(resetNbUnreadPollsMessages());
break;
@@ -252,33 +276,6 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
}
);
// TODO: clean this up, there should be a central place that dispatches this. -saghul
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any) => {
const state = store.getState();
if (!isReactionsEnabled(state)) {
return;
}
if (args && args.length >= 2) {
const [ { _id }, eventData ] = args;
if (eventData.name === ENDPOINT_REACTION_NAME) {
store.dispatch(pushReactions(eventData.reactions));
_handleReceivedMessage(store, {
id: _id,
message: getReactionMessageFromBuffer(eventData.reactions),
privateMessage: false,
lobbyChat: false,
timestamp: eventData.timestamp
}, false, true);
}
}
});
conference.on(
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType: string, error: Error) => {
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);

View File

@@ -17,7 +17,6 @@ import { IReduxState, IStore } from '../../../app/types';
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
import { FULLSCREEN_ENABLED, PIP_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { getParticipantCount } from '../../../base/participants/functions';
import Container from '../../../base/react/components/native/Container';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import TintedView from '../../../base/react/components/native/TintedView';
@@ -101,11 +100,6 @@ interface IProps extends AbstractProps {
*/
_fullscreenEnabled: boolean;
/**
* The indicator which determines if the conference type is one to one.
*/
_isOneToOneConference: boolean;
/**
* The indicator which determines if the participants pane is open.
*/
@@ -370,7 +364,6 @@ class Conference extends AbstractConference<IProps, State> {
_aspectRatio,
_connecting,
_filmstripVisible,
_isOneToOneConference,
_largeVideoParticipantId,
_reducedUI,
_shouldDisplayTileView,
@@ -426,13 +419,11 @@ class Conference extends AbstractConference<IProps, State> {
<Captions onPress = { this._onClick } />
{
_shouldDisplayTileView || (
!_isOneToOneConference
&& <Container style = { styles.displayNameContainer }>
<DisplayNameLabel
participantId = { _largeVideoParticipantId } />
</Container>
)
_shouldDisplayTileView
|| <Container style = { styles.displayNameContainer }>
<DisplayNameLabel
participantId = { _largeVideoParticipantId } />
</Container>
}
{ !_shouldDisplayTileView && <LonelyMeetingExperience /> }
@@ -573,7 +564,6 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { backgroundColor } = state['features/dynamic-branding'];
const { startCarMode } = state['features/base/settings'];
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
const participantCount = getParticipantCount(state);
const brandingStyles = backgroundColor ? {
backgroundColor
} : undefined;
@@ -587,7 +577,6 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isOneToOneConference: Boolean(participantCount === 2),
_isParticipantsPaneOpen: isOpen,
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),

View File

@@ -4,11 +4,12 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getConferenceName, getConferenceTimestamp } from '../../../base/conference/functions';
import { CONFERENCE_TIMER_ENABLED, MEETING_NAME_ENABLED } from '../../../base/flags/constants';
import { CONFERENCE_TIMER_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import AudioDeviceToggleButton from '../../../mobile/audio-mode/components/AudioDeviceToggleButton';
import PictureInPictureButton from '../../../mobile/picture-in-picture/components/PictureInPictureButton';
import ParticipantsPaneButton from '../../../participants-pane/components/native/ParticipantsPaneButton';
import { isRoomNameEnabled } from '../../../prejoin/functions';
import ToggleCameraButton from '../../../toolbox/components/native/ToggleCameraButton';
import { isToolboxVisible } from '../../../toolbox/functions.native';
import ConferenceTimer from '../ConferenceTimer';
@@ -16,6 +17,7 @@ import ConferenceTimer from '../ConferenceTimer';
import Labels from './Labels';
import styles from './styles';
interface IProps {
/**
@@ -35,9 +37,9 @@ interface IProps {
_meetingName: string;
/**
* Whether displaying the current meeting name is enabled or not.
* Whether displaying the current room name is enabled or not.
*/
_meetingNameEnabled: boolean;
_roomNameEnabled: boolean;
/**
* True if the navigation bar should be visible.
@@ -75,7 +77,7 @@ const TitleBar = (props: IProps) => {
</View>
}
{
props._meetingNameEnabled
props._roomNameEnabled
&& <View style = { styles.roomNameView as ViewStyle }>
<Text
numberOfLines = { 1 }
@@ -107,15 +109,14 @@ const TitleBar = (props: IProps) => {
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { hideConferenceTimer, hideConferenceSubject } = state['features/base/config'];
const { hideConferenceTimer } = state['features/base/config'];
const startTimestamp = getConferenceTimestamp(state);
return {
_conferenceTimerEnabled:
Boolean(getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer && startTimestamp),
_meetingName: getConferenceName(state),
_meetingNameEnabled:
getFeatureFlag(state, MEETING_NAME_ENABLED, true) && !hideConferenceSubject,
_roomNameEnabled: isRoomNameEnabled(state),
_visible: isToolboxVisible(state)
};
}

View File

@@ -1,10 +1,7 @@
import i18next from 'i18next';
import { CONFERENCE_JOINED, KICKED_OUT } from '../base/conference/actionTypes';
import { IJitsiConference } from '../base/conference/reducer';
import { ENDPOINT_MESSAGE_RECEIVED, KICKED_OUT } from '../base/conference/actionTypes';
import { hangup } from '../base/connection/actions.web';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getParticipantDisplayName } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { openAllowToggleCameraDialog, setCameraFacingMode } from '../base/tracks/actions.web';
@@ -14,8 +11,15 @@ import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED: {
_addSetCameraFacingModeListener(action.conference);
case ENDPOINT_MESSAGE_RECEIVED: {
const { participant, data } = action;
if (data?.name === CAMERA_FACING_MODE_MESSAGE) {
APP.store.dispatch(openAllowToggleCameraDialog(
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(data.facingMode)),
/* initiatorId */ participant.getId()
));
}
break;
}
@@ -34,28 +38,3 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that
* will perform various chat related activities.
*
* @param {IJitsiConference} conference - The conference.
* @returns {void}
*/
function _addSetCameraFacingModeListener(conference: IJitsiConference) {
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any) => {
if (args && args.length >= 2) {
const [ sender, eventData ] = args;
if (eventData.name === CAMERA_FACING_MODE_MESSAGE) {
APP.store.dispatch(openAllowToggleCameraDialog(
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(eventData.facingMode)),
/* initiatorId */ sender._id
));
}
}
}
);
}

View File

@@ -56,6 +56,7 @@ class SharedDocument extends PureComponent<IProps> {
style = { styles.sharedDocContainer }>
<WebView
hideKeyboardAccessoryView = { true }
incognito = { true }
renderLoading = { this._renderLoading }
source = {{ uri: _documentUrl ?? '' }}
startInLoadingState = { true }

View File

@@ -3,12 +3,11 @@ import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import {
CONFERENCE_JOINED,
CONFERENCE_WILL_LEAVE
CONFERENCE_WILL_LEAVE,
ENDPOINT_MESSAGE_RECEIVED
} from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getLocalParticipant, getParticipantCount } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from '../base/tracks/actionTypes';
@@ -28,27 +27,22 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
FaceLandmarksDetector.init(store);
}
// allow using remote face centering data when local face centering is not enabled
action.conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(participant: IParticipant | undefined, eventData: any) => {
if (!participant || !eventData || !participant.getId) {
return;
}
return next(action);
} else if (action.type === ENDPOINT_MESSAGE_RECEIVED) {
// Allow using remote face centering data when local face centering is not enabled.
const { participant, data } = action;
if (eventData.type === FACE_BOX_EVENT_TYPE) {
dispatch({
type: UPDATE_FACE_COORDINATES,
faceBox: eventData.faceBox,
id: participant.getId()
});
}
if (data?.type === FACE_BOX_EVENT_TYPE) {
dispatch({
type: UPDATE_FACE_COORDINATES,
faceBox: data.faceBox,
id: participant.getId()
});
}
return next(action);
}
if (!isEnabled) {
return next(action);
}
@@ -57,7 +51,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
case CONFERENCE_WILL_LEAVE : {
FaceLandmarksDetector.stopDetection(store);
return next(action);
break;
}
case TRACK_ADDED: {
const { jitsiTrack: { isLocal, videoType }, muted } = action.track;
@@ -67,18 +61,18 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
FaceLandmarksDetector.startDetection(store, action.track);
}
return next(action);
break;
}
case TRACK_UPDATED: {
const { jitsiTrack: { isLocal, videoType } } = action.track;
if (videoType !== 'camera' || !isLocal()) {
return next(action);
break;
}
const { muted } = action.track;
if (muted !== undefined) {
if (typeof muted !== 'undefined') {
// addresses video mute state changes
if (muted) {
FaceLandmarksDetector.stopDetection(store);
@@ -87,7 +81,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
}
}
return next(action);
break;
}
case TRACK_REMOVED: {
const { jitsiTrack: { isLocal, videoType } } = action.track;
@@ -96,7 +90,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
FaceLandmarksDetector.stopDetection(store);
}
return next(action);
break;
}
case ADD_FACE_LANDMARKS: {
const state = getState();
@@ -110,7 +104,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
// Disabling for now as there is no value of having the data in speakerstats at the server
// sendFaceExpressionToServer(conference, faceLandmarks);
return next(action);
break;
}
case NEW_FACE_COORDINATES: {
const state = getState();
@@ -127,6 +121,8 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
faceBox,
id: localParticipant?.id
});
break;
}
}

View File

@@ -76,6 +76,7 @@ class DialInSummary extends PureComponent<IProps> {
<JitsiScreen
style = { styles.backDrop }>
<WebView
incognito = { true }
onError = { this._onError }
onShouldStartLoadWithRequest = { this._onNavigate }
renderLoading = { this._renderLoading }

View File

@@ -21,7 +21,8 @@ import {
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
import { LOBBY_CHAT_INITIALIZED, MODERATOR_IN_CHAT_WITH_LEFT } from './constants';
import { getKnockingParticipants, getLobbyConfig, getLobbyEnabled } from './functions';
import { getKnockingParticipants, getLobbyConfig, getLobbyEnabled, isEnablingLobbyAllowed } from './functions';
import logger from './logger';
import { IKnockingParticipant } from './types';
/**
@@ -243,7 +244,11 @@ export function toggleLobbyMode(enabled: boolean) {
const conference = getCurrentConference(getState);
if (enabled) {
conference?.enableLobby();
if (isEnablingLobbyAllowed(getState())) {
conference?.enableLobby();
} else {
logger.info('Ignoring enable lobby request because there are visitors in the call already.');
}
} else {
conference?.disableLobby();
}

View File

@@ -1,5 +1,6 @@
import { IReduxState } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { getVisitorsCount } from '../visitors/functions';
import { IKnockingParticipant } from './types';
@@ -92,3 +93,13 @@ export function showLobbyChatButton(
return false;
};
}
/**
* Returns true if enabling lobby is allowed and false otherwise.
*
* @param {IReduxState} state - State object.
* @returns {boolean}
*/
export function isEnablingLobbyAllowed(state: IReduxState) {
return getVisitorsCount(state) <= 0;
}

View File

@@ -6,7 +6,8 @@ import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED
CONFERENCE_JOINED,
ENDPOINT_MESSAGE_RECEIVED
} from '../base/conference/actionTypes';
import { conferenceWillJoin } from '../base/conference/actions';
import {
@@ -79,6 +80,13 @@ MiddlewareRegistry.register(store => next => action => {
return _conferenceFailed(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case ENDPOINT_MESSAGE_RECEIVED: {
const { participant, data } = action;
_maybeSendLobbyNotification(participant, data, store);
break;
}
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED: {
// We need the full update result to be in the store already
const result = next(action);
@@ -166,13 +174,6 @@ StateListenerRegistry.register(
dispatch(updateLobbyParticipantOnLeave(id));
});
});
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin: any, sender: any) =>
_maybeSendLobbyNotification(origin, sender, {
dispatch,
getState
})
);
}
}
);

View File

@@ -16,6 +16,7 @@ import {
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
ENDPOINT_MESSAGE_RECEIVED,
SET_ROOM
} from '../../base/conference/actionTypes';
import { JITSI_CONFERENCE_URL_KEY } from '../../base/conference/constants';
@@ -184,6 +185,22 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
break;
}
case ENDPOINT_MESSAGE_RECEIVED: {
const { participant, data } = action;
if (data?.name === ENDPOINT_TEXT_MESSAGE_NAME) {
sendEvent(
store,
ENDPOINT_TEXT_MESSAGE_RECEIVED,
/* data */ {
message: data.text,
senderId: participant.getId()
});
}
break;
}
case ENTER_PICTURE_IN_PICTURE:
sendEvent(store, type, /* data */ {});
break;
@@ -419,24 +436,6 @@ function _unregisterForNativeEvents() {
function _registerForEndpointTextMessages(store: IStore) {
const conference = getCurrentConference(store.getState());
conference?.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any[]) => {
if (args && args.length >= 2) {
const [ sender, eventData ] = args;
if (eventData.name === ENDPOINT_TEXT_MESSAGE_NAME) {
sendEvent(
store,
ENDPOINT_TEXT_MESSAGE_RECEIVED,
/* data */ {
message: eventData.text,
senderId: sender._id
});
}
}
});
conference?.on(
JitsiConferenceEvents.MESSAGE_RECEIVED,
(id: string, message: string, timestamp: number) => {

View File

@@ -13,6 +13,7 @@ import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
// @ts-ignore
import WelcomePage from '../../../welcome/components/WelcomePage';
import { isWelcomePageEnabled } from '../../../welcome/functions';
import Whiteboard from '../../../whiteboard/components/native/Whiteboard';
import { _ROOT_NAVIGATION_READY } from '../actionTypes';
import { rootNavigationRef } from '../rootNavigationContainerRef';
import { screen } from '../routes';
@@ -23,7 +24,8 @@ import {
navigationContainerTheme,
preJoinScreenOptions,
unsafeMeetingScreenOptions,
welcomeScreenOptions
welcomeScreenOptions,
whiteboardScreenOptions
} from '../screenOptions';
import ConnectingPage from './ConnectingPage';
@@ -94,6 +96,10 @@ const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWel
component = { ConnectingPage }
name = { screen.connecting }
options = { connectingScreenOptions } />
<RootStack.Screen // @ts-ignore
component = { Whiteboard }
name = { screen.conference.whiteboard }
options = { whiteboardScreenOptions } />
<RootStack.Screen
component = { Prejoin }
name = { screen.preJoin }

View File

@@ -22,7 +22,8 @@ export const screen = {
security: 'Security Options',
sharedDocument: 'Shared document',
speakerStats: 'Speaker Stats',
subtitles: 'Subtitles'
subtitles: 'Subtitles',
whiteboard: 'Whiteboard'
},
connecting: 'Connecting',
dialInSummary: 'Dial-In Info',

View File

@@ -188,6 +188,19 @@ export const connectingScreenOptions = {
headerShown: false
};
/**
* Screen options for the whiteboard screen.
*/
export const whiteboardScreenOptions = {
gestureEnabled: true,
headerStyle: {
backgroundColor: BaseTheme.palette.ui01
},
headerTitleStyle: {
color: BaseTheme.palette.text01
}
};
/**
* Screen options for pre-join screen.
*/

View File

@@ -1,6 +1,8 @@
import { DOMParser } from '@xmldom/xmldom';
import { atob, btoa } from 'abab';
import { Platform } from 'react-native';
import BackgroundTimer from 'react-native-background-timer';
import { TextDecoder, TextEncoder } from 'text-encoding';
import 'promise.allsettled/auto'; // Promise.allSettled.
import 'react-native-url-polyfill/auto'; // Complete URL polyfill.
@@ -313,4 +315,23 @@ function _visitNode(node, callback) {
global.sessionStorage = new Storage();
}
global.TextDecoder = TextDecoder;
global.TextEncoder = TextEncoder;
// atob
//
// Required by:
// - Strophe
if (typeof global.atob === 'undefined') {
global.atob = atob;
}
// btoa
//
// Required by:
// - Strophe
if (typeof global.btoa === 'undefined') {
global.btoa = btoa;
}
})(global || window || this); // eslint-disable-line no-invalid-this

View File

@@ -103,6 +103,13 @@ export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION';
*/
export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
/**
* The identifier of the lobby notification.
*
* @type {string}
*/
export const VISITORS_PROMOTION_NOTIFICATION_ID = 'VISITORS_PROMOTION_NOTIFICATION';
/**
* Amount of participants beyond which no join notification will be emitted.
*/

View File

@@ -1,7 +1,6 @@
import { IStore } from '../app/types';
import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import { ENDPOINT_MESSAGE_RECEIVED, NON_PARTICIPANT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound } from '../base/sounds/actions';
@@ -59,36 +58,32 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
switch (action.type) {
case CONFERENCE_JOIN_IN_PROGRESS: {
const { conference } = action;
case ENDPOINT_MESSAGE_RECEIVED: {
const { participant, data } = action;
const isNewPoll = data.type === COMMAND_NEW_POLL;
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(user: any, data: any) => {
const isNewPoll = data.type === COMMAND_NEW_POLL;
_handleReceivePollsMessage({
...data,
senderId: isNewPoll ? user._id : undefined,
voterId: isNewPoll ? undefined : user._id
}, dispatch, getState);
});
conference.on(JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED,
(id: any, data: any) => {
const isNewPoll = data.type === COMMAND_NEW_POLL;
_handleReceivePollsMessage({
...data,
senderId: isNewPoll ? id : undefined,
voterId: isNewPoll ? undefined : id
}, dispatch, getState);
});
_handleReceivePollsMessage({
...data,
senderId: isNewPoll ? participant.getId() : undefined,
voterId: isNewPoll ? undefined : participant.getId()
}, dispatch, getState);
break;
}
// Middleware triggered when a poll is received
case RECEIVE_POLL: {
case NON_PARTICIPANT_MESSAGE_RECEIVED: {
const { id, json: data } = action;
const isNewPoll = data.type === COMMAND_NEW_POLL;
_handleReceivePollsMessage({
...data,
senderId: isNewPoll ? id : undefined,
voterId: isNewPoll ? undefined : id
}, dispatch, getState);
break;
}
case RECEIVE_POLL: {
const state = getState();
if (arePollsDisabled(state)) {

View File

@@ -38,10 +38,11 @@ import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainer
import { screen } from '../../../mobile/navigation/routes';
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
import VideoMuteButton from '../../../toolbox/components/native/VideoMuteButton';
import { isDisplayNameRequired } from '../../functions';
import { isDisplayNameRequired, isRoomNameEnabled } from '../../functions';
import { IPrejoinProps } from '../../types';
import { hasDisplayName } from '../../utils';
import RecordingWarning from './RecordingWarning';
import { preJoinStyles as styles } from './styles';
@@ -58,6 +59,9 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
= useSelector((state: IReduxState) => !getFeatureFlag(state, PREJOIN_PAGE_HIDE_DISPLAY_NAME, false));
const isDisplayNameReadonly = useSelector(isNameReadOnly);
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
const roomNameEnabled = useSelector((state: IReduxState) => isRoomNameEnabled(state));
const showRecordingWarning
= useSelector((state: IReduxState) => state['features/base/config'].recordings?.showPrejoinWarning);
const participantName = localParticipant?.name;
const [ displayName, setDisplayName ]
= useState(participantName || '');
@@ -168,12 +172,17 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
{
isFocused
&& <View style = { largeVideoContainerStyles as StyleProp<ViewStyle> }>
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
<Text
numberOfLines = { 1 }
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
{ roomName }
</Text>
<View style = { styles.conferenceInfo as StyleProp<ViewStyle> }>
{roomNameEnabled && (
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
<Text
numberOfLines = { 1 }
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
{ roomName }
</Text>
</View>
)}
{showRecordingWarning && <RecordingWarning />}
</View>
<LargeVideo />
</View>
@@ -194,10 +203,15 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
placeholder = { t('dialog.enterDisplayName') }
value = { displayName } />
}
{showDisplayNameError && (
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
<Text style = { styles.error as StyleProp<TextStyle> }>{t('prejoin.errorMissingName')}</Text>
</View>)}
{
showDisplayNameError && (
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
<Text style = { styles.error as StyleProp<TextStyle> }>
{ t('prejoin.errorMissingName') }
</Text>
</View>
)
}
<Button
accessibilityLabel = 'prejoin.joinMeeting'
disabled = { showDisplayNameError }

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
StyleProp,
Text,
TextStyle,
View,
ViewStyle
} from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { IconRecord } from '../../../base/icons/svg';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import { preJoinStyles as styles } from './styles';
const RecordingWarning = (): JSX.Element => {
const { t } = useTranslation();
const color = BaseTheme.palette.icon04;
return (
<View style = { styles.recordingWarningContainer as StyleProp<ViewStyle> }>
<View style = { styles.recordingWarning as StyleProp<ViewStyle> }>
<Icon
color = { color }
size = { 20 }
src = { IconRecord } />
<Text
numberOfLines = { 1 }
style = { styles.recordingWarningText as StyleProp<TextStyle> }>
{ t('prejoin.recordingWarning') }
</Text>
</View>
</View>
);
};
export default RecordingWarning;

View File

@@ -106,18 +106,47 @@ export const preJoinStyles = {
textAlign: 'center'
},
displayRoomNameBackdrop: {
conferenceInfo: {
alignSelf: 'center',
backgroundColor: BaseTheme.palette.uiBackground,
borderRadius: BaseTheme.shape.borderRadius,
marginTop: BaseTheme.spacing[3],
opacity: 0.7,
paddingHorizontal: BaseTheme.spacing[3],
paddingVertical: BaseTheme.spacing[1],
position: 'absolute',
maxWidth: 243,
maxWidth: 273,
zIndex: 1
},
displayRoomNameBackdrop: {
backgroundColor: BaseTheme.palette.uiBackground,
borderRadius: BaseTheme.shape.borderRadius,
opacity: 0.7,
paddingHorizontal: BaseTheme.spacing[3],
paddingVertical: BaseTheme.spacing[1]
},
recordingWarningContainer: {
backgroundColor: BaseTheme.palette.warning01,
borderRadius: BaseTheme.shape.borderRadius,
color: BaseTheme.palette.text04,
display: 'flex',
justifyContent: 'center',
lineHeight: 22,
marginBottom: BaseTheme.spacing[2],
marginTop: BaseTheme.spacing[1],
paddingBottom: BaseTheme.spacing[1],
paddingTop: BaseTheme.spacing[1],
width: 'auto'
},
recordingWarning: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
paddingLeft: BaseTheme.spacing[2],
paddingRight: BaseTheme.spacing[2]
},
recordingWarningText: {
fontWeight: '600',
paddingLeft: BaseTheme.spacing[1]
},
unsafeRoomWarningContainer: {
height: '100%',
width: '100%',

View File

@@ -111,6 +111,11 @@ interface IProps {
*/
showErrorOnJoin: boolean;
/**
* If the recording warning is visible or not.
*/
showRecordingWarning: boolean;
/**
* If should show unsafe room warning when joining.
*/
@@ -219,6 +224,7 @@ const Prejoin = ({
showCameraPreview,
showDialog,
showErrorOnJoin,
showRecordingWarning,
showUnsafeRoomWarning,
unsafeRoomConsent,
updateSettings: dispatchUpdateSettings,
@@ -390,6 +396,7 @@ const Prejoin = ({
return (
<PreMeetingScreen
showDeviceStatus = { deviceStatusVisible }
showRecordingWarning = { showRecordingWarning }
showUnsafeRoomWarning = { showUnsafeRoomWarning }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
@@ -483,6 +490,7 @@ function mapStateToProps(state: IReduxState) {
const { joiningInProgress } = state['features/prejoin'];
const { room } = state['features/base/conference'];
const { unsafeRoomConsent } = state['features/base/premeeting'];
const { showPrejoinWarning: showRecordingWarning } = state['features/base/config'].recordings ?? {};
return {
deviceStatusVisible: isDeviceStatusVisible(state),
@@ -496,6 +504,7 @@ function mapStateToProps(state: IReduxState) {
showCameraPreview: !isVideoMutedByUser(state),
showDialog: isJoinByPhoneDialogVisible(state),
showErrorOnJoin,
showRecordingWarning: Boolean(showRecordingWarning),
showUnsafeRoomWarning: isInsecureRoomName(room) && isUnsafeRoomWarningEnabled(state),
unsafeRoomConsent,
videoTrack: getLocalJitsiVideoTrack(state)

View File

@@ -1,7 +1,10 @@
import { IReduxState } from '../app/types';
import { getRoomName } from '../base/conference/functions';
import { getDialOutStatusUrl, getDialOutUrl } from '../base/config/functions.any';
import { UNSAFE_ROOM_WARNING } from '../base/flags/constants';
import {
MEETING_NAME_ENABLED,
UNSAFE_ROOM_WARNING
} from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { isAudioMuted, isVideoMutedByUser } from '../base/media/functions';
import { getLobbyConfig } from '../lobby/functions';
@@ -184,3 +187,14 @@ export function isUnsafeRoomWarningEnabled(state: IReduxState): boolean {
return getFeatureFlag(state, UNSAFE_ROOM_WARNING, enableInsecureRoomNameWarning);
}
/**
* Returns true if the room name is enabled.
*
* @param {IReduxState} state - The state of the app.
* @returns {boolean}
*/
export function isRoomNameEnabled(state: IReduxState): boolean {
return getFeatureFlag(state, MEETING_NAME_ENABLED, true)
|| !state['features/base/config'].hideConferenceSubject;
}

View File

@@ -83,3 +83,12 @@ export const START_LOCAL_RECORDING = 'START_LOCAL_RECORDING';
* }
*/
export const STOP_LOCAL_RECORDING = 'STOP_LOCAL_RECORDING';
/**
* Indicates that the start recording notification has been shown.
*
* {
* type: SET_START_RECORDING_NOTIFICATION_SHOWN
* }
*/
export const SET_START_RECORDING_NOTIFICATION_SHOWN = 'SET_START_RECORDING_NOTIFICATION_SHOWN';

View File

@@ -26,6 +26,7 @@ import {
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_SELECTED_RECORDING_SERVICE,
SET_START_RECORDING_NOTIFICATION_SHOWN,
SET_STREAM_KEY,
START_LOCAL_RECORDING,
STOP_LOCAL_RECORDING
@@ -57,6 +58,20 @@ export function clearRecordingSessions() {
};
}
/**
* Marks the start recording notification as shown.
*
* @returns {{
* type: SET_START_RECORDING_NOTIFICATION_SHOWN
* }}
*/
export function setStartRecordingNotificationShown() {
return {
type: SET_START_RECORDING_NOTIFICATION_SHOWN
};
}
/**
* Sets the meeting highlight button disable state.
*
@@ -394,14 +409,17 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
const { suggestRecording } = recordings || {};
const recordButtonProps = getRecordButtonProps(state);
const isAlreadyRecording = isRecordingRunning(state);
const wasNotificationShown = state['features/recording'].wasStartRecordingSuggested;
if (!suggestRecording
|| isAlreadyRecording
|| !recordButtonProps.visible
|| recordButtonProps.disabled) {
|| recordButtonProps.disabled
|| wasNotificationShown) {
return;
}
dispatch(setStartRecordingNotificationShown());
dispatch(showNotification({
titleKey: 'notify.suggestRecordingTitle',
descriptionKey: 'notify.suggestRecordingDescription',
@@ -432,7 +450,7 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
});
if (autoTranscribeOnRecord) {
dispatch(setRequestingSubtitles(true, false));
dispatch(setRequestingSubtitles(true, false, null));
}
} else {
openRecordingDialog();

View File

@@ -2,8 +2,8 @@ import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState } from '../../app/types';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { getSessionStatusToShow } from '../functions';
import { isTranscribing } from '../../transcribing/functions';
import { getSessionStatusToShow, isRecordingRunning } from '../functions';
interface IProps extends WithTranslation {
@@ -13,6 +13,11 @@ interface IProps extends WithTranslation {
*/
_iAmRecorder: boolean;
/**
* Whether the recording is currently running.
*/
_isRecordingRunning: boolean;
/**
* Whether this meeting is being transcribed.
*/
@@ -45,79 +50,17 @@ interface IState {
staleLabel: boolean;
}
/**
* The timeout after a label is considered stale. See {@code _updateStaleStatus}
* for more details.
*/
const STALE_TIMEOUT = 10 * 1000;
/**
* Abstract class for the {@code RecordingLabel} component.
*/
export default class AbstractRecordingLabel extends Component<IProps, IState> {
_mounted: boolean;
/**
* Implements {@code Component#getDerivedStateFromProps}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: IProps, prevState: IState) {
return {
staleLabel: props._status !== JitsiRecordingConstants.status.OFF
&& prevState.staleLabel ? false : prevState.staleLabel
};
}
/**
* Initializes a new {@code AbstractRecordingLabel} component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
staleLabel: true
};
this._updateStaleStatus(undefined, props);
}
/**
* Implements React {@code Component}'s componentDidMount.
*
* @inheritdoc
*/
componentDidMount() {
this._mounted = true;
}
/**
* Implements React {@code Component}'s componentWillUnmount.
*
* @inheritdoc
*/
componentWillUnmount() {
this._mounted = false;
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: IProps) {
this._updateStaleStatus(prevProps, this.props);
}
/**
* Implements React {@code Component}'s render.
*
* @inheritdoc
*/
render() {
return (this.props._status || this.props._isTranscribing) && !this.state.staleLabel && !this.props._iAmRecorder
return this.props._isRecordingRunning && !this.props._iAmRecorder
? this._renderLabel() : null;
}
@@ -130,33 +73,6 @@ export default class AbstractRecordingLabel extends Component<IProps, IState> {
_renderLabel(): React.ReactNode | null {
return null;
}
/**
* Updates the stale status of the label on a prop change. A label is stale
* if it's in a {@code _status} that doesn't need to be rendered anymore.
*
* @param {IProps} oldProps - The previous props of the component.
* @param {IProps} newProps - The new props of the component.
* @returns {void}
*/
_updateStaleStatus(oldProps: IProps | undefined, newProps: IProps) {
if (newProps._status === JitsiRecordingConstants.status.OFF) {
if (oldProps?._status !== JitsiRecordingConstants.status.OFF) {
setTimeout(() => {
if (!this._mounted) {
return;
}
// Only if it's still OFF.
if (this.props._status === JitsiRecordingConstants.status.OFF) {
this.setState({
staleLabel: true
});
}
}, STALE_TIMEOUT);
}
}
}
}
/**
@@ -174,8 +90,9 @@ export function _mapStateToProps(state: IReduxState, ownProps: any) {
const { mode } = ownProps;
return {
_isRecordingRunning: isRecordingRunning(state),
_iAmRecorder: Boolean(state['features/base/config'].iAmRecorder),
_isTranscribing: state['features/transcribing'].isTranscribing,
_isTranscribing: isTranscribing(state),
_status: getSessionStatusToShow(state, mode)
};
}

View File

@@ -7,7 +7,7 @@ import { isLocalParticipantModerator } from '../../../base/participants/function
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
import { getActiveSession } from '../../functions';
import { getActiveSession, isCloudRecordingRunning } from '../../functions';
import { getLiveStreaming } from './functions';
@@ -140,7 +140,7 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
}
// disable the button if the recording is running.
if (visible && getActiveSession(state, JitsiRecordingConstants.mode.FILE)) {
if (visible && isCloudRecordingRunning(state)) {
_disabled = true;
_tooltip = 'dialog.liveStreamingDisabledBecauseOfActiveRecordingTooltip';
}

View File

@@ -4,14 +4,17 @@ import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { MEET_FEATURES } from '../../../base/jwt/constants';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
import { hideNotification, showNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../../notifications/constants';
import { iAmVisitor } from '../../../visitors/functions';
import { highlightMeetingMoment } from '../../actions.any';
import { PROMPT_RECORDING_NOTIFICATION_ID } from '../../constants';
import { getActiveSession, getRecordButtonProps, isHighlightMeetingMomentDisabled } from '../../functions';
import {
getRecordButtonProps,
isCloudRecordingRunning,
isHighlightMeetingMomentDisabled
} from '../../functions';
import { StartRecordingDialog } from './index';
@@ -103,7 +106,7 @@ export default class AbstractHighlightButton<P extends IProps, S={}> extends Com
* }}
*/
export function _abstractMapStateToProps(state: IReduxState) {
const isRecordingRunning = getActiveSession(state, JitsiRecordingConstants.mode.FILE);
const isRecordingRunning = isCloudRecordingRunning(state);
const isButtonDisabled = isHighlightMeetingMomentDisabled(state);
const { webhookProxyUrl } = state['features/base/config'];
const _iAmVisitor = iAmVisitor(state);

View File

@@ -6,7 +6,7 @@ import { MEET_FEATURES } from '../../../base/jwt/constants';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
import { getRecordButtonProps, isRecordingRunning } from '../../functions';
import { canStopRecording, getRecordButtonProps } from '../../functions';
/**
* The type of the React {@code Component} props of
@@ -131,7 +131,7 @@ export function _mapStateToProps(state: IReduxState) {
return {
_disabled,
_isRecordingRunning: isRecordingRunning(state),
_isRecordingRunning: canStopRecording(state),
_tooltip,
visible
};

View File

@@ -12,7 +12,7 @@ import { showErrorNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
import { setSelectedRecordingService, startLocalVideoRecording } from '../../actions';
import { RECORDING_TYPES } from '../../constants';
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../constants';
import { isRecordingSharingEnabled, shouldAutoTranscribeOnRecord, supportsLocalRecording } from '../../functions';
export interface IProps extends WithTranslation {
@@ -178,7 +178,7 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
userName: undefined,
sharingEnabled: true,
shouldRecordAudioAndVideo: true,
shouldRecordTranscription: true,
shouldRecordTranscription: this.props._autoTranscribeOnRecord,
spaceLeft: undefined,
selectedRecordingService,
localRecordingOnlySelf: false
@@ -335,7 +335,6 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
_onSubmit() {
const {
_appKey,
_autoTranscribeOnRecord,
_conference,
_isDropboxEnabled,
_rToken,
@@ -398,10 +397,14 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
});
}
if (_autoTranscribeOnRecord || this.state.shouldRecordTranscription) {
dispatch(setRequestingSubtitles(true, false));
if (this.state.shouldRecordTranscription) {
dispatch(setRequestingSubtitles(true, false, null));
}
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: this.state.shouldRecordTranscription
});
return true;
}

View File

@@ -9,7 +9,7 @@ import { _abstractMapStateToProps } from '../../../base/dialog/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { authorizeDropbox, updateDropboxToken } from '../../../dropbox/actions';
import { isVpaasMeeting } from '../../../jaas/functions';
import { canStartTranscribing } from '../../../subtitles/functions';
import { canAddTranscriber } from '../../../transcribing/functions';
import { RECORDING_TYPES } from '../../constants';
import { supportsLocalRecording } from '../../functions';
@@ -418,7 +418,7 @@ export function mapStateToProps(state: IReduxState) {
return {
..._abstractMapStateToProps(state),
isVpaas: isVpaasMeeting(state),
_canStartTranscribing: canStartTranscribing(state),
_canStartTranscribing: canAddTranscriber(state),
_hideStorageWarning: Boolean(recordingService?.hideStorageWarning),
_isModerator: isLocalParticipantModerator(state),
_localRecordingAvailable,

View File

@@ -4,6 +4,7 @@ import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import ExpandedLabel, { IProps as AbstractProps } from '../../../base/label/components/native/ExpandedLabel';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { isTranscribing } from '../../../transcribing/functions';
import { getSessionStatusToShow } from '../../functions';
interface IProps extends AbstractProps {
@@ -90,7 +91,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const { mode } = ownProps;
return {
_isTranscribing: state['features/transcribing'].isTranscribing,
_isTranscribing: isTranscribing(state),
_status: getSessionStatusToShow(state, mode)
};
}

View File

@@ -60,3 +60,5 @@ export const RECORDING_STATUS_PRIORITIES = [
];
export const START_RECORDING_NOTIFICATION_ID = 'START_RECORDING_NOTIFICATION_ID';
export const RECORDING_METADATA_ID = 'recording';

View File

@@ -14,6 +14,7 @@ import { registerSound, unregisterSound } from '../base/sounds/actions';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
import { isTranscribing } from '../transcribing/functions';
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
import {
@@ -158,6 +159,16 @@ export function supportsLocalRecording() {
&& navigator.product !== 'ReactNative';
}
/**
* Returns true if there is a cloud recording running.
*
* @param {IReduxState} state - The redux state to search in.
* @returns {boolean}
*/
export function isCloudRecordingRunning(state: IReduxState) {
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
}
/**
* Returns true if there is a recording session running.
*
@@ -165,15 +176,35 @@ export function supportsLocalRecording() {
* @returns {boolean}
*/
export function isRecordingRunning(state: IReduxState) {
const { isTranscribing } = state['features/transcribing'];
return (
Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE))
isCloudRecordingRunning(state)
|| LocalRecordingManager.isRecordingLocally()
|| isTranscribing
|| isTranscribing(state)
);
}
/**
* Returns true if the participant can stop recording.
*
* @param {Object} state - The redux state to search in.
* @returns {boolean}
*/
export function canStopRecording(state: IReduxState) {
if (!isRecordingRunning(state)) {
return false;
}
if (LocalRecordingManager.isRecordingLocally()) {
return true;
}
if (isCloudRecordingRunning(state) || isTranscribing(state)) {
return isLocalParticipantModerator(state) && isJwtFeatureEnabled(state, 'recording', true);
}
return false;
}
/**
* Returns whether the transcription should start automatically when recording starts.
*

View File

@@ -9,8 +9,10 @@ import JitsiMeetJS, {
JitsiRecordingConstants
} from '../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../base/media/constants';
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
import { updateLocalRecordingStatus } from '../base/participants/actions';
import { getParticipantDisplayName } from '../base/participants/functions';
import { PARTICIPANT_ROLE } from '../base/participants/constants';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import {
@@ -20,6 +22,7 @@ import {
import { TRACK_ADDED } from '../base/tracks/actionTypes';
import { hideNotification, showErrorNotification, showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { isTranscribing } from '../transcribing/functions';
import { RECORDING_SESSION_UPDATED, START_LOCAL_RECORDING, STOP_LOCAL_RECORDING } from './actionTypes';
import {
@@ -29,6 +32,7 @@ import {
showRecordingError,
showRecordingLimitNotification,
showRecordingWarning,
showStartRecordingNotification,
showStartedRecordingNotification,
showStoppedRecordingNotification,
updateRecordingSessionData
@@ -175,6 +179,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
}
case RECORDING_SESSION_UPDATED: {
const state = getState();
// When in recorder mode no notifications are shown
// or extra sounds are also not desired
// but we want to indicate those in case of sip gateway
@@ -182,22 +188,21 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
iAmRecorder,
iAmSipGateway,
recordingLimit
} = getState()['features/base/config'];
} = state['features/base/config'];
if (iAmRecorder && !iAmSipGateway) {
break;
}
const updatedSessionData
= getSessionById(getState(), action.sessionData.id);
= getSessionById(state, action.sessionData.id);
const { initiator, mode = '', terminator } = updatedSessionData ?? {};
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
if (updatedSessionData?.status === PENDING
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
if (updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING) {
dispatch(showPendingRecordingNotification(mode));
dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
} else if (updatedSessionData?.status !== PENDING) {
} else {
dispatch(hidePendingRecordingNotification(mode));
if (updatedSessionData?.status === ON) {
@@ -215,12 +220,13 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
}
}
if (!oldSessionData || oldSessionData.status !== ON) {
if (oldSessionData?.status !== ON) {
sendAnalytics(createRecordingEvent('start', mode));
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
if (mode === JitsiRecordingConstants.mode.FILE && !isTranscribing(state)) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
@@ -234,12 +240,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
APP.API.notifyRecordingStatusChanged(true, mode);
}
}
} else if (updatedSessionData?.status === OFF
&& (!oldSessionData || oldSessionData.status !== OFF)) {
} else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
if (terminator) {
dispatch(
showStoppedRecordingNotification(
mode, getParticipantDisplayName(getState, getResourceId(terminator))));
mode, getParticipantDisplayName(state, getResourceId(terminator))));
}
let duration = 0, soundOff, soundOn;
@@ -250,7 +255,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (mode === JitsiRecordingConstants.mode.FILE) {
if (mode === JitsiRecordingConstants.mode.FILE && !isTranscribing(state)) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
@@ -281,6 +286,21 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
}
break;
}
case PARTICIPANT_UPDATED: {
const { id, role } = action.participant;
const state = getState();
const localParticipant = getLocalParticipant(state);
if (localParticipant?.id !== id) {
return next(action);
}
if (role === PARTICIPANT_ROLE.MODERATOR) {
dispatch(showStartRecordingNotification());
}
return next(action);
}
}
return result;

View File

@@ -6,6 +6,7 @@ import {
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_SELECTED_RECORDING_SERVICE,
SET_START_RECORDING_NOTIFICATION_SHOWN,
SET_STREAM_KEY
} from './actionTypes';
@@ -35,6 +36,7 @@ export interface IRecordingState {
selectedRecordingService: string;
sessionDatas: Array<ISessionData>;
streamKey?: string;
wasStartRecordingSuggested?: boolean;
}
/**
@@ -94,6 +96,12 @@ ReducerRegistry.register<IRecordingState>(STORE_NAME,
disableHighlightMeetingMoment: action.disabled
};
case SET_START_RECORDING_NOTIFICATION_SHOWN:
return {
...state,
wasStartRecordingSuggested: true
};
default:
return state;
}

View File

@@ -1,8 +1,7 @@
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import { getActiveSession } from '../recording/functions';
import { isCloudRecordingRunning } from '../recording/functions';
import { isScreenVideoShared } from '../screen-share/functions';
import ScreenshotCaptureSummary from './ScreenshotCaptureSummary';
@@ -48,7 +47,7 @@ export function isScreenshotCaptureEnabled(state: IReduxState, checkSharing?: bo
}
// Feature enabled only when recording is also on.
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
return isCloudRecordingRunning(state);
}
return true;

View File

@@ -22,6 +22,7 @@ import { BUTTON_TYPES } from '../../../../base/ui/constants.native';
import { copyText } from '../../../../base/util/copyText.native';
import { isInBreakoutRoom } from '../../../../breakout-rooms/functions';
import { toggleLobbyMode } from '../../../../lobby/actions.any';
import { isEnablingLobbyAllowed } from '../../../../lobby/functions';
import {
endRoomLockRequest,
unlockRoom
@@ -50,6 +51,11 @@ interface IProps {
*/
_conference?: IJitsiConference;
/**
* Whether enabling lobby is allowed or not.
*/
_isEnablingLobbyAllowed: boolean;
/**
* Whether the local user is the moderator.
*/
@@ -174,11 +180,12 @@ class SecurityDialog extends PureComponent<IProps, IState> {
*/
_renderLobbyMode() {
const {
_isEnablingLobbyAllowed,
_lobbyModeSwitchVisible,
t
} = this.props;
if (!_lobbyModeSwitchVisible) {
if (!_lobbyModeSwitchVisible || !_isEnablingLobbyAllowed) {
return null;
}
@@ -509,6 +516,7 @@ function _mapStateToProps(state: IReduxState) {
return {
_conference: conference,
_isEnablingLobbyAllowed: isEnablingLobbyAllowed(state),
_isModerator: isLocalParticipantModerator(state),
_lobbyEnabled: lobbyEnabled,
_lobbyModeSwitchVisible:

View File

@@ -9,6 +9,7 @@ import { isLocalParticipantModerator } from '../../../../base/participants/funct
import Dialog from '../../../../base/ui/components/web/Dialog';
import E2EESection from '../../../../e2ee/components/E2EESection';
import LobbySection from '../../../../lobby/components/web/LobbySection';
import { isEnablingLobbyAllowed } from '../../../../lobby/functions';
import PasswordSection from './PasswordSection';
@@ -40,6 +41,11 @@ interface IProps {
*/
_disableLobbyPassword?: boolean;
/**
* Whether to hide the lobby section.
*/
_isEnablingLobbyAllowed: boolean;
/**
* The value for how the conference is locked (or undefined if not locked)
* as defined by room-lock constants.
@@ -77,6 +83,7 @@ function SecurityDialog({
_canEditPassword,
_conference,
_disableLobbyPassword,
_isEnablingLobbyAllowed,
_locked,
_password,
_passwordNumberOfDigits,
@@ -97,25 +104,29 @@ function SecurityDialog({
ok = {{ hidden: true }}
titleKey = 'security.title'>
<div className = 'security-dialog'>
<LobbySection />
{!_disableLobbyPassword && (
<>
<div className = 'separator-line' />
<PasswordSection
buttonsWithNotifyClick = { _buttonsWithNotifyClick }
canEditPassword = { _canEditPassword }
conference = { _conference }
locked = { _locked }
password = { _password }
passwordEditEnabled = { passwordEditEnabled }
passwordNumberOfDigits = { _passwordNumberOfDigits }
setPassword = { setPassword }
setPasswordEditEnabled = { setPasswordEditEnabled } />
</>
)}
{
_isEnablingLobbyAllowed && <LobbySection />
}
{
!_disableLobbyPassword && (
<>
{ _isEnablingLobbyAllowed && <div className = 'separator-line' /> }
<PasswordSection
buttonsWithNotifyClick = { _buttonsWithNotifyClick }
canEditPassword = { _canEditPassword }
conference = { _conference }
locked = { _locked }
password = { _password }
passwordEditEnabled = { passwordEditEnabled }
passwordNumberOfDigits = { _passwordNumberOfDigits }
setPassword = { setPassword }
setPasswordEditEnabled = { setPasswordEditEnabled } />
</>
)
}
{
_showE2ee ? <>
<div className = 'separator-line' />
{ (_isEnablingLobbyAllowed || !_disableLobbyPassword) && <div className = 'separator-line' /> }
<E2EESection />
</> : null
}
@@ -145,6 +156,7 @@ function mapStateToProps(state: IReduxState) {
buttonsWithNotifyClick
} = state['features/base/config'];
const { disableLobbyPassword } = getSecurityUiConfig(state);
const _isEnablingLobbyAllowed = isEnablingLobbyAllowed(state);
const showE2ee = Boolean(e2eeSupported) && isLocalParticipantModerator(state);
@@ -154,6 +166,7 @@ function mapStateToProps(state: IReduxState) {
_conference: conference,
_dialIn: state['features/invite'],
_disableLobbyPassword: disableLobbyPassword,
_isEnablingLobbyAllowed,
_locked: locked,
_password: password,
_passwordNumberOfDigits: roomPasswordNumberOfDigits,

View File

@@ -1,16 +1,3 @@
/**
* The type of (redux) action which indicates that an endpoint message
* sent by another participant to the data channel is received.
*
* {
* type: ENDPOINT_MESSAGE_RECEIVED,
* participant: Object,
* json: Object
* }
*/
export const ENDPOINT_MESSAGE_RECEIVED = 'ENDPOINT_MESSAGE_RECEIVED';
/**
* The type of (redux) action which indicates that an existing transcript
* has to be removed from the state.

View File

@@ -1,32 +1,12 @@
import { DEFAULT_LANGUAGE } from '../base/i18n/i18next';
import {
ENDPOINT_MESSAGE_RECEIVED,
REMOVE_TRANSCRIPT_MESSAGE,
SET_REQUESTING_SUBTITLES,
TOGGLE_REQUESTING_SUBTITLES,
UPDATE_TRANSCRIPT_MESSAGE
} from './actionTypes';
/**
* Signals that a participant sent an endpoint message on the data channel.
*
* @param {Object} participant - The participant details sending the message.
* @param {Object} json - The json carried by the endpoint message.
* @returns {{
* type: ENDPOINT_MESSAGE_RECEIVED,
* participant: Object,
* json: Object
* }}
*/
export function endpointMessageReceived(participant: Object, json: Object) {
return {
type: ENDPOINT_MESSAGE_RECEIVED,
participant,
json
};
}
/**
* Signals that a transcript has to be removed from the state.
*

View File

@@ -4,7 +4,7 @@ import { IReduxState } from '../../app/types';
import { MEET_FEATURES } from '../../base/jwt/constants';
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
import { maybeShowPremiumFeatureDialog } from '../../jaas/actions';
import { canStartTranscribing } from '../functions';
import { canStartSubtitles } from '../functions';
export interface IAbstractProps extends AbstractButtonProps {
@@ -106,7 +106,7 @@ export function _abstractMapStateToProps(state: IReduxState, ownProps: IAbstract
// if the participant is moderator, it can enable transcriptions and if
// transcriptions are already started for the meeting, guests can just show them
const { visible = canStartTranscribing(state) } = ownProps;
const { visible = canStartSubtitles(state) } = ownProps;
return {
_requestingSubtitles,

View File

@@ -1,15 +1,12 @@
import { IReduxState } from '../app/types';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { canAddTranscriber, isTranscribing } from '../transcribing/functions';
/**
* Checks whether the participant can start the transcription.
* Checks whether the participant can start the subtitles.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - True if the participant can start the transcription.
* @returns {boolean} - True if the participant can start the subtitles.
*/
export function canStartTranscribing(state: IReduxState) {
const { transcription } = state['features/base/config'];
const { isTranscribing } = state['features/transcribing'];
return Boolean(transcription?.enabled && (isLocalParticipantModerator(state) || isTranscribing));
export function canStartSubtitles(state: IReduxState) {
return canAddTranscriber(state) || isTranscribing(state);
}

View File

@@ -1,10 +1,10 @@
import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import {
ENDPOINT_MESSAGE_RECEIVED,
SET_REQUESTING_SUBTITLES,
TOGGLE_REQUESTING_SUBTITLES
} from './actionTypes';
@@ -90,7 +90,7 @@ MiddlewareRegistry.register(store => next => action => {
* @returns {Object} The value returned by {@code next(action)}.
*/
function _endpointMessageReceived({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
const { json } = action;
const { data: json } = action;
if (![ JSON_TYPE_TRANSCRIPTION_RESULT, JSON_TYPE_TRANSLATION_RESULT ].includes(json?.type)) {
return next(action);
@@ -100,7 +100,7 @@ function _endpointMessageReceived({ dispatch, getState }: IStore, next: Function
const language
= state['features/base/conference'].conference
?.getLocalParticipantProperty(P_NAME_TRANSLATION_LANGUAGE);
const { skipInterimTranscriptions } = state['features/base/config'].testing ?? {};
const { dumpTranscript, skipInterimTranscriptions } = state['features/base/config'].testing ?? {};
const transcriptMessageID = json.message_id;
const { name, id, avatar_url: avatarUrl } = json.participant;
@@ -146,6 +146,22 @@ function _endpointMessageReceived({ dispatch, getState }: IStore, next: Function
participant,
...txt
});
// Dump transcript in a <transcript> element for debugging purposes.
if (!json.is_interim && dumpTranscript) {
try {
let elem = document.body.getElementsByTagName('transcript')[0];
if (!elem) {
elem = document.createElement('transcript');
document.body.appendChild(elem);
}
elem.append(`${new Date(json.timestamp).toISOString()} ${participant.name}: ${text}`);
} catch (_) {
// Ignored.
}
}
}
// If the suer is not requesting transcriptions just bail.

View File

@@ -23,6 +23,7 @@ import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
import TileViewButton from '../../../video-layout/components/TileViewButton';
import styles from '../../../video-menu/components/native/styles';
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
import { getMovableButtons } from '../../functions.native';
import AudioOnlyButton from './AudioOnlyButton';
@@ -156,6 +157,7 @@ class OverflowMenu extends PureComponent<IProps, IState> {
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<LinkToSalesforceButton { ...buttonProps } />
<WhiteboardButton { ...buttonProps } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<SharedVideoButton { ...buttonProps } />

View File

@@ -1,6 +1,9 @@
import i18next from 'i18next';
import { IReduxState } from '../app/types';
import { IConfig } from '../base/config/configType';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import JITSI_TO_BCP47_MAP from './jitsi-bcp47-map.json';
import logger from './logger';
@@ -42,3 +45,34 @@ export function determineTranscriptionLanguage(config: IConfig) {
return safeBCP47Locale;
}
/**
* Returns whether there is transcribing.
*
* @param {IReduxState} state - The redux state to search in.
* @returns {boolean}
*/
export function isTranscribing(state: IReduxState) {
return state['features/transcribing'].isTranscribing;
}
/**
* Checks whether the participant can start the transcription.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - True if the participant can start the transcription.
*/
export function canAddTranscriber(state: IReduxState) {
const { transcription } = state['features/base/config'];
const isJwtTranscribingEnabled = isJwtFeatureEnabled(state, 'transcription', isLocalParticipantModerator(state));
if (!transcription?.enabled) {
return false;
}
if (isJwtTranscribingEnabled) {
return true;
}
return false;
}

View File

@@ -93,9 +93,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState: IStore['getState'], on: boolean) {
const state = getState();
const { sessionDatas } = state['features/recording'];
const { status: statusConstants } = JitsiRecordingConstants;
const { mode: modeConstants, status: statusConstants } = JitsiRecordingConstants;
if (sessionDatas.some(sd => sd.mode !== statusConstants.OFF)) {
if (sessionDatas.some(sd => sd.mode === modeConstants.FILE && sd.status === statusConstants.ON)) {
// If a recording is still ongoing, don't send any notification.
return;
}

View File

@@ -36,7 +36,7 @@ export function admitMultiple(requests: Array<IPromotionRequest>): Function {
* @param {IPromotionRequest} request - The request from the visitor.
* @returns {Function}
*/
export function approveRequest(request: IPromotionRequest): Function {
export function approveRequest(request: IPromotionRequest) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const conference = getCurrentConference(getState);
@@ -57,7 +57,7 @@ export function approveRequest(request: IPromotionRequest): Function {
* @param {IPromotionRequest} request - The request from the visitor.
* @returns {Function}
*/
export function denyRequest(request: IPromotionRequest): Function {
export function denyRequest(request: IPromotionRequest) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const conference = getCurrentConference(getState);

View File

@@ -34,3 +34,14 @@ export function getPromotionRequests(state: IReduxState) {
export function iAmVisitor(stateful: IStateful) {
return toState(stateful)['features/visitors'].iAmVisitor;
}
/**
* Returns the number of visitors.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {number} - The number of visitors.
*/
export function getVisitorsCount(stateful: IStateful) {
return toState(stateful)['features/visitors'].count ?? 0;
}

View File

@@ -1,11 +1,31 @@
import { CONFERENCE_JOINED, CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
import i18n from 'i18next';
import { batch } from 'react-redux';
import { IStore } from '../app/types';
import {
CONFERENCE_JOINED,
CONFERENCE_JOIN_IN_PROGRESS,
ENDPOINT_MESSAGE_RECEIVED
} from '../base/conference/actionTypes';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { raiseHand } from '../base/participants/actions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { BUTTON_TYPES } from '../base/ui/constants.any';
import { hideNotification, showNotification } from '../notifications/actions';
import {
NOTIFICATION_ICON,
NOTIFICATION_TIMEOUT_TYPE,
VISITORS_PROMOTION_NOTIFICATION_ID
} from '../notifications/constants';
import { open as openParticipantsPane } from '../participants-pane/actions';
import { clearPromotionRequest, promotionRequestReceived, updateVisitorsCount } from './actions';
import {
approveRequest,
clearPromotionRequest,
denyRequest,
promotionRequestReceived,
updateVisitorsCount
} from './actions';
import { getPromotionRequests } from './functions';
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
@@ -44,25 +64,94 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
} else {
dispatch(clearPromotionRequest(request));
}
_handlePromotionNotification({
dispatch,
getState
});
});
conference.on(JitsiConferenceEvents.VISITORS_REJECTION, () => {
dispatch(raiseHand(false));
});
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(user: any, data: any) => {
if (data?.action === 'promotion-response' && data.approved) {
const request = getPromotionRequests(getState())
.find(r => r.from === data.id);
break;
}
case ENDPOINT_MESSAGE_RECEIVED: {
const { data } = action;
request && dispatch(clearPromotionRequest(request));
}
});
if (data?.action === 'promotion-response' && data.approved) {
const request = getPromotionRequests(getState())
.find(r => r.from === data.id);
request && dispatch(clearPromotionRequest(request));
}
break;
}
}
return next(action);
});
/**
* Function to handle the promotion notification.
*
* @param {Object} store - The Redux store.
* @returns {void}
*/
function _handlePromotionNotification(
{ dispatch, getState }: { dispatch: IStore['dispatch']; getState: IStore['getState']; }) {
const requests = getPromotionRequests(getState());
if (requests.length === 0) {
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
return;
}
let notificationTitle;
let customActionNameKey;
let customActionHandler;
let customActionType;
let descriptionKey;
let icon;
if (requests.length === 1) {
const firstRequest = requests[0];
descriptionKey = 'notify.participantWantsToJoin';
notificationTitle = firstRequest.nick;
icon = NOTIFICATION_ICON.PARTICIPANT;
customActionNameKey = [ 'participantsPane.actions.admit', 'participantsPane.actions.reject' ];
customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
dispatch(approveRequest(firstRequest));
}),
() => batch(() => {
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
dispatch(denyRequest(firstRequest));
}) ];
} else {
descriptionKey = 'notify.participantsWantToJoin';
notificationTitle = i18n.t('notify.waitingParticipants', {
waitingParticipants: requests.length
});
icon = NOTIFICATION_ICON.PARTICIPANTS;
customActionNameKey = [ 'notify.viewLobby' ];
customActionType = [ BUTTON_TYPES.PRIMARY ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
dispatch(openParticipantsPane());
}) ];
}
dispatch(showNotification({
title: notificationTitle,
descriptionKey,
uid: VISITORS_PROMOTION_NOTIFICATION_ID,
customActionNameKey,
customActionType,
customActionHandler,
icon
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
}

View File

@@ -1,31 +1,68 @@
import { IStore } from '../app/types';
import { showWarningNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { setWhiteboardOpen } from './actions';
import { isWhiteboardAllowed, isWhiteboardOpen, isWhiteboardVisible } from './functions';
import { WhiteboardStatus } from './types';
import {
RESET_WHITEBOARD,
SETUP_WHITEBOARD,
SET_WHITEBOARD_OPEN
} from './actionTypes';
import { IWhiteboardAction } from './reducer';
/**
* API to toggle the whiteboard.
* Configures the whiteboard collaboration details.
*
* @param {Object} payload - The whiteboard settings.
* @returns {{
* type: SETUP_WHITEBOARD,
* collabDetails: { roomId: string, roomKey: string }
* }}
*/
export const setupWhiteboard = ({ collabDetails }: {
collabDetails: { roomId: string; roomKey: string; };
}): IWhiteboardAction => {
return {
type: SETUP_WHITEBOARD,
collabDetails
};
};
/**
* Cleans up the whiteboard collaboration settings.
* To be used only on native for cleanup in between conferences.
*
* @returns {{
* type: RESET_WHITEBOARD
* }}
*/
export const resetWhiteboard = (): IWhiteboardAction => {
return { type: RESET_WHITEBOARD };
};
/**
* Sets the whiteboard visibility status.
*
* @param {boolean} isOpen - The whiteboard visibility flag.
* @returns {{
* type: SET_WHITEBOARD_OPEN,
* isOpen
* }}
*/
export const setWhiteboardOpen = (isOpen: boolean): IWhiteboardAction => {
return {
type: SET_WHITEBOARD_OPEN,
isOpen
};
};
/**
* Shows a warning notification about the whiteboard user limit.
*
* @returns {Function}
*/
export function toggleWhiteboard() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const isAllowed = isWhiteboardAllowed(state);
const isOpen = isWhiteboardOpen(state);
if (isAllowed) {
if (isOpen && !isWhiteboardVisible(state)) {
dispatch(setWhiteboardOpen(true));
} else if (isOpen && isWhiteboardVisible(state)) {
dispatch(setWhiteboardOpen(false));
} else if (!isOpen) {
dispatch(setWhiteboardOpen(true));
}
} else if (typeof APP !== 'undefined') {
APP.API.notifyWhiteboardStatusChanged(WhiteboardStatus.FORBIDDEN);
}
};
}
export const notifyWhiteboardLimit = () => (dispatch: IStore['dispatch']) => {
dispatch(showWarningNotification({
titleKey: 'notify.whiteboardLimitTitle',
descriptionKey: 'notify.whiteboardLimitDescription'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
};

View File

@@ -0,0 +1,23 @@
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { resetWhiteboard } from './actions.any';
export * from './actions.any';
/**
* Restricts the whiteboard usage.
*
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard.
* @returns {Function}
*/
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => {
if (shouldCloseWhiteboard) {
navigateRoot(screen.conference.root);
}
dispatch(resetWhiteboard());
sendAnalytics(createRestrictWhiteboardEvent());
};

View File

@@ -1,82 +0,0 @@
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { showWarningNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import {
RESET_WHITEBOARD,
SETUP_WHITEBOARD,
SET_WHITEBOARD_OPEN
} from './actionTypes';
import { IWhiteboardAction } from './reducer';
/**
* Configures the whiteboard collaboration details.
*
* @param {Object} payload - The whiteboard settings.
* @returns {{
* type: SETUP_WHITEBOARD,
* collabDetails: { roomId: string, roomKey: string }
* }}
*/
export const setupWhiteboard = ({ collabDetails }: {
collabDetails: { roomId: string; roomKey: string; };
}): IWhiteboardAction => {
return {
type: SETUP_WHITEBOARD,
collabDetails
};
};
/**
* Cleans up the whiteboard collaboration settings.
* To be used only on native for cleanup in between conferences.
*
* @returns {{
* type: RESET_WHITEBOARD
* }}
*/
export const resetWhiteboard = (): IWhiteboardAction => {
return { type: RESET_WHITEBOARD };
};
/**
* Sets the whiteboard visibility status.
*
* @param {boolean} isOpen - The whiteboard visibility flag.
* @returns {{
* type: SET_WHITEBOARD_OPEN,
* isOpen
* }}
*/
export const setWhiteboardOpen = (isOpen: boolean): IWhiteboardAction => {
return {
type: SET_WHITEBOARD_OPEN,
isOpen
};
};
/**
* Shows a warning notification about the whiteboard user limit.
*
* @returns {Function}
*/
export const notifyWhiteboardLimit = () => (dispatch: IStore['dispatch']) => {
dispatch(showWarningNotification({
titleKey: 'notify.whiteboardLimitTitle',
descriptionKey: 'notify.whiteboardLimitDescription'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
};
/**
* Restricts the whiteboard usage.
*
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard participant.
* @returns {Function}
*/
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => {
shouldCloseWhiteboard && dispatch(setWhiteboardOpen(false));
dispatch(resetWhiteboard());
sendAnalytics(createRestrictWhiteboardEvent());
};

View File

@@ -0,0 +1,48 @@
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { resetWhiteboard, setWhiteboardOpen } from './actions.any';
import { isWhiteboardAllowed, isWhiteboardOpen, isWhiteboardVisible } from './functions';
import { WhiteboardStatus } from './types';
export * from './actions.any';
/**
* API to toggle the whiteboard.
*
* @returns {Function}
*/
export function toggleWhiteboard() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const isAllowed = isWhiteboardAllowed(state);
const isOpen = isWhiteboardOpen(state);
if (isAllowed) {
if (isOpen && !isWhiteboardVisible(state)) {
dispatch(setWhiteboardOpen(true));
} else if (isOpen && isWhiteboardVisible(state)) {
dispatch(setWhiteboardOpen(false));
} else if (!isOpen) {
dispatch(setWhiteboardOpen(true));
}
} else if (typeof APP !== 'undefined') {
APP.API.notifyWhiteboardStatusChanged(WhiteboardStatus.FORBIDDEN);
}
};
}
/**
* Restricts the whiteboard usage.
*
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard.
* @returns {Function}
*/
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => {
if (shouldCloseWhiteboard) {
dispatch(setWhiteboardOpen(false));
}
dispatch(resetWhiteboard());
sendAnalytics(createRestrictWhiteboardEvent());
};

View File

@@ -0,0 +1,220 @@
import { Route } from '@react-navigation/native';
import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { View, ViewStyle } from 'react-native';
import { WebView } from 'react-native-webview';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { getCurrentConference } from '../../../base/conference/functions';
import { IJitsiConference } from '../../../base/conference/reducer';
import { openDialog } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import { safeDecodeURIComponent } from '../../../base/util/uri';
import { setupWhiteboard } from '../../actions.any';
import { WHITEBOARD_ID } from '../../constants';
import { getCollabServerUrl, getWhiteboardInfoForURIString } from '../../functions';
import WhiteboardErrorDialog from './WhiteboardErrorDialog';
import styles, { INDICATOR_COLOR } from './styles';
interface IProps extends WithTranslation {
/**
* The whiteboard collab server url.
*/
collabServerUrl?: string;
/**
* The current Jitsi conference.
*/
conference?: IJitsiConference;
/**
* Redux store dispatch method.
*/
dispatch: IStore['dispatch'];
/**
* Window location href.
*/
locationHref: string;
/**
* Default prop for navigating between screen components(React Navigation).
*/
navigation: any;
/**
* Default prop for navigating between screen components(React Navigation).
*/
route: Route<'', {
collabDetails: { roomId: string; roomKey: string; };
collabServerUrl: string;
localParticipantName: string;
}>;
}
/**
* Implements a React native component that displays the whiteboard page for a specific room.
*/
class Whiteboard extends PureComponent<IProps> {
/**
* Initializes a new instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._onError = this._onError.bind(this);
this._onNavigate = this._onNavigate.bind(this);
this._onMessage = this._onMessage.bind(this);
this._renderLoading = this._renderLoading.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately after mounting occurs.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
const { navigation, t } = this.props;
navigation.setOptions({
headerTitle: t('whiteboard.screenTitle')
});
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { locationHref, route } = this.props;
const collabServerUrl = safeDecodeURIComponent(route.params?.collabServerUrl);
const localParticipantName = safeDecodeURIComponent(route.params?.localParticipantName);
const collabDetails = route.params?.collabDetails;
const uri = getWhiteboardInfoForURIString(
locationHref,
collabServerUrl,
collabDetails,
localParticipantName
) ?? '';
return (
<JitsiScreen
safeAreaInsets = { [ 'bottom', 'left', 'right' ] }
style = { styles.backDrop }>
<WebView
incognito = { true }
javaScriptEnabled = { true }
nestedScrollEnabled = { true }
onError = { this._onError }
onMessage = { this._onMessage }
onShouldStartLoadWithRequest = { this._onNavigate }
renderLoading = { this._renderLoading }
scrollEnabled = { true }
setSupportMultipleWindows = { false }
source = {{ uri }}
startInLoadingState = { true }
style = { styles.webView } />
</JitsiScreen>
);
}
/**
* Callback to handle the error if the page fails to load.
*
* @returns {void}
*/
_onError() {
this.props.dispatch(openDialog(WhiteboardErrorDialog));
}
/**
* Callback to intercept navigation inside the webview and make the native app handle the whiteboard requests.
*
* NOTE: We don't navigate to anywhere else from that view.
*
* @param {any} request - The request object.
* @returns {boolean}
*/
_onNavigate(request: { url: string; }) {
const { url } = request;
const { locationHref, route } = this.props;
const collabServerUrl = route.params?.collabServerUrl;
const collabDetails = route.params?.collabDetails;
const localParticipantName = route.params?.localParticipantName;
return url === getWhiteboardInfoForURIString(
locationHref,
collabServerUrl,
collabDetails,
localParticipantName
);
}
/**
* Callback to handle the message events.
*
* @param {any} event - The event.
* @returns {void}
*/
_onMessage(event: any) {
const { collabServerUrl, conference } = this.props;
const collabDetails = JSON.parse(event.nativeEvent.data);
if (collabDetails?.roomId && collabDetails?.roomKey) {
this.props.dispatch(setupWhiteboard({ collabDetails }));
// Broadcast the collab details.
conference?.getMetadataHandler().setMetadata(WHITEBOARD_ID, {
collabServerUrl,
collabDetails
});
}
}
/**
* Renders the loading indicator.
*
* @returns {React$Component<any>}
*/
_renderLoading() {
return (
<View style = { styles.indicatorWrapper as ViewStyle }>
<LoadingIndicator
color = { INDICATOR_COLOR }
size = 'large' />
</View>
);
}
}
/**
* Maps (parts of) the redux state to the associated
* {@code WaitForOwnerDialog}'s props.
*
* @param {Object} state - The redux state.
* @private
* @returns {IProps}
*/
function mapStateToProps(state: IReduxState) {
const { locationURL } = state['features/base/connection'];
const { href = '' } = locationURL ?? {};
return {
conference: getCurrentConference(state),
collabServerUrl: getCollabServerUrl(state),
locationHref: href
};
}
export default translate(connect(mapStateToProps)(Whiteboard));

View File

@@ -0,0 +1,43 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconWhiteboard } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { setWhiteboardOpen } from '../../actions.any';
import { isWhiteboardButtonVisible } from '../../functions';
/**
* Component that renders a toolbar button for the whiteboard.
*/
class WhiteboardButton extends AbstractButton<AbstractButtonProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.showWhiteboard';
icon = IconWhiteboard;
label = 'toolbar.showWhiteboard';
tooltip = 'toolbar.showWhiteboard';
/**
* Handles clicking / pressing the button, and opens the whiteboard view.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(setWhiteboardOpen(true));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
return {
visible: isWhiteboardButtonVisible(state)
};
}
export default translate(connect(_mapStateToProps)(WhiteboardButton));

View File

@@ -0,0 +1,15 @@
import React from 'react';
import AlertDialog from '../../../base/dialog/components/native/AlertDialog';
/**
* Dialog to inform the user that we couldn't load the whiteboard.
*
* @returns {JSX.Element}
*/
const WhiteboardErrorDialog = () => (
<AlertDialog
contentKey = 'info.whiteboardError' />
);
export default WhiteboardErrorDialog;

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TextStyle } from 'react-native';
import { useSelector } from 'react-redux';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import Link from '../../../base/react/components/native/Link';
import { getWhiteboardConfig } from '../../functions';
import styles from './styles';
/**
* Component that renders the whiteboard user limit dialog.
*
* @returns {JSX.Element}
*/
const WhiteboardLimitDialog = () => {
const { t } = useTranslation();
const { limitUrl } = useSelector(getWhiteboardConfig);
return (
<ConfirmDialog
cancelLabel = { 'dialog.Ok' }
descriptionKey = { 'dialog.whiteboardLimitContent' }
isConfirmHidden = { true }
title = { 'dialog.whiteboardLimitTitle' }>
{limitUrl && (
<Text style = { styles.limitUrlText as TextStyle }>
{` ${t('dialog.whiteboardLimitReference')}
`}
<Link
style = { styles.limitUrl }
url = { limitUrl }>
{t('dialog.whiteboardLimitReferenceUrl')}
</Link>
.
</Text>
)}
</ConfirmDialog>
);
};
export default WhiteboardLimitDialog;

View File

@@ -0,0 +1,37 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export const INDICATOR_COLOR = BaseTheme.palette.ui07;
const WV_BACKGROUND = BaseTheme.palette.ui03;
export default {
backDrop: {
backgroundColor: WV_BACKGROUND,
flex: 1
},
indicatorWrapper: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui10,
height: '100%',
justifyContent: 'center'
},
webView: {
backgroundColor: WV_BACKGROUND,
flex: 1
},
limitUrlText: {
alignItems: 'center',
display: 'flex',
marginBottom: BaseTheme.spacing[2],
textAlign: 'center'
},
limitUrl: {
color: BaseTheme.palette.link01,
fontWeight: 'bold'
}
};

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
/**
* The type of the React {@code Component} props of {@link NoWhiteboardError}.
*/
interface IProps {
/**
* Additional CSS classnames to append to the root of the component.
*/
className?: string;
}
const NoWhiteboardError = ({ className }: IProps) => {
const { t } = useTranslation();
return (
<div className = { className } >
{t('info.noWhiteboard')}
</div>
);
};
export default NoWhiteboardError;

View File

@@ -1,5 +1,6 @@
import { ExcalidrawApp } from '@jitsi/excalidraw';
import clsx from 'clsx';
import i18next from 'i18next';
import React, { useCallback, useEffect, useRef } from 'react';
import { WithTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
@@ -143,6 +144,7 @@ const Whiteboard = (props: WithTranslation): JSX.Element => {
collabServerUrl = { collabServerUrl }
excalidraw = {{
isCollaborating: true,
langCode: i18next.language,
// @ts-ignore
ref: excalidrawRef,

View File

@@ -0,0 +1,91 @@
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
import React, { ComponentType } from 'react';
import BaseApp from '../../../base/app/components/BaseApp';
import GlobalStyles from '../../../base/ui/components/GlobalStyles.web';
import JitsiThemeProvider from '../../../base/ui/components/JitsiThemeProvider.web';
import { decodeFromBase64URL } from '../../../base/util/httpUtils';
import { parseURLParams } from '../../../base/util/parseURLParams';
import { safeDecodeURIComponent } from '../../../base/util/uri';
import logger from '../../logger';
import NoWhiteboardError from './NoWhiteboardError';
import WhiteboardWrapper from './WhiteboardWrapper';
/**
* Wrapper application for the whiteboard.
*
* @augments BaseApp
*/
export default class WhiteboardApp extends BaseApp<any> {
/**
* Navigates to {@link Whiteboard} upon mount.
*
* @returns {void}
*/
async componentDidMount() {
await super.componentDidMount();
const { state } = parseURLParams(window.location.href, true);
const decodedState = JSON.parse(decodeFromBase64URL(state));
const { collabServerUrl, localParticipantName } = decodedState;
let { roomId, roomKey } = decodedState;
if (!roomId && !roomKey) {
try {
const collabDetails = await generateCollaborationLinkData();
roomId = collabDetails.roomId;
roomKey = collabDetails.roomKey;
if (window.ReactNativeWebView) {
setTimeout(() => {
window.ReactNativeWebView.postMessage(JSON.stringify(collabDetails));
}, 0);
}
} catch (e: any) {
logger.error('Couldn\'t generate collaboration link data.', e);
}
}
super._navigate({
component: () => (
<>{
roomId && roomKey && collabServerUrl
? <WhiteboardWrapper
className = 'whiteboard'
collabDetails = {{
roomId,
roomKey
}}
collabServerUrl = { safeDecodeURIComponent(collabServerUrl) }
localParticipantName = { localParticipantName } />
: <NoWhiteboardError className = 'whiteboard' />
}</>
) });
}
/**
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
* the top most component.
*
* @override
*/
_createMainElement(component: ComponentType<any>, props: Object) {
return (
<JitsiThemeProvider>
<GlobalStyles />
{super._createMainElement(component, props)}
</JitsiThemeProvider>
);
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer() {
return null;
}
}

View File

@@ -5,7 +5,7 @@ import { translate } from '../../../base/i18n/functions';
import { IconWhiteboard, IconWhiteboardHide } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
import { setWhiteboardOpen } from '../../actions';
import { setWhiteboardOpen } from '../../actions.any';
import { isWhiteboardButtonVisible, isWhiteboardVisible } from '../../functions';
interface IProps extends AbstractButtonProps {

View File

@@ -0,0 +1,70 @@
import { ExcalidrawApp } from '@jitsi/excalidraw';
import i18next from 'i18next';
import React, { useCallback, useRef } from 'react';
import { WHITEBOARD_UI_OPTIONS } from '../../constants';
/**
* Whiteboard wrapper for mobile.
*
* @returns {JSX.Element}
*/
const WhiteboardWrapper = ({
className,
collabDetails,
collabServerUrl,
localParticipantName
}: {
className?: string;
collabDetails: {
roomId: string;
roomKey: string;
};
collabServerUrl: string;
localParticipantName: string;
}) => {
const excalidrawRef = useRef<any>(null);
const excalidrawAPIRef = useRef<any>(null);
const collabAPIRef = useRef<any>(null);
const getExcalidrawAPI = useCallback(excalidrawAPI => {
if (excalidrawAPIRef.current) {
return;
}
excalidrawAPIRef.current = excalidrawAPI;
}, []);
const getCollabAPI = useCallback(collabAPI => {
if (collabAPIRef.current) {
return;
}
collabAPIRef.current = collabAPI;
collabAPIRef.current.setUsername(localParticipantName);
}, [ localParticipantName ]);
return (
<div className = { className }>
<div className = 'excalidraw-wrapper'>
<ExcalidrawApp
collabDetails = { collabDetails }
collabServerUrl = { collabServerUrl }
detectScroll = { true }
excalidraw = {{
isCollaborating: true,
langCode: i18next.language,
// @ts-ignore
ref: excalidrawRef,
theme: 'light',
UIOptions: WHITEBOARD_UI_OPTIONS
}}
getCollabAPI = { getCollabAPI }
getExcalidrawAPI = { getExcalidrawAPI } />
</div>
</div>
);
};
export default WhiteboardWrapper;

View File

@@ -62,3 +62,10 @@ export const MIN_USER_LIMIT = 10;
* Whiteboard soft limit diff.
*/
export const USER_LIMIT_THRESHOLD = 5;
/**
* The pathName for the whiteboard page.
*
* @type {string}
*/
export const WHITEBOARD_PATH_NAME = 'static/whiteboard.html';

View File

@@ -5,10 +5,11 @@ import { IReduxState } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { IWhiteboardConfig } from '../base/config/configType';
import { getRemoteParticipants, isLocalParticipantModerator } from '../base/participants/functions';
import { appendURLParam } from '../base/util/uri';
import { encodeToBase64URL } from '../base/util/httpUtils';
import { appendURLHashParam, appendURLParam } from '../base/util/uri';
import { getCurrentRoomId, isInBreakoutRoom } from '../breakout-rooms/functions';
import { MIN_USER_LIMIT, USER_LIMIT_THRESHOLD, WHITEBOARD_ID } from './constants';
import { MIN_USER_LIMIT, USER_LIMIT_THRESHOLD, WHITEBOARD_ID, WHITEBOARD_PATH_NAME } from './constants';
import { IWhiteboardState } from './reducer';
const getWhiteboardState = (state: IReduxState): IWhiteboardState => state['features/whiteboard'];
@@ -156,3 +157,50 @@ export const shouldNotifyUserLimit = (state: IReduxState): boolean => {
return participantCount + USER_LIMIT_THRESHOLD > userLimit;
};
/**
* Generates the URL for the static whiteboard page.
*
* @param {string} locationUrl - The window location href.
* @param {string} collabServerUrl - The whiteboard collaboration server url.
* @param {Object} collabDetails - The whiteboard collaboration details.
* @param {string} localParticipantName - The local participant name.
* @returns {string}
*/
export function getWhiteboardInfoForURIString(
locationUrl: any,
collabServerUrl: string,
collabDetails: { roomId: string; roomKey: string; },
localParticipantName: string
): string | undefined {
if (!collabServerUrl || !locationUrl) {
return undefined;
}
let state = {};
let url = `${locationUrl.substring(0, locationUrl.lastIndexOf('/'))}/${WHITEBOARD_PATH_NAME}`;
if (collabDetails?.roomId) {
state = {
...state,
roomId: collabDetails.roomId
};
}
if (collabDetails?.roomKey) {
state = {
...state,
roomKey: collabDetails.roomKey
};
}
state = {
...state,
collabServerUrl,
localParticipantName
};
url = appendURLHashParam(url, 'state', encodeToBase64URL(JSON.stringify(state)));
return url;
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/whiteboard');

View File

@@ -0,0 +1,92 @@
import { createOpenWhiteboardEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
import { getCurrentConference } from '../base/conference/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SET_WHITEBOARD_OPEN } from './actionTypes';
import {
notifyWhiteboardLimit,
resetWhiteboard,
restrictWhiteboard,
setWhiteboardOpen,
setupWhiteboard
} from './actions';
import { WHITEBOARD_ID } from './constants';
import {
isWhiteboardOpen,
shouldEnforceUserLimit,
shouldNotifyUserLimit
} from './functions';
MiddlewareRegistry.register((store: IStore) => next => action => {
const state = store.getState();
switch (action.type) {
case SET_WHITEBOARD_OPEN: {
const enforceUserLimit = shouldEnforceUserLimit(state);
const notifyUserLimit = shouldNotifyUserLimit(state);
if (action.isOpen && !enforceUserLimit && !notifyUserLimit) {
sendAnalytics(createOpenWhiteboardEvent());
return next(action);
}
break;
}
case UPDATE_CONFERENCE_METADATA: {
const { metadata } = action;
if (metadata?.[WHITEBOARD_ID]) {
store.dispatch(setupWhiteboard({
collabDetails: metadata[WHITEBOARD_ID].collabDetails
}));
store.dispatch(setWhiteboardOpen(true));
}
break;
}
}
return next(action);
});
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed, e.g. Disable the whiteboard if it's left open.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch }, previousConference): void => {
if (conference !== previousConference) {
dispatch(resetWhiteboard());
}
});
/**
* Set up state change listener to limit whiteboard access.
*/
StateListenerRegistry.register(
state => shouldEnforceUserLimit(state),
(enforceUserLimit, { dispatch, getState }): void => {
if (isWhiteboardOpen(getState()) && enforceUserLimit) {
dispatch(restrictWhiteboard());
}
}
);
/**
* Set up state change listener to notify about whiteboard usage.
*/
StateListenerRegistry.register(
state => shouldNotifyUserLimit(state),
(notifyUserLimit, { dispatch, getState }, prevNotifyUserLimit): void => {
if (isWhiteboardOpen(getState()) && notifyUserLimit && !prevNotifyUserLimit) {
dispatch(notifyWhiteboardLimit());
}
}
);

View File

@@ -0,0 +1,80 @@
import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { hideDialog, openDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import { getLocalParticipant } from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { SET_WHITEBOARD_OPEN } from './actionTypes';
import {
notifyWhiteboardLimit,
restrictWhiteboard
} from './actions';
import WhiteboardLimitDialog from './components/native/WhiteboardLimitDialog';
import {
getCollabDetails,
getCollabServerUrl,
shouldEnforceUserLimit,
shouldNotifyUserLimit
} from './functions';
import './middleware.any';
/**
* Middleware which intercepts whiteboard actions to handle changes to the related state.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action: AnyAction) => {
const { dispatch, getState } = store;
const state = getState();
switch (action.type) {
case SET_WHITEBOARD_OPEN: {
const enforceUserLimit = shouldEnforceUserLimit(state);
const notifyUserLimit = shouldNotifyUserLimit(state);
if (enforceUserLimit) {
dispatch(restrictWhiteboard(false));
dispatch(openDialog(WhiteboardLimitDialog));
return next(action);
}
if (action.isOpen) {
if (enforceUserLimit) {
dispatch(restrictWhiteboard());
return next(action);
}
if (notifyUserLimit) {
dispatch(notifyWhiteboardLimit());
}
if (isDialogOpen(state, WhiteboardLimitDialog)) {
dispatch(hideDialog(WhiteboardLimitDialog));
}
const collabDetails = getCollabDetails(state);
const collabServerUrl = getCollabServerUrl(state);
const localParticipantName = getLocalParticipant(state)?.name;
navigateRoot(screen.conference.whiteboard, {
collabDetails,
collabServerUrl,
localParticipantName
});
return next(action);
}
break;
}
}
return next(action);
});

View File

@@ -1,17 +1,13 @@
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
import { AnyAction } from 'redux';
import { createOpenWhiteboardEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { IStore } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { hideDialog, openDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { participantJoined, participantLeft, pinParticipant } from '../base/participants/actions';
import { FakeParticipant } from '../base/participants/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { getCurrentRoomId } from '../breakout-rooms/functions';
import { addStageParticipant } from '../filmstrip/actions.web';
import { isStageFilmstripAvailable } from '../filmstrip/functions.web';
@@ -19,9 +15,7 @@ import { isStageFilmstripAvailable } from '../filmstrip/functions.web';
import { RESET_WHITEBOARD, SET_WHITEBOARD_OPEN } from './actionTypes';
import {
notifyWhiteboardLimit,
resetWhiteboard,
restrictWhiteboard,
setWhiteboardOpen,
setupWhiteboard
} from './actions';
import WhiteboardLimitDialog from './components/web/WhiteboardLimitDialog';
@@ -29,13 +23,14 @@ import { WHITEBOARD_ID, WHITEBOARD_PARTICIPANT_NAME } from './constants';
import {
getCollabDetails,
getCollabServerUrl,
isWhiteboardOpen,
isWhiteboardPresent,
shouldEnforceUserLimit,
shouldNotifyUserLimit
} from './functions';
import { WhiteboardStatus } from './types';
import './middleware.any';
const focusWhiteboard = (store: IStore) => {
const { dispatch, getState } = store;
const state = getState();
@@ -79,7 +74,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
dispatch(restrictWhiteboard(false));
dispatch(openDialog(WhiteboardLimitDialog));
return;
return next(action);
}
if (!existingCollabDetails) {
@@ -99,14 +94,14 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
});
raiseWhiteboardNotification(WhiteboardStatus.INSTANTIATED);
return;
return next(action);
}
if (action.isOpen) {
if (enforceUserLimit) {
dispatch(restrictWhiteboard());
return;
return next(action);
}
if (notifyUserLimit) {
@@ -118,10 +113,9 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action
}
focusWhiteboard(store);
sendAnalytics(createOpenWhiteboardEvent());
raiseWhiteboardNotification(WhiteboardStatus.SHOWN);
return;
return next(action);
}
dispatch(participantLeft(WHITEBOARD_ID, conference, { fakeParticipant: FakeParticipant.Whiteboard }));
@@ -152,49 +146,3 @@ function raiseWhiteboardNotification(status: WhiteboardStatus) {
}
}
/**
* Set up state change listener to perform maintenance tasks when the conference
* is left or failed, e.g. Disable the whiteboard if it's left open.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch }, previousConference): void => {
if (conference !== previousConference) {
dispatch(resetWhiteboard());
}
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: any) => {
if (metadata[WHITEBOARD_ID]) {
dispatch(setupWhiteboard({
collabDetails: metadata[WHITEBOARD_ID].collabDetails
}));
dispatch(setWhiteboardOpen(true));
}
});
}
});
/**
* Set up state change listener to limit whiteboard access.
*/
StateListenerRegistry.register(
state => shouldEnforceUserLimit(state),
(enforceUserLimit, { dispatch, getState }): void => {
if (isWhiteboardOpen(getState()) && enforceUserLimit) {
dispatch(restrictWhiteboard());
}
}
);
/**
* Set up state change listener to notify about whiteboard usage.
*/
StateListenerRegistry.register(
state => shouldNotifyUserLimit(state),
(notifyUserLimit, { dispatch, getState }, prevNotifyUserLimit): void => {
if (isWhiteboardOpen(getState()) && notifyUserLimit && !prevNotifyUserLimit) {
dispatch(notifyWhiteboardLimit());
}
}
);

View File

@@ -7,6 +7,7 @@ import Platform from './features/base/react/Platform.web';
import { getJitsiMeetGlobalNS } from './features/base/util/helpers';
import DialInSummaryApp from './features/invite/components/dial-in-summary/web/DialInSummaryApp';
import PrejoinApp from './features/prejoin/components/web/PrejoinApp';
import WhiteboardApp from './features/whiteboard/components/web/WhiteboardApp';
const logger = getLogger('index.web');
@@ -60,7 +61,8 @@ document.addEventListener('DOMContentLoaded', () => {
globalNS.entryPoints = {
APP: App,
PREJOIN: PrejoinApp,
DIALIN: DialInSummaryApp
DIALIN: DialInSummaryApp,
WHITEBOARD: WhiteboardApp
};
globalNS.renderEntryPoint = ({

View File

@@ -105,6 +105,7 @@ VirtualHost 'vX.meet.jitsi'
'external_services';
'smacks';
'jiconop';
'conference_duration';
}
main_muc = 'conference.vX.meet.jitsi';

View File

@@ -2,7 +2,7 @@
-- * session resumption
-- Copyright (C) 2021-present 8x8, Inc.
local generate_random_id = require "util.id".medium;
local generate_uuid = require "util.uuid".generate;
local new_sasl = require "util.sasl".new;
local sasl = require "util.sasl";
local sessions = prosody.full_sessions;
@@ -59,7 +59,7 @@ module:provides("auth", provider);
local function anonymous(self, message)
-- Same as the vanilla anonymous auth plugin
local username = generate_random_id():lower();
local username = generate_uuid();
-- This calls the handler created in 'provider.get_sasl_handler(session)'
local result, err, msg = self.profile.anonymous(self, username, self.realm);

Some files were not shown because too many files have changed in this diff Show More