Compare commits
33 Commits
7788
...
jitsi-meet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
895afbab65 | ||
|
|
1d6529af65 | ||
|
|
4d5fb719d2 | ||
|
|
4061a77af8 | ||
|
|
7f889b2028 | ||
|
|
6f49041d80 | ||
|
|
e73c3b6697 | ||
|
|
5a6b1d0b47 | ||
|
|
da9cded75b | ||
|
|
9b61ad3616 | ||
|
|
583725bf31 | ||
|
|
9e2244210d | ||
|
|
361b82a1ed | ||
|
|
0ed25cda7e | ||
|
|
73ee1205eb | ||
|
|
21f2c60638 | ||
|
|
fd062c40fb | ||
|
|
b87e6abc11 | ||
|
|
9f25726706 | ||
|
|
d5ee7f3069 | ||
|
|
ba1102100a | ||
|
|
e1ce83d0c3 | ||
|
|
78cf510c0b | ||
|
|
3f657c3ded | ||
|
|
2035cd7e62 | ||
|
|
6207e95cad | ||
|
|
40b63a187a | ||
|
|
6c40250e18 | ||
|
|
0268374b88 | ||
|
|
b1b60ec143 | ||
|
|
82c58178b3 | ||
|
|
46d15a9c5c | ||
|
|
248908f476 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
7
css/modals/_whiteboard.scss
Normal file
7
css/modals/_whiteboard.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.whiteboard {
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
@@ -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
1
globals.d.ts
vendored
@@ -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
3
globals.native.d.ts
vendored
@@ -19,6 +19,9 @@ interface IWindow {
|
||||
location: ILocation;
|
||||
PressureObserver?: any;
|
||||
PressureRecord?: any;
|
||||
ReactNativeWebView?: any;
|
||||
TextDecoder?: any;
|
||||
TextEncoder?: any;
|
||||
self: any;
|
||||
top: any;
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
230
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -51,5 +51,6 @@ import '../video-layout/middleware';
|
||||
import '../video-quality/middleware';
|
||||
import '../videosipgw/middleware';
|
||||
import '../visitors/middleware';
|
||||
import '../whiteboard/middleware.any';
|
||||
|
||||
import './middleware';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -55,3 +55,4 @@ import '../video-layout/reducer';
|
||||
import '../video-quality/reducer';
|
||||
import '../videosipgw/reducer';
|
||||
import '../visitors/reducer';
|
||||
import '../whiteboard/reducer';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -201,6 +201,7 @@ export default [
|
||||
'remoteVideoMenu',
|
||||
'roomPasswordNumberOfDigits',
|
||||
'readOnlyName',
|
||||
'recordings',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
'salesforceUrl',
|
||||
|
||||
@@ -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) : ''
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
@@ -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%',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 } />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
23
react/features/whiteboard/actions.native.ts
Normal file
23
react/features/whiteboard/actions.native.ts
Normal 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());
|
||||
};
|
||||
@@ -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());
|
||||
};
|
||||
48
react/features/whiteboard/actions.web.ts
Normal file
48
react/features/whiteboard/actions.web.ts
Normal 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());
|
||||
};
|
||||
220
react/features/whiteboard/components/native/Whiteboard.tsx
Normal file
220
react/features/whiteboard/components/native/Whiteboard.tsx
Normal 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));
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
37
react/features/whiteboard/components/native/styles.ts
Normal file
37
react/features/whiteboard/components/native/styles.ts
Normal 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'
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
91
react/features/whiteboard/components/web/WhiteboardApp.tsx
Normal file
91
react/features/whiteboard/components/web/WhiteboardApp.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
3
react/features/whiteboard/logger.ts
Normal file
3
react/features/whiteboard/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/whiteboard');
|
||||
92
react/features/whiteboard/middleware.any.ts
Normal file
92
react/features/whiteboard/middleware.any.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
);
|
||||
80
react/features/whiteboard/middleware.native.ts
Normal file
80
react/features/whiteboard/middleware.native.ts
Normal 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);
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
@@ -105,6 +105,7 @@ VirtualHost 'vX.meet.jitsi'
|
||||
'external_services';
|
||||
'smacks';
|
||||
'jiconop';
|
||||
'conference_duration';
|
||||
}
|
||||
main_muc = 'conference.vX.meet.jitsi';
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user