Compare commits

..

58 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
38e50e1ca7 chore(rnsdk) bump version 2023-09-15 10:04:13 +02:00
Saúl Ibarra Corretgé
055ee1495e chore(deps,rnsdk) sync dependencies 2023-09-15 09:56:29 +02:00
Calin-Teodor
7b0ff75a40 chore(rn, versions): bump app and sdk versions 2023-09-14 20:00:06 +02:00
damencho
8304e77a04 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1686.0.0+1b5830f1...v1687.0.0+cafe30d7
2023-09-14 16:02:29 +02:00
Saúl Ibarra Corretgé
b1db315582 fix(authentication) fix moderator logout 2023-09-14 13:44:08 +02:00
Shawn
4e785dd982 fix(config): missing colon in example for lobby config 2023-09-14 12:19:35 +02:00
damencho
40f5afcf43 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1685.0.0+02c54a23...v1686.0.0+1b5830f1
2023-09-13 16:27:06 -05:00
Saúl Ibarra Corretgé
de2688bb33 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1683.0.0+fc7775bc...v1685.0.0+02c54a23
2023-09-13 00:10:08 +02:00
Saúl Ibarra Corretgé
76db09303b fix(rn,conference) avoid starting to knock twice
The auto-knock process is started by the lobby middleware.
2023-09-12 23:20:41 +02:00
José Luís Andrade
ea4e20f9a7 lang: Update Portuguese translation (#13818) 2023-09-11 10:34:03 -05:00
rca
01a74856a3 Fix missing closing tag for identity node in presence stanza 2023-09-11 10:03:09 -05:00
Mihaela Dumitru
36045100bf feat(branding) add support for custom icons 2023-09-11 09:11:06 +02:00
Hristo Terezov
cc344cb548 chore(LJM): Update 2023-09-08 16:01:28 -05:00
Hristo Terezov
a2624952a0 feat(iframe-api): replace ice servers 2023-09-08 16:01:28 -05:00
Calinteodor
b8259e00dc sdk(react-native-sdk): created script that compares dep with peerDep versions (#13789)
sdk(react-native-sdk): created script that compares app dep with RNSDK peerDep versions
2023-09-08 20:26:25 +03:00
Horatiu Muresan
463c823d3b fix(toolbar-buttons) Show tileview in toolbar when separate reactions button (#13810) 2023-09-08 14:44:32 +03:00
Shawn
5a6f3ead5a feat(external-api) Add functions to query supported commands/events 2023-09-08 09:31:23 +02:00
Andrei Gavrilescu
1b4d666af3 chore(deps) lib-jitsi-meet@latest (#13801)
https://github.com/jitsi/lib-jitsi-meet/compare/v1681.0.0+6cd397fa...v1682.0.0+9832ef2c
2023-09-06 16:51:44 +03:00
Andrei Gavrilescu
77d299338a feat(rtcstats): use rtcstats from lib-jitsi-meet (#13693)
* added option to use rtctstats from lib-jitsi-meet

* Addressed review feedback:
- moved rtcstats function into JitsiMeetJS.rtcstats
- changed from callbacks to events

* moved rtcstatsUseLibJitsi from analytics to testing

* fixed linting errors

* use ljm rtcstats

* remove debug logs, additional dependencies

* fix ts and dependency

* address code review

---------

Co-authored-by: Nils Ohlmeier <github@ohlmeier.org>
2023-09-06 16:00:53 +03:00
Horatiu Muresan
33fc6e2f3f fix(disable-filmstrip) Fix disabling filmstrim through config
- there was a problem with pinning the participants from the Participants pane with the previous approach
2023-09-06 14:30:13 +03:00
Calinteodor
a95eaa6c2e feat(base/ui): Native buttons UI fixes (#13788)
* feat(base/ui): native buttons UI fixes and improvements
2023-09-05 16:36:09 +03:00
Andrei Gavrilescu
5a3947bb23 feat(amplitude) add amplitude UTM tracking option 2023-09-05 13:20:31 +02:00
Calinteodor
f84a561d9e sdk(react-native-sdk): Update rnsdk peer deps (#13793)
* sdk(react-native-sdk): prepare_sdk script updates regarding deps and peer deps
2023-09-05 14:09:13 +03:00
Robert Pintilii
295878ffff ref(styles) Move some SCSS to JSS (#13568) 2023-09-05 11:20:01 +03:00
Saúl Ibarra Corretgé
609942654a fix(android) disable full-screen when screen-sharing
Fixes not being able to put the app in background mode easily on Android
13.

Fixes: https://github.com/jitsi/jitsi-meet/issues/13513
2023-09-04 19:21:20 +02:00
Calin-Teodor
60ad0196c3 ref(dependency): latest react-native-video and device-info updates 2023-09-04 19:30:12 +03:00
Calin-Teodor
caea6966ef ref(dependency): reverted react-native-dialog dep update 2023-09-04 19:30:12 +03:00
Calin-Teodor
d4c269f7cb ref(dependency): updated native fixDeviceID 2023-09-04 19:30:12 +03:00
Calin-Teodor
54a1ee53b4 ref(dependency): reverted react-native-dialog dep updates 2023-09-04 19:30:12 +03:00
Calin-Teodor
2c51e8ac06 ref(dependency): regenerated podfile.lock file 2023-09-04 19:30:12 +03:00
Calin-Teodor
3cbd69eef2 ref(dependency): update deps after rebase 2023-09-04 19:30:12 +03:00
Calin-Teodor
ee539644d8 ref(dependency): replaced DeviceInfo.getUniqueId with getUniqueId 2023-09-04 19:30:12 +03:00
Calin-Teodor
465263bc97 fixed linter 2023-09-04 19:30:12 +03:00
Calin-Teodor
1def65eb90 ref(dependency): update react native device info dependency 2023-09-04 19:30:12 +03:00
Calin-Teodor
746be98bfc ref(dependency): update react native gesture handler dependency 2023-09-04 19:30:12 +03:00
Calin-Teodor
99b58dd318 ref(dependency): fixed rebase conflict 2023-09-04 19:30:12 +03:00
Gabriel Borlea
df3ef0d895 fix(video-select): remove video preview from device selection and fix video switch on mobile browsers (#13780)
* fix(video-select): remove video preview from device selection and fix video switch on android browsers

* simplify if statement

* add for all mobile devices the stop stream

* move mobile check to middleware

* code review
2023-09-04 16:27:04 +03:00
Saúl Ibarra Corretgé
83e4042668 fix(android) bump target API level to 33
It is now required by the Play Store to target an API released within a
year of $NOW to be able to push updates.
2023-09-04 12:47:08 +02:00
Saúl Ibarra Corretgé
c6e87568b6 chore(deps) react-antive-webview@13.5.1
Requirement for bumping Android API target to 33.
2023-09-04 12:47:08 +02:00
Дамян Минков
0170c65c7b feat: Sends conference request over http before connecting to xmpp (#13725)
* feat: Moves redirected event to connection events.

* feat: Pass room name when connecting.

We need the room name we will join to be able to send the http conference request from ljm.

* squash: Drops dispatching redirected action.

* squash: Updates ljm.
2023-08-29 14:13:04 -05:00
Hristo Terezov
a7c1ccec71 fix: Attempt to fix setSinkId failures. 2023-08-29 13:25:48 -05:00
Hristo Terezov
1adbebf9dc fix(logger): Prevent JSON stringify errors 2023-08-29 12:26:48 -05:00
Mihaela Dumitru
9d68cb52b3 fix(virtual-background) standardize options object (#13760) 2023-08-29 14:02:30 +03:00
Gabriel Borlea
44272b650c fix(rn, participants): set badge horizontal padding 2023-08-29 13:53:03 +03:00
Gabriel Borlea
5ce96d379a fix(rn, settings): row text wrapping 2023-08-29 13:53:03 +03:00
Gabriel Borlea
173c5fe430 fix(rn, settings): fix arrow back navigation for lang selection 2023-08-29 13:53:03 +03:00
Mihaela Dumitru
e10595c3ed fix(breakout-rooms) allow spaces when renaming (#13761) 2023-08-29 13:37:56 +03:00
Calin-Teodor
9138f56701 feat(chat): fixed action import for abstract component 2023-08-28 17:06:34 +03:00
Avram Tudor
974e2a5106 ref: improve handling for room destroyed events (#13591)
* ref: improve handling for room destroyed events

* add missing translation

* code review

* implement kick handling

* implement native handling

* fix tests

* code review changes

* add dialog testId

* fix end conf for react native

* fix lobby test

* add translation for lobby closing

---------

Co-authored-by: Gabriel Borlea <gabriel.borlea@8x8.com>
2023-08-28 15:14:03 +03:00
Horatiu Muresan
509cf661f5 feat(filmstrip) Add config for disabling vertical filmstrip (#13752) 2023-08-28 14:44:45 +03:00
nbeck.indy
25fdea9984 fix(video-menu) hide Grant Moderator inside breakout rooms on native 2023-08-24 11:52:53 +03:00
Calin-Teodor
9979e470fc feat(authentication): fix normal authentication 2023-08-24 11:44:44 +03:00
damencho
2a492f5036 feat(authentication): Fixes logging out on web.
It was hanging up and canceling visiting the logout page.
2023-08-23 10:35:06 -05:00
Hristo Terezov
baf1f01e44 fix(jitsi-local-storage): remove debug log. 2023-08-23 09:14:51 -05:00
damencho
1f8dc944e3 feat(authentication): Changes wait for owner cancel txt.
When in lobby and waiting for host cancel just hides the dialog and leave you waiting in the lobby that is enabled.
2023-08-22 21:51:41 -05:00
damencho
dc07c6fede feat(authentication): Hides password button from lobby on waiting for host. 2023-08-22 21:51:41 -05:00
damencho
94a63f8aea feat(authentication): Fixes logout on web. 2023-08-22 21:51:41 -05:00
Horatiu Muresan
a47cb595db fix(localFlipX) Fix localFlipX for large video (#13728)
- fixed case when localFlipX was taken from store on it`s value update, before the new value was set into store - so always taking the previous value instead of updated one
2023-08-18 17:37:07 +03:00
119 changed files with 4239 additions and 2548 deletions

View File

@@ -10,7 +10,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.1'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath 'com.google.gms:google-services:4.3.14'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'
}
@@ -18,21 +18,16 @@ buildscript {
ext {
kotlinVersion = "1.7.0"
buildToolsVersion = "31.0.0"
compileSdkVersion = 32
buildToolsVersion = "33.0.2"
compileSdkVersion = 33
minSdkVersion = 24
targetSdkVersion = 32
targetSdkVersion = 33
supportLibVersion = "28.0.0"
if (System.properties['os.arch'] == "aarch64") {
// For M1 Users we need to use the NDK 24 which added support for aarch64
ndkVersion = "24.0.8215888"
} else {
// Otherwise we default to the side-by-side NDK version from AGP.
ndkVersion = "21.4.7075529"
}
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
// The Maven artifact groupdId of the third-party react-native modules which
// The Maven artifact groupId of the third-party react-native modules which
// Jitsi Meet SDK for Android depends on and which are not available in
// third-party Maven repositories so we have to deploy to a Maven repository
// of ours.

View File

@@ -26,5 +26,5 @@ android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
appVersion=23.3.0
sdkVersion=8.3.0
appVersion=23.4.0
sdkVersion=8.4.0

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -130,6 +130,7 @@ import {
isUserInteractionRequiredForUnmute
} from './react/features/base/tracks/functions';
import { downloadJSON } from './react/features/base/util/downloadJSON';
import { openLeaveReasonDialog } from './react/features/conference/actions.web';
import { showDesktopPicker } from './react/features/desktop-picker/actions';
import { appendSuffix } from './react/features/display-name/functions';
import { maybeOpenFeedbackDialog, submitFeedback } from './react/features/feedback/actions';
@@ -2428,9 +2429,10 @@ export default {
/**
* Disconnect from the conference and optionally request user feedback.
* @param {boolean} [requestFeedback=false] if user feedback should be
* @param {string} [hangupReason] the reason for leaving the meeting
* requested
*/
hangup(requestFeedback = false) {
async hangup(requestFeedback = false, hangupReason) {
APP.store.dispatch(disableReceiver());
this._stopProxyConnection();
@@ -2447,36 +2449,33 @@ export default {
APP.UI.removeAllListeners();
let requestFeedbackPromise;
let feedbackResult = {};
if (requestFeedback) {
requestFeedbackPromise
= APP.store.dispatch(maybeOpenFeedbackDialog(room))
// false because the thank you dialog shouldn't be displayed
.catch(() => Promise.resolve(false));
} else {
requestFeedbackPromise = Promise.resolve(true);
try {
feedbackResult = await APP.store.dispatch(maybeOpenFeedbackDialog(room, hangupReason));
} catch (err) { // eslint-disable-line no-empty
}
}
Promise.all([
requestFeedbackPromise,
this.leaveRoom()
])
.then(values => {
this._room = undefined;
room = undefined;
if (!feedbackResult.wasDialogShown && hangupReason) {
await APP.store.dispatch(openLeaveReasonDialog(hangupReason));
}
/**
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
* and let the page take care of sending the message, since there will be
* a redirect to the page regardlessly.
*/
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
APP.API.notifyReadyToClose();
}
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
});
await this.leaveRoom();
this._room = undefined;
room = undefined;
/**
* Don't call {@code notifyReadyToClose} if the promotional page flag is set
* and let the page take care of sending the message, since there will be
* a redirect to the page anyway.
*/
if (!interfaceConfig.SHOW_PROMOTIONAL_CLOSE_PAGE) {
APP.API.notifyReadyToClose();
}
APP.store.dispatch(maybeRedirectToWelcomePage(feedbackResult));
},
/**

View File

@@ -588,7 +588,7 @@ var config = {
// },
// Configs for the lobby screen.
// lobby {
// lobby: {
// // If Lobby is enabled, it starts knocking automatically. Replaces `autoKnockLobby`.
// autoKnock: false,
// // Enables the lobby chat. Replaces `enableLobbyChat`.
@@ -1015,6 +1015,10 @@ var config = {
// The Amplitude APP Key:
// amplitudeAPPKey: '<APP_KEY>',
// Enables Amplitude UTM tracking:
// Default value is false.
// amplitudeIncludeUTM: false,
// Obfuscates room name sent to analytics (amplitude, rtcstats)
// Default value is false.
// obfuscateRoomName: false,
@@ -1294,6 +1298,16 @@ var config = {
// A list of images that can be used as video backgrounds.
// When this field is present, the default images will be replaced with those provided.
virtualBackgrounds: ['https://example.com/img.jpg'],
// Object containing customized icons that should replace the default ones.
// The keys need to be the exact same icon names used in here:
// https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/icons/svg/index.ts
// To avoid having the icons trimmed or displayed in an unexpected way, please provide svg
// files containing svg xml icons in the size that the default icons come in.
customIcons: {
IconArrowUp: 'https://example.com/arrow-up.svg',
IconDownload: 'https://example.com/download.svg',
IconRemoteControlStart: 'https://example.com/remote-start.svg',
},
// Object containing a theme's properties. It also supports partial overwrites of the main theme.
// For a list of all possible theme tokens and their current defaults, please check:
// https://github.com/jitsi/jitsi-meet/tree/master/resources/custom-theme/custom-theme.json
@@ -1572,6 +1586,8 @@ var config = {
// disableFilmstripAutohiding: false,
// filmstrip: {
// // Disable the vertical/horizonal filmstrip.
// disabled: false,
// // Disables user resizable filmstrip. Also, allows configuration of the filmstrip
// // (width, tiles aspect ratios) through the interfaceConfig options.
// disableResizable: false,

View File

@@ -1,62 +0,0 @@
.drawer-portal {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 351;
border-radius: 16px 16px 0 0;
&.notification-portal {
z-index: 901;
}
}
.drawer-portal::after {
content: '';
background-color: #141414;
margin-bottom: env(safe-area-inset-bottom, 0);
}
.drawer-menu-container {
height: 100vh;
display: flex;
align-items: flex-end;
}
.drawer-menu {
overflow-y: auto;
margin-bottom: env(safe-area-inset-bottom, 0);
width: 100%;
&#{&} .overflow-menu {
margin: auto;
font-size: 1.2em;
list-style-type: none;
padding: 0;
height: calc(80vh - 144px - 64px);
overflow-y: auto;
.overflow-menu-item {
box-sizing: border-box;
height: 48px;
padding: 12px 16px;
align-items: center;
color: #fff;
cursor: pointer;
display: flex;
font-size: 16px;
div {
display: flex;
flex-direction: row;
align-items: center;
}
&.disabled {
cursor: initial;
color: #3b475c;
}
}
}
}

View File

@@ -1,42 +1,3 @@
.participants_pane {
background-color: #141414;
flex-shrink: 0;
overflow: hidden;
position: relative;
transition: width .16s ease-in-out;
width: 315px;
z-index: $zindex0;
}
.participants_pane-content {
display: flex;
flex-direction: column;
font-weight: 600;
height: 100%;
width: 315px;
& > *:first-child,
& > *:last-child {
flex-shrink: 0;
}
}
@media (max-width: 580px) {
.participants_pane {
height: 100vh;
height: -webkit-fill-available;
left: 0;
position: fixed;
right: 0;
top: 0;
width: auto;
}
.participants_pane-content {
width: 100%;
}
}
.jitsi-icon {
&-dominant-speaker {
background-color: #1EC26A;

View File

@@ -73,7 +73,6 @@ $flagsImagePath: "../images/";
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'responsive';
@import 'drawer';
@import 'participants-pane';
@import 'reactions-menu';
@import 'plan-limit';

View File

@@ -3,11 +3,11 @@ PODS:
- amplitude-react-native (2.7.0):
- Amplitude (= 8.7.1)
- React-Core
- AppAuth (1.6.1):
- AppAuth/Core (= 1.6.1)
- AppAuth/ExternalUserAgent (= 1.6.1)
- AppAuth/Core (1.6.1)
- AppAuth/ExternalUserAgent (1.6.1):
- AppAuth (1.6.2):
- AppAuth/Core (= 1.6.2)
- AppAuth/ExternalUserAgent (= 1.6.2)
- AppAuth/Core (1.6.2)
- AppAuth/ExternalUserAgent (1.6.2):
- AppAuth/Core
- boost (1.76.0)
- CocoaLumberjack (3.7.2):
@@ -103,56 +103,59 @@ PODS:
- GoogleUtilities/Network (~> 7.7)
- "GoogleUtilities/NSData+zlib (~> 7.7)"
- nanopb (~> 2.30908.0)
- GoogleDataTransport (9.2.2):
- GoogleDataTransport (9.2.5):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleSignIn (6.2.4):
- GoogleSignIn (7.0.0):
- AppAuth (~> 1.5)
- GTMAppAuth (~> 1.3)
- GTMSessionFetcher/Core (< 3.0, >= 1.1)
- GoogleUtilities/AppDelegateSwizzler (7.11.1):
- GTMAppAuth (< 3.0, >= 1.3)
- GTMSessionFetcher/Core (< 4.0, >= 1.1)
- GoogleUtilities/AppDelegateSwizzler (7.11.5):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Environment (7.11.1):
- GoogleUtilities/Environment (7.11.5):
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.11.1):
- GoogleUtilities/Logger (7.11.5):
- GoogleUtilities/Environment
- GoogleUtilities/MethodSwizzler (7.11.1):
- GoogleUtilities/MethodSwizzler (7.11.5):
- GoogleUtilities/Logger
- GoogleUtilities/Network (7.11.1):
- GoogleUtilities/Network (7.11.5):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (7.11.1)"
- GoogleUtilities/Reachability (7.11.1):
- "GoogleUtilities/NSData+zlib (7.11.5)"
- GoogleUtilities/Reachability (7.11.5):
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (7.11.1):
- GoogleUtilities/UserDefaults (7.11.5):
- GoogleUtilities/Logger
- GTMAppAuth (1.3.1):
- GTMAppAuth (2.0.0):
- AppAuth/Core (~> 1.6)
- GTMSessionFetcher/Core (< 3.0, >= 1.5)
- GTMSessionFetcher/Core (2.3.0)
- GTMSessionFetcher/Core (< 4.0, >= 1.5)
- GTMSessionFetcher/Core (3.1.1)
- JitsiWebRTC (111.0.2)
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
- libwebp/mux (= 1.2.4)
- libwebp/webp (= 1.2.4)
- libwebp/demux (1.2.4):
- libwebp (1.3.1):
- libwebp/demux (= 1.3.1)
- libwebp/mux (= 1.3.1)
- libwebp/sharpyuv (= 1.3.1)
- libwebp/webp (= 1.3.1)
- libwebp/demux (1.3.1):
- libwebp/webp
- libwebp/mux (1.2.4):
- libwebp/mux (1.3.1):
- libwebp/demux
- libwebp/webp (1.2.4)
- libwebp/sharpyuv (1.3.1)
- libwebp/webp (1.3.1):
- libwebp/sharpyuv
- nanopb (2.30908.0):
- nanopb/decode (= 2.30908.0)
- nanopb/encode (= 2.30908.0)
- nanopb/decode (2.30908.0)
- nanopb/encode (2.30908.0)
- ObjectiveDropboxOfficial (6.2.3)
- PromisesObjC (2.2.0)
- PromisesSwift (2.2.0):
- PromisesObjC (= 2.2.0)
- PromisesObjC (2.3.1)
- PromisesSwift (2.3.1):
- PromisesObjC (= 2.3.1)
- RCT-Folly (2021.06.28.00-v2):
- boost
- DoubleConversion
@@ -364,36 +367,34 @@ PODS:
- glog
- react-native-background-timer (2.4.1):
- React-Core
- react-native-get-random-values (1.7.2):
- react-native-get-random-values (1.9.0):
- React-Core
- react-native-keep-awake (4.0.0):
- React
- react-native-netinfo (7.1.7):
- react-native-netinfo (9.4.1):
- React-Core
- react-native-orientation-locker (1.5.0):
- React-Core
- react-native-pager-view (5.4.9):
- react-native-pager-view (6.2.0):
- React-Core
- react-native-safe-area-context (4.6.4):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- react-native-performance (5.0.0):
- React-Core
- ReactCommon/turbomodule/core
- react-native-slider (4.1.12):
- react-native-safe-area-context (4.7.1):
- React-Core
- react-native-slider (4.4.3):
- React-Core
- react-native-splash-screen (3.3.0):
- React-Core
- react-native-video (6.0.0-alpha.1):
- react-native-video (6.0.0-alpha.7):
- React-Core
- react-native-video/Video (= 6.0.0-alpha.1)
- react-native-video/Video (6.0.0-alpha.1):
- react-native-video/Video (= 6.0.0-alpha.7)
- react-native-video/Video (6.0.0-alpha.7):
- PromisesSwift
- React-Core
- react-native-webrtc (111.0.3):
- JitsiWebRTC (~> 111.0.0)
- React-Core
- react-native-webview (11.15.1):
- react-native-webview (13.5.1):
- React-Core
- React-perflogger (0.69.11)
- React-RCTActionSheet (0.69.11):
@@ -463,30 +464,30 @@ PODS:
- React-perflogger (= 0.69.11)
- RNCalendarEvents (2.2.0):
- React
- RNCAsyncStorage (1.17.3):
- RNCAsyncStorage (1.19.3):
- React-Core
- RNCClipboard (1.5.1):
- React-Core
- RNDefaultPreference (1.4.4):
- React-Core
- RNDeviceInfo (8.4.8):
- RNDeviceInfo (10.9.0):
- React-Core
- RNGestureHandler (2.9.0):
- React-Core
- RNGoogleSignin (9.0.2):
- GoogleSignIn (~> 6.2)
- RNGoogleSignin (10.0.1):
- GoogleSignIn (~> 7.0)
- React-Core
- RNScreens (3.22.0):
- RNScreens (3.24.0):
- React-Core
- React-RCTImage
- RNSound (0.11.1):
- RNSound (0.11.2):
- React-Core
- RNSound/Core (= 0.11.1)
- RNSound/Core (0.11.1):
- RNSound/Core (= 0.11.2)
- RNSound/Core (0.11.2):
- React-Core
- RNSVG (12.4.3):
- RNSVG (13.13.0):
- React-Core
- RNWatch (1.0.11):
- RNWatch (1.1.0):
- React
- Yoga (1.14.0)
@@ -525,6 +526,7 @@ DEPENDENCIES:
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`)
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
- react-native-performance (from `../node_modules/react-native-performance`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
@@ -638,6 +640,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-orientation-locker"
react-native-pager-view:
:path: "../node_modules/react-native-pager-view"
react-native-performance:
:path: "../node_modules/react-native-performance"
react-native-safe-area-context:
:path: "../node_modules/react-native-safe-area-context"
react-native-slider:
@@ -702,7 +706,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Amplitude: 834c7332dfb9640a751e21c13efb22a07c0c12d4
amplitude-react-native: 0ed8cab759aafaa94961b82122bf56297da607ad
AppAuth: e48b432bb4ba88b10cb2bcc50d7f3af21e78b9c2
AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
@@ -720,17 +724,17 @@ SPEC CHECKSUMS:
giphy-react-native-sdk: fcda9639f8ca2cc47e0517b6ef11c19359db5f5a
glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
GoogleDataTransport: 8378d1fa8ac49753ea6ce70d65a7cb70ce5f66e6
GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2
GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2
GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842
GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084
GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae
GTMSessionFetcher: e8647203b65cee28c5f73d0f473d096653945e72
JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
libwebp: 33dc822fbbf4503668d09f7885bbfedc76c45e96
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
RCTRequired: 8e9a57dddc8f8e9e816c67c2d2537271a997137a
RCTTypeSafety: 2b19e268e2036a2c2f6db6deb1ac03e28b1d607a
@@ -746,17 +750,18 @@ SPEC CHECKSUMS:
React-jsinspector: 9ca5bf73ed0a195397e45fdbcd507cf7d503c428
React-logger: 700340e325f21ba2a2d6413a61ef14268c7360aa
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-get-random-values: 30b3f74ca34e30e2e480de48e4add2706a40ac8f
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-keep-awake: afad8a51dfef9fe9655a6344771be32c8596d774
react-native-netinfo: 27f287f2d191693f3b9d01a4273137fcf91c3b5d
react-native-netinfo: fefd4e98d75cbdd6e85fc530f7111a8afdf2b0c5
react-native-orientation-locker: 851f6510d8046ea2f14aa169b1e01fcd309a94ba
react-native-pager-view: 3ee7d4c7697fb3ef788346e834a60cca97ed8540
react-native-safe-area-context: 68b07eabfb0d14547d36f6929c0e98d818064f02
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df
react-native-performance: 47ac22ebf2aa24f324a96a5825581f6ce18c09e8
react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
react-native-video: 967eead48aaa42c25a9e1d65c3b1ab30762a88df
react-native-webrtc: 4d1669c2ed29767fe70b0169428b4466589ecf8b
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
React-perflogger: fdee2a0c512167ae4c19c4e230ccf6aa66a6aff0
React-RCTActionSheet: 1cf5fef4e372f1c877969710a51bea4bb25e78fe
React-RCTAnimation: 73816e3acd1f5e3f00166fc7eedb34f6b112f734
@@ -770,16 +775,16 @@ SPEC CHECKSUMS:
React-runtimeexecutor: 59407514818b2afbb1d7507e4e1ac834d24b0fbd
ReactCommon: b8487da74723562d7368dab27135fd182f00a91c
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
RNCAsyncStorage: 005c0e2f09575360f142d0d1f1f15e4ec575b1af
RNCAsyncStorage: c913ede1fa163a71cea118ed4670bbaaa4b511bb
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
RNDeviceInfo: 0400a6d0c94186d1120c3cbd97b23abc022187a9
RNDeviceInfo: 02ea8b23e2280fa18e00a06d7e62804d74028579
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNGoogleSignin: 22e468a9474dbcb8618d8847205ad4f0b2575d13
RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f
RNSVG: f3b60aeeaa81960e2e0536c3a9eef50b667ef3a9
RNWatch: dae6c858a2051dbdcfb00b9a86cf4d90400263b4
RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0
RNScreens: b21dc57dfa2b710c30ec600786a3fc223b1b92e7
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82
RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236
Yoga: 7f5ad94937ba3fc58c151ad1b7bbada2c275b28e
PODFILE CHECKSUM: e3579df5272b8b697c9fdc0e55aa0845b189c4dd

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -250,13 +250,14 @@
"dialog": {
"Back": "Voltar",
"Cancel": "Cancelar",
"IamHost": "Eu sou o anfitrião",
"IamHost": "Iniciar sessão",
"Ok": "OK",
"Remove": "Remover",
"Share": "Partilhar",
"Submit": "Submeter",
"WaitForHostMsg": "A conferência ainda não começou. Se for o anfitrião, por favor autentique. Caso contrário, por favor aguarde que o anfitrião chegue.",
"WaitingForHostTitle": "À espera do anfitrião ...",
"WaitForHostMsg": "A conferência ainda não começou porque ainda não chegaram moderadores. Se quiser ser um moderador, inicie a sessão. Caso contrário, aguarde.",
"WaitingForHostButton": "Esperar pelo moderador",
"WaitingForHostTitle": "À espera de um moderador...",
"Yes": "Sim",
"accessibilityLabel": {
"Cancel": "Cancelar (sair da caixa de diálogo)",
@@ -269,6 +270,8 @@
"addMeetingNote": "Acrescentar uma nota sobre esta reunião",
"addOptionalNote": "Adicionar uma nota (opcional):",
"allow": "Permitir",
"allowToggleCameraDialog": "Permite que {{initiatorName}} alterne o modo de visualização da câmara?",
"allowToggleCameraTitle": "Permitir alternar a câmara?",
"alreadySharedVideoMsg": "Outro participante já está a partilhar um vídeo. Esta conferência permite apenas um vídeo partilhado de cada vez.",
"alreadySharedVideoTitle": "Só é permitido um vídeo partilhado de cada vez",
"applicationWindow": "Janela de aplicação",
@@ -329,6 +332,7 @@
"lockRoom": "Adicionar reunião $t(lockRoomPassword)",
"lockTitle": "Bloqueio falhado",
"login": "Entrar",
"loginQuestion": "Tem a certeza de que pretende iniciar sessão e abandonar a conferência?",
"logoutQuestion": "Tem a certeza de que quer terminar a sessão e sair da conferência?",
"logoutTitle": "Sair",
"maxUsersLimitReached": "O limite para o número máximo de participantes foi atingido. A conferência está cheia. Por favor contacte o proprietário da reunião ou tente novamente mais tarde!",
@@ -409,6 +413,7 @@
"sendPrivateMessageTitle": "Enviar em privado?",
"serviceUnavailable": "Serviço indisponível",
"sessTerminated": "Chamada terminada",
"sessTerminatedReason": "A reunião foi encerrada",
"sessionRestarted": "Chamada reiniciada devido a um problema de ligação.",
"shareAudio": "Continuar",
"shareAudioTitle": "Como partilhar áudio",
@@ -440,7 +445,24 @@
"thankYou": "Obrigado por utilizar {{appName}}!",
"token": "token",
"tokenAuthFailed": "Desculpe, não está autorizado a juntar-se a esta chamada.",
"tokenAuthFailedReason": {
"audInvalid": "Valor `aud` inválido. Deveria ser `jitsi`.",
"contextNotFound": "O objeto `context` está em falta na carga útil.",
"expInvalid": "Valor `exp` inválido.",
"featureInvalid": "Funcionalidade inválida: {{feature}}, muito provavelmente ainda não implementada.",
"featureValueInvalid": "Valor inválido para a caraterística: {{feature}}.",
"featuresNotFound": "O objeto `features` está em falta na carga útil.",
"headerNotFound": "Falta o cabeçalho.",
"issInvalid": "Valor `iss` inválido. Deveria ser `chat`.",
"kidMismatch": "O ID da chave (kid) não corresponde ao sub.",
"kidNotFound": "Falta o ID da chave (kid)",
"nbfFuture": "O valor `nbf` está no futuro.",
"nbfInvalid": "Valor `nbf` inválido.",
"payloadNotFound": "Falta a carga útil.",
"tokenExpired": "O token expirou."
},
"tokenAuthFailedTitle": "A autenticação falhou",
"tokenAuthFailedWithReasons": "Lamentamos, mas não está autorizado a participar nesta chamada. Razões possíveis: {{reason}}",
"tokenAuthUnsupported": "O URL de token não é suportado.",
"transcribing": "Transcrição",
"unlockRoom": "Retirar reunião $t(lockRoomPassword)",
@@ -529,7 +551,8 @@
"numbers": "Números para entrar por chamada telefónica",
"password": "$t(lockRoomPasswordUppercase): ",
"reachedLimit": "atingiu o limite do seu plano.",
"sip": "Endereços SIP",
"sip": "Endereço SIP",
"sipAudioOnly": "Endereço SIP só de áudio",
"title": "Partilhar",
"tooltip": "Partilhar link e acesso telefónico para esta reunião",
"upgradeOptions": "Por favor, verifique as opções de atualização em"
@@ -593,15 +616,15 @@
"limitNotificationDescriptionWeb": "Devido à grande procura, a sua transmissão será limitada a {{limit}} min. Para uma tentativa de streaming ilimitada tente <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"off": "Transmissão em direto encerrada",
"offBy": "{{name}} parou a transmissão em direto",
"on": "Transmissão em Direto",
"on": "Iniciada a transmissão em direto",
"onBy": "{{name}} iniciou a transmissão em direto",
"pending": "Iniciando Transmissão em Direto...",
"pending": "Início da transmissão em direto...",
"serviceName": "Serviço de Transmissão em Direto",
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
"signIn": "Faça login no Google",
"signInCTA": "Faça login ou insira sua chave de transmissão em Direto do YouTube.",
"signIn": "Iniciar sessão com o Google",
"signInCTA": "Inicie sessão ou introduza a sua chave de transmissão em direto do YouTube.",
"signOut": "Sair",
"signedInAs": "Está conectado como:",
"signedInAs": "Atualmente, tem sessão iniciada como:",
"start": "Iniciar uma transmissão em direto",
"streamIdHelp": "O que é isso?",
"title": "Transmissão em direto",
@@ -636,13 +659,13 @@
"knockingParticipantList": "Lista de participantes a expulsar",
"lobbyChatStartedNotification": "{{moderator}} iniciou com {{attendee}} uma conversa na sala de espera",
"lobbyChatStartedTitle": "{{moderator}} iniciou consigo uma conversa na sala de espera.",
"lobbyClosed": "A sala de espera foi encerrada.",
"nameField": "Introduza o seu nome",
"notificationLobbyAccessDenied": "{{targetParticipantName}} foi recusada a adesão por {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} foi autorizado a aderir por {{originParticipantName}}",
"notificationLobbyDisabled": "A sala de espera foi desactivada por {{originParticipantName}}",
"notificationLobbyEnabled": "A sala de espera foi activada por {{originParticipantName}}",
"notificationTitle": "Sala de espera",
"passwordField": "Introduza a senha da reunião",
"passwordJoinButton": "Solicitar",
"reject": "Rejeitar",
"rejectAll": "Rejeitar todos",
@@ -1061,6 +1084,7 @@
"alertOk": "OK",
"alertTitle": "Atenção",
"alertURLText": "A URL digitada do servidor é inválida",
"apply": "Aplicar",
"buildInfoSection": "Informações de compilação",
"conferenceSection": "Conferência",
"disableCallIntegration": "Desactivar a integração de chamadas nativas",
@@ -1071,6 +1095,7 @@
"displayNamePlaceholderText": "Ex: João Dias",
"email": "Email",
"emailPlaceholderText": "email@example.com",
"gavatarMessage": "Se o seu e-mail estiver associado a uma conta Gravatar, utilizá-la-emos para apresentar a sua imagem de perfil.",
"goTo": "Ir para",
"header": "Configurações",
"help": "Ajuda",
@@ -1254,7 +1279,7 @@
"lobbyButtonDisable": "Desativar sala de espera",
"lobbyButtonEnable": "Ativar sala de espera",
"login": "Iniciar sessão",
"logout": "Encerrar sessão",
"logout": "Terminar sessão",
"lowerYourHand": "Baixar a mão",
"moreActions": "Mais ações",
"moreOptions": "Mais opções",

View File

@@ -256,6 +256,7 @@
"Share": "Share",
"Submit": "Submit",
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
"WaitingForHostButton": "Wait for moderator",
"WaitingForHostTitle": "Waiting for a moderator...",
"Yes": "Yes",
"accessibilityLabel": {
@@ -412,6 +413,7 @@
"sendPrivateMessageTitle": "Send privately?",
"serviceUnavailable": "Service unavailable",
"sessTerminated": "Call terminated",
"sessTerminatedReason": "The meeting has been terminated",
"sessionRestarted": "Call restarted because of a connection issue.",
"shareAudio": "Continue",
"shareAudioTitle": "How to share audio",
@@ -657,13 +659,13 @@
"knockingParticipantList": "Knocking participant list",
"lobbyChatStartedNotification": "{{moderator}} started a lobby chat with {{attendee}}",
"lobbyChatStartedTitle": "{{moderator}} has started a lobby chat with you.",
"lobbyClosed": "The lobby room has been closed.",
"nameField": "Enter your name",
"notificationLobbyAccessDenied": "{{targetParticipantName}} has been rejected to join by {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} has been allowed to join by {{originParticipantName}}",
"notificationLobbyDisabled": "Lobby has been disabled by {{originParticipantName}}",
"notificationLobbyEnabled": "Lobby has been enabled by {{originParticipantName}}",
"notificationTitle": "Lobby",
"passwordField": "Enter meeting password",
"passwordJoinButton": "Join",
"reject": "Reject",
"rejectAll": "Reject all",

View File

@@ -1141,7 +1141,11 @@ class API {
*/
_sendEvent(event = {}) {
if (this._enabled) {
transport.sendEvent(event);
try {
transport.sendEvent(event);
} catch (error) {
logger.error('Failed to send and IFrame API event', error);
}
}
}
@@ -1475,11 +1479,43 @@ class API {
* @param {Array<string>} args - Array of strings composing the log message.
* @returns {void}
*/
notifyLog(logLevel, args) {
notifyLog(logLevel, args = []) {
if (!Array.isArray(args)) {
logger.error('notifyLog received wrong argument types!');
return;
}
// Trying to convert arguments to strings. Otherwise in order to send the event the arguments will be formatted
// with JSON.stringify which can throw an error because of circular objects and we will lose the whole log.
const formattedArguments = [];
args.forEach(arg => {
let formattedArgument = '';
if (arg instanceof Error) {
formattedArgument += `${arg.toString()}: ${arg.stack}`;
} else if (typeof arg === 'object') {
// NOTE: The non-enumerable properties of the objects wouldn't be included in the string after
// JSON.strigify. For example Map instance will be translated to '{}'. So I think we have to eventually
// do something better for parsing the arguments. But since this option for strigify is part of the
// public interface and I think it could be useful in some cases I will it for now.
try {
formattedArgument += JSON.stringify(arg);
} catch (error) {
formattedArgument += arg;
}
} else {
formattedArgument += arg;
}
formattedArguments.push(formattedArgument);
});
this._sendEvent({
name: 'log',
logLevel,
args
args: formattedArguments
});
}

View File

@@ -305,6 +305,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* configuration options defined in config.js to be overridden.
* @param {Object} [options.interfaceConfigOverwrite] - Object containing
* configuration options defined in interface_config.js to be overridden.
* @param {IIceServers} [options.iceServers] - Object with rules that will be used to modify/remove the existing
* ice server configuration.
* NOTE: This property is currently experimental and may be removed in the future!
* @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
* authentication.
* @param {string} [options.lang] - The meeting's default language.
@@ -334,6 +337,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
lang = undefined,
onload = undefined,
invitees,
iceServers,
devices,
userInfo,
e2eeKey,
@@ -345,6 +349,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this._parentNode = parentNode;
this._url = generateURL(domain, {
configOverwrite,
iceServers,
interfaceConfigOverwrite,
jwt,
lang,
@@ -1191,6 +1196,24 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return this._numberOfParticipants;
}
/**
* Returns array of commands supported by executeCommand().
*
* @returns {Array<string>} Array of commands.
*/
getSupportedCommands() {
return Object.keys(commands);
}
/**
* Returns array of events supported by addEventListener().
*
* @returns {Array<string>} Array of events.
*/
getSupportedEvents() {
return Object.values(events);
}
/**
* Check if the video is available.
*

View File

@@ -1,6 +1,7 @@
/* global APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import Logger from '@jitsi/logger';
import $ from 'jquery';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -23,6 +24,8 @@ export const VIDEO_CONTAINER_TYPE = 'camera';
// Corresponds to animation duration from the animatedFadeIn and animatedFadeOut CSS classes.
const FADE_DURATION_MS = 300;
const logger = Logger.getLogger(__filename);
/**
* Returns an array of the video dimensions, so that it keeps it's aspect
* ratio and fits available area with it's larger dimension. This method
@@ -489,7 +492,9 @@ export class VideoContainer extends LargeContainer {
}
if (this.video) {
stream.attach(this.video);
stream.attach(this.video).catch(error => {
logger.error(`Attaching the remote track ${stream} has failed with `, error);
});
// Ensure large video gets play() called on it when a new stream is attached to it. This is necessary in the
// case of Safari as autoplay doesn't kick-in automatically on Safari 15 and newer versions.

View File

@@ -23,11 +23,8 @@ const VideoLayout = {
/**
* Handler for local flip X changed event.
*/
onLocalFlipXChanged() {
onLocalFlipXChanged(localFlipX) {
if (largeVideo) {
const { store } = APP;
const { localFlipX } = store.getState()['features/base/settings'];
largeVideo.onLocalFlipXChange(localFlipX);
}
},

2226
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,19 +23,19 @@
"@giphy/react-components": "6.8.1",
"@giphy/react-native-sdk": "2.3.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"@jitsi/js-utils": "2.1.2",
"@jitsi/logger": "2.0.0",
"@jitsi/js-utils": "2.1.3",
"@jitsi/logger": "2.0.2",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.5.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
"@mui/material": "5.12.1",
"@mui/styles": "5.12.0",
"@react-native-async-storage/async-storage": "1.17.3",
"@react-native-async-storage/async-storage": "1.19.3",
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/netinfo": "7.1.7",
"@react-native-community/slider": "4.1.12",
"@react-native-google-signin/google-signin": "9.0.2",
"@react-native-community/netinfo": "9.4.1",
"@react-native-community/slider": "4.4.3",
"@react-native-google-signin/google-signin": "10.0.1",
"@react-navigation/bottom-tabs": "6.5.8",
"@react-navigation/elements": "1.3.18",
"@react-navigation/material-top-tabs": "6.6.3",
@@ -65,7 +65,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/v1678.0.0+77e6803f/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1687.0.0+cafe30d7/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -82,31 +82,31 @@
"react-native": "0.69.11",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.7",
"react-native-callstats": "3.73.22",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-device-info": "10.9.0",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.9.0",
"react-native-get-random-values": "1.7.2",
"react-native-get-random-values": "1.9.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "1.5.0",
"react-native-pager-view": "5.4.9",
"react-native-paper": "5.1.2",
"react-native-performance": "2.1.0",
"react-native-safe-area-context": "4.6.4",
"react-native-screens": "3.22.0",
"react-native-sound": "0.11.1",
"react-native-pager-view": "6.2.0",
"react-native-paper": "5.10.3",
"react-native-performance": "5.0.0",
"react-native-safe-area-context": "4.7.1",
"react-native-screens": "3.24.0",
"react-native-sound": "0.11.2",
"react-native-splash-screen": "3.3.0",
"react-native-svg": "12.4.3",
"react-native-svg-transformer": "1.0.0",
"react-native-tab-view": "3.1.1",
"react-native-url-polyfill": "1.3.0",
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
"react-native-watch-connectivity": "1.0.11",
"react-native-svg": "13.13.0",
"react-native-svg-transformer": "1.1.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
"react-native-video": "6.0.0-alpha.7",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "111.0.3",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-native-webview": "13.5.1",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
"react-window": "1.8.6",
@@ -115,6 +115,7 @@
"redux-thunk": "2.4.1",
"resemblejs": "4.0.0",
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"tss-react": "4.4.4",
"util": "0.12.1",
"uuid": "8.3.2",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@jitsi/react-native-sdk",
"version": "0.0.0",
"version": "0.4.0",
"description": "React Native SDK for Jitsi Meet.",
"main": "index.tsx",
"license": "Apache-2.0",
@@ -11,9 +11,8 @@
"url": "git+https://github.com/jitsi/jitsi-meet.git"
},
"dependencies": {
"@hapi/bourne": "2.0.0",
"@jitsi/js-utils": "2.0.5",
"@jitsi/logger": "2.0.0",
"@jitsi/js-utils": "2.1.3",
"@jitsi/logger": "2.0.2",
"@jitsi/rtcstats": "9.5.1",
"@react-navigation/bottom-tabs": "6.5.8",
"@react-navigation/elements": "1.3.18",
@@ -25,11 +24,10 @@
"grapheme-splitter": "1.0.4",
"i18n-iso-countries": "6.8.0",
"i18next": "17.0.6",
"i18next-xhr-backend": "3.0.0",
"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/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1687.0.0+cafe30d7/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -39,12 +37,12 @@
"react-emoji-render": "1.2.4",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native-callstats": "3.73.7",
"react-native-callstats": "3.73.22",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-svg-transformer": "1.0.0",
"react-native-tab-view": "3.1.1",
"react-native-url-polyfill": "1.3.0",
"react-native-youtube-iframe": "2.2.1",
"react-native-svg-transformer": "1.1.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"redux": "4.0.4",
"redux-thunk": "2.4.1",
@@ -56,34 +54,34 @@
"peerDependencies": {
"@amplitude/react-native": "2.7.0",
"@giphy/react-native-sdk": "2.3.0",
"@react-native-async-storage/async-storage": "1.18.2",
"@react-native-async-storage/async-storage": "1.19.3",
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/netinfo": "7.1.7",
"@react-native-community/slider": "4.1.12",
"@react-native-google-signin/google-signin": "7.0.4",
"@react-native-community/netinfo": "9.4.1",
"@react-native-community/slider": "4.4.3",
"@react-native-google-signin/google-signin": "10.0.1",
"react-native": "*",
"react": "*",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-get-random-values": "1.7.2",
"react-native-device-info": "10.9.0",
"react-native-get-random-values": "1.9.0",
"react-native-gesture-handler": "2.9.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-pager-view": "5.4.9",
"react-native-paper": "4.11.1",
"react-native-performance": "2.1.0",
"react-native-orientation-locker": "https://git@github.com/wonday/react-native-orientation-locker#f483520ea6b64b97002374a9e9f053a5299a062a",
"react-native-safe-area-context": "4.4.1",
"react-native-screens": "3.22.0",
"react-native-sound": "0.11.1",
"react-native-pager-view": "6.2.0",
"react-native-paper": "5.10.3",
"react-native-performance": "5.0.0",
"react-native-orientation-locker": "1.5.0",
"react-native-safe-area-context": "4.7.1",
"react-native-screens": "3.24.0",
"react-native-sound": "0.11.2",
"react-native-splash-screen": "3.3.0",
"react-native-svg": "12.4.3",
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
"react-native-watch-connectivity": "1.0.11",
"react-native-webrtc": "111.0.1",
"react-native-webview": "11.15.1"
"react-native-svg": "13.13.0",
"react-native-video": "6.0.0-alpha.7",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "111.0.3",
"react-native-webview": "13.5.1"
},
"overrides": {
"@xmldom/xmldom": "0.8.7"

View File

@@ -58,11 +58,25 @@ function copyFolderRecursiveSync(source, target) {
* Merges the dependency versions from the root package.json with the dependencies of the SDK package.json.
*/
function mergeDependencyVersions() {
// Updates SDK dependencies to match project dependencies.
for (const key in SDKPackageJSON.dependencies) {
if (SDKPackageJSON.dependencies.hasOwnProperty(key)) {
SDKPackageJSON.dependencies[key] = packageJSON.dependencies[key] || packageJSON.devDependencies[key];
}
}
// Updates SDK peer dependencies.
for (const key in packageJSON.dependencies) {
if (SDKPackageJSON.peerDependencies.hasOwnProperty(key)) {
// Updates all peer dependencies except react and react-native.
if (key !== 'react' && key !== 'react-native') {
SDKPackageJSON.peerDependencies[key] = packageJSON.dependencies[key];
}
}
}
const data = JSON.stringify(SDKPackageJSON, null, 4);
fs.writeFileSync('package.json', data);

View File

@@ -1,8 +1,10 @@
/* eslint-disable guard-for-in */
/* eslint-disable guard-for-in, no-continue */
/* global __dirname */
const fs = require('fs');
const path = require('path');
const semver = require('semver');
const pathToPackageJSON = path.resolve(__dirname, '../../../package.json');
@@ -10,6 +12,7 @@ const packageJSON = require(pathToPackageJSON);
const RNSDKpackageJSON = require(path.resolve(__dirname, './package.json'));
/**
* Updates dependencies from the app package.json with the peer dependencies of the RNSDK package.json.
*/
@@ -21,6 +24,17 @@ function updateDependencies() {
packageJSON.dependencies[key] = RNSDKpackageJSON.peerDependencies[key];
updated = true;
}
if (semver.satisfies(RNSDKpackageJSON.peerDependencies[key], `=${packageJSON.dependencies[key]}`)) {
continue;
}
if (semver.satisfies(RNSDKpackageJSON.peerDependencies[key], `>${packageJSON.dependencies[key]}`)) {
packageJSON.dependencies[key] = RNSDKpackageJSON.peerDependencies[key];
updated = true;
console.log(`${key} is now set to ${RNSDKpackageJSON.peerDependencies[key]}`);
}
}
packageJSON.overrides = packageJSON.overrides || {};
@@ -33,6 +47,8 @@ function updateDependencies() {
}
if (!updated) {
console.log('All your dependencies are up to date!');
return;
}

View File

@@ -82,6 +82,7 @@ export async function createHandlers({ getState }: IStore) {
} = config;
const {
amplitudeAPPKey,
amplitudeIncludeUTM,
blackListedEvents,
scriptURLs,
googleAnalyticsTrackingId,
@@ -92,6 +93,7 @@ export async function createHandlers({ getState }: IStore) {
const { group, user } = state['features/base/jwt'];
const handlerConstructorOptions = {
amplitudeAPPKey,
amplitudeIncludeUTM,
blackListedEvents,
envType: deploymentInfo?.envType || 'dev',
googleAnalyticsTrackingId,

View File

@@ -11,6 +11,7 @@ export interface IEvent {
interface IOptions {
amplitudeAPPKey?: string;
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
envType?: string;
googleAnalyticsTrackingId?: string;

View File

@@ -15,13 +15,18 @@ export default class AmplitudeHandler extends AbstractHandler {
* Creates new instance of the Amplitude analytics handler.
*
* @param {Object} options -
* @param {string} options.amplitudeAPPKey - The Amplitude app key required
* by the Amplitude API.
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API.
* @param {boolean} options.amplitudeIncludeUTM - Whether to include UTM parameters
* in the Amplitude events.
*/
constructor(options: any) {
super(options);
const { amplitudeAPPKey, user } = options;
const {
amplitudeAPPKey,
amplitudeIncludeUTM: includeUtm = true,
user
} = options;
this._enabled = true;
@@ -43,6 +48,8 @@ export default class AmplitudeHandler extends AbstractHandler {
} else {
const amplitudeOptions: any = {
includeReferrer: true,
includeUtm,
saveParamsReferrerOncePerSession: false,
onError
};

View File

@@ -1,6 +1,9 @@
import { Amplitude } from '@amplitude/react-native';
import DefaultPreference from 'react-native-default-preference';
import DeviceInfo from 'react-native-device-info';
import { getUniqueId } from 'react-native-device-info';
import logger from '../../logger';
/**
* Custom logic for setting the correct device id.
@@ -14,11 +17,17 @@ export async function fixDeviceID(amplitude: Amplitude) {
const current = await DefaultPreference.get('amplitudeDeviceId');
if (current) {
amplitude.setDeviceId(current);
await amplitude.setDeviceId(current);
} else {
const uid = DeviceInfo.getUniqueId();
const uid = await getUniqueId();
amplitude.setDeviceId(uid);
DefaultPreference.set('amplitudeDeviceId', uid);
if (!uid) {
logger.warn('Device ID is not set!');
return;
}
await amplitude.setDeviceId(uid as string);
await DefaultPreference.set('amplitudeDeviceId', uid as string);
}
}

View File

@@ -27,6 +27,7 @@ import '../connection-indicator/middleware';
import '../deep-linking/middleware';
import '../device-selection/middleware';
import '../display-name/middleware';
import '../dynamic-branding/middleware';
import '../etherpad/middleware';
import '../filmstrip/middleware';
import '../follow-me/middleware';

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { IStore } from '../../../app/types';
import { IReduxState, IStore } from '../../../app/types';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import { cancelWaitForOwner, login } from '../../actions.native';
@@ -12,6 +12,11 @@ import { cancelWaitForOwner, login } from '../../actions.native';
*/
interface IProps {
/**
* Whether to show alternative cancel button text.
*/
_alternativeCancelText?: boolean;
/**
* Redux store dispatch function.
*/
@@ -53,7 +58,7 @@ class WaitForOwnerDialog extends Component<IProps> {
render() {
return (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
cancelLabel = { this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }
confirmLabel = 'dialog.IamHost'
descriptionKey = 'dialog.WaitForHostMsg'
onCancel = { this._onCancel }
@@ -79,9 +84,23 @@ class WaitForOwnerDialog extends Component<IProps> {
*/
_onLogin() {
this.props.dispatch(login());
return true;
}
}
export default translate(connect()(WaitForOwnerDialog));
/**
* 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 { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
return {
_alternativeCancelText: membersOnly && lobbyWaitingForHost
};
}
export default translate(connect(mapStateToProps)(WaitForOwnerDialog));

View File

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IStore } from '../../../app/types';
import { IReduxState, IStore } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import Dialog from '../../../base/ui/components/web/Dialog';
import { cancelWaitForOwner, login } from '../../actions.web';
@@ -12,6 +12,11 @@ import { cancelWaitForOwner, login } from '../../actions.web';
*/
interface IProps extends WithTranslation {
/**
* Whether to show alternative cancel button text.
*/
_alternativeCancelText?: boolean;
/**
* Redux store dispatch method.
*/
@@ -71,6 +76,8 @@ class WaitForOwnerDialog extends PureComponent<IProps> {
return (
<Dialog
cancel = {{ translationKey:
this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }}
disableBackdropClose = { true }
hideCloseButton = { true }
ok = {{ translationKey: 'dialog.IamHost' }}
@@ -85,4 +92,20 @@ class WaitForOwnerDialog extends PureComponent<IProps> {
}
}
export default translate(connect()(WaitForOwnerDialog));
/**
* 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 { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
return {
_alternativeCancelText: membersOnly && lobbyWaitingForHost
};
}
export default translate(connect(mapStateToProps)(WaitForOwnerDialog));

View File

@@ -2,9 +2,11 @@ import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEve
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';
import { getReplaceParticipant } from '../config/functions';
import { hangup } from '../connection/actions';
import { connect, disconnect, hangup } from '../connection/actions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../media/actions';
@@ -73,6 +75,7 @@ import {
getConferenceOptions,
getConferenceState,
getCurrentConference,
getVisitorOptions,
sendLocalParticipant
} from './functions';
import logger from './logger';
@@ -983,3 +986,41 @@ export function setAssumedBandwidthBps(assumedBandwidthBps: number) {
assumedBandwidthBps
};
}
/**
* Redirects to a new visitor node.
*
* @param {string | undefined} vnode - The vnode to use or undefined if moving back to the main room.
* @param {string} focusJid - The focus jid to use.
* @param {string} username - The username to use.
* @returns {void}
*/
export function redirect(vnode: string, focusJid: string, username: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { conference, joining } = getState()['features/base/conference'];
const newConfig = getVisitorOptions(getState, vnode, focusJid, username);
if (!newConfig) {
logger.warn('Not redirected missing params');
return;
}
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(() => dispatch(conferenceWillLeave(conference || joining)))
.then(() => dispatch(disconnect()))
.then(() => dispatch(setIAmVisitor(Boolean(vnode))))
// we do not clear local tracks on error, so we need to manually clear them
.then(() => dispatch(destroyLocalTracks()))
.then(() => dispatch(conferenceWillInit()))
.then(() => dispatch(connect()))
.then(() => {
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
});
};
}

View File

@@ -28,9 +28,10 @@ export const EMAIL_COMMAND = 'email';
*/
export const JITSI_CONFERENCE_URL_KEY = Symbol('url');
export const TRIGGER_READY_TO_CLOSE_REASONS = [
'The meeting has been terminated'
];
export const TRIGGER_READY_TO_CLOSE_REASONS = {
'dialog.sessTerminatedReason': 'The meeting has been terminated',
'lobby.lobbyClosed': 'Lobby room closed.'
};
/**
* Conference leave reasons.
@@ -39,8 +40,3 @@ export const CONFERENCE_LEAVE_REASONS = {
SWITCH_ROOM: 'switch_room',
UNRECOVERABLE_ERROR: 'unrecoverable_error'
};
/**
* Timeout for properly leaving the conference if it was destroyed.
*/
export const CONFERENCE_DESTROYED_LEAVE_TIMEOUT = 10000;

View File

@@ -284,12 +284,12 @@ export function restoreConferenceOptions(stateful: IStateful) {
* Override the global config (that is, window.config) with XMPP configuration required to join as a visitor.
*
* @param {IStateful} stateful - The redux store state.
* @param {Array<string>} params - The received parameters.
* @param {string|undefined} vnode - The received parameters.
* @param {string} focusJid - The received parameters.
* @param {string|undefined} username - The received parameters.
* @returns {Object}
*/
export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
const [ vnode, focusJid, username ] = params;
export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid: string, username: string) {
const config = toState(stateful)['features/base/config'];
if (!config?.hosts) {

View File

@@ -14,12 +14,10 @@ import { reloadNow } from '../../app/actions';
import { IStore } from '../../app/types';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { readyToClose } from '../../mobile/external-api/actions';
import { showErrorNotification, showWarningNotification } from '../../notifications/actions';
import { showErrorNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { stopLocalVideoRecording } from '../../recording/actions.any';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
import { setIAmVisitor } from '../../visitors/actions';
import { iAmVisitor } from '../../visitors/functions';
import { overwriteConfig } from '../config/actions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
@@ -35,7 +33,6 @@ import {
} from '../participants/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
import { destroyLocalTracks } from '../tracks/actions.any';
import { getLocalTracks } from '../tracks/functions.any';
import {
@@ -52,24 +49,17 @@ import {
import {
authStatusChanged,
conferenceFailed,
conferenceWillInit,
conferenceWillLeave,
createConference,
leaveConference,
setLocalSubject,
setSubject
} from './actions';
import {
CONFERENCE_DESTROYED_LEAVE_TIMEOUT,
CONFERENCE_LEAVE_REASONS,
TRIGGER_READY_TO_CLOSE_REASONS
} from './constants';
import { CONFERENCE_LEAVE_REASONS } from './constants';
import {
_addLocalTracksToConference,
_removeLocalTracksFromConference,
forEachConference,
getCurrentConference,
getVisitorOptions,
restoreConferenceOptions
} from './functions';
import logger from './logger';
@@ -153,25 +143,6 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
// Handle specific failure reasons.
switch (error.name) {
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
const [ reason ] = error.params;
dispatch(showWarningNotification({
description: reason,
titleKey: 'dialog.sessTerminated'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
if (TRIGGER_READY_TO_CLOSE_REASONS.includes(reason)) {
if (typeof APP === 'undefined') {
dispatch(readyToClose());
} else {
APP.API.notifyReadyToClose();
}
setTimeout(() => dispatch(leaveConference()), CONFERENCE_DESTROYED_LEAVE_TIMEOUT);
}
break;
}
case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
if (enableForcedReload) {
dispatch(showErrorNotification({
@@ -227,34 +198,6 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
sendAnalytics(createOfferAnswerFailedEvent());
break;
case JitsiConferenceErrors.REDIRECTED: {
const newConfig = getVisitorOptions(getState, error.params);
if (!newConfig) {
logger.warn('Not redirected missing params');
break;
}
const [ vnode ] = error.params;
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(() => dispatch(conferenceWillLeave(conference)))
.then(() => dispatch(disconnect()))
.then(() => dispatch(setIAmVisitor(Boolean(vnode))))
// we do not clear local tracks on error, so we need to manually clear them
.then(() => dispatch(destroyLocalTracks()))
.then(() => dispatch(conferenceWillInit()))
.then(() => dispatch(connect()))
.then(() => {
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
});
break;
}
}
!error.recoverable

View File

@@ -1 +1,36 @@
import { appNavigate } from '../../app/actions.native';
import { notifyConferenceFailed } from '../../conference/actions.native';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { CONFERENCE_FAILED } from './actionTypes';
import { conferenceLeft } from './actions';
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
const { dispatch } = store;
const { error } = action;
switch (action.type) {
case CONFERENCE_FAILED: {
if (error?.name !== JitsiConferenceErrors.CONFERENCE_DESTROYED) {
break;
}
const [ reason ] = error.params;
const reasonKey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
];
dispatch(notifyConferenceFailed(reasonKey, () => {
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));
}));
}
}
return next(action);
});

View File

@@ -1,7 +1,10 @@
import i18next from 'i18next';
import {
setPrejoinPageVisibility,
setSkipPrejoinOnReload
} from '../../prejoin/actions.web';
import { hangup } from '../connection/actions.web';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
@@ -12,7 +15,9 @@ import {
CONFERENCE_LEFT,
KICKED_OUT
} from './actionTypes';
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
import logger from './logger';
import './middleware.any';
let screenLock: WakeLockSentinel | undefined;
@@ -108,6 +113,15 @@ MiddlewareRegistry.register(store => next => action => {
dispatch(setSkipPrejoinOnReload(true));
}
if (errorName === JitsiConferenceErrors.CONFERENCE_DESTROYED) {
const [ reason ] = action.error.params;
const titlekey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
];
dispatch(hangup(true, i18next.t(titlekey) || reason));
}
releaseScreenLock();
break;

View File

@@ -137,6 +137,7 @@ export interface IConferenceState {
followMeEnabled?: boolean;
joining?: IJitsiConference;
leaving?: IJitsiConference;
lobbyWaitingForHost?: boolean;
localSubject?: string;
locked?: string;
membersOnly?: IJitsiConference;
@@ -155,11 +156,17 @@ export interface IConferenceState {
export interface IJitsiConferenceRoom {
locked: boolean;
moderator: {
logout: Function;
};
myroomjid: string;
roomjid: string;
xmpp: {
moderator: {
logout: Function;
};
};
}
interface IConferenceFailedError extends Error {
params: Array<any>;
}
/**
@@ -274,7 +281,7 @@ function _authStatusChanged(state: IConferenceState,
* reduction of the specified action.
*/
function _conferenceFailed(state: IConferenceState, { conference, error }: {
conference: IJitsiConference; error: Error; }) {
conference: IJitsiConference; error: IConferenceFailedError; }) {
// The current (similar to getCurrentConference in
// base/conference/functions.any.js) conference which is joining or joined:
const conference_ = state.conference || state.joining;
@@ -286,6 +293,7 @@ function _conferenceFailed(state: IConferenceState, { conference, error }: {
let authRequired;
let membersOnly;
let passwordRequired;
let lobbyWaitingForHost;
switch (error.name) {
case JitsiConferenceErrors.AUTHENTICATION_REQUIRED:
@@ -293,9 +301,16 @@ function _conferenceFailed(state: IConferenceState, { conference, error }: {
break;
case JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED:
case JitsiConferenceErrors.MEMBERS_ONLY_ERROR:
case JitsiConferenceErrors.MEMBERS_ONLY_ERROR: {
membersOnly = conference;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ _lobbyJid, _lobbyWaitingForHost ] = error.params;
lobbyWaitingForHost = _lobbyWaitingForHost;
break;
}
case JitsiConferenceErrors.PASSWORD_REQUIRED:
passwordRequired = conference;
@@ -309,6 +324,7 @@ function _conferenceFailed(state: IConferenceState, { conference, error }: {
error,
joining: undefined,
leaving: undefined,
lobbyWaitingForHost,
/**
* The indicator of how the conference/room is locked. If falsy, the
@@ -365,6 +381,8 @@ function _conferenceJoined(state: IConferenceState, { conference }: { conference
membersOnly: undefined,
leaving: undefined,
lobbyWaitingForHost: undefined,
/**
* The indicator which determines whether the conference is locked.
*

View File

@@ -182,6 +182,7 @@ export interface IConfig {
_screenshotHistoryRegionUrl?: number;
analytics?: {
amplitudeAPPKey?: string;
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
disabled?: boolean;
googleAnalyticsTrackingId?: string;
@@ -401,6 +402,7 @@ export interface IConfig {
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
disableTopPanel?: boolean;
disabled?: boolean;
minParticipantCountForTopPanel?: number;
};
firefox_fake_device?: string;

View File

@@ -1,9 +1,11 @@
import _ from 'lodash';
import { IReduxState, IStore } from '../../app/types';
import { conferenceLeft, conferenceWillLeave } from '../conference/actions';
import { conferenceLeft, conferenceWillLeave, redirect } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import { IConfigState } from '../config/reducer';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { parseURLParams } from '../util/parseURLParams';
import {
appendURLParam,
getBackendSafeRoomName
@@ -18,53 +20,14 @@ import {
} from './actionTypes';
import { JITSI_CONNECTION_URL_KEY } from './constants';
import logger from './logger';
import { ConnectionFailedError, IIceServers } from './types';
/**
* The error structure passed to the {@link connectionFailed} action.
*
* Note there was an intention to make the error resemble an Error instance (to
* the extent that jitsi-meet needs it).
* The options that will be passed to the JitsiConnection instance.
*/
export type ConnectionFailedError = {
/**
* The invalid credentials that were used to authenticate and the
* authentication failed.
*/
credentials?: {
/**
* The XMPP user's ID.
*/
jid: string;
/**
* The XMPP user's password.
*/
password: string;
};
/**
* The details about the connection failed event.
*/
details?: Object;
/**
* Error message.
*/
message?: string;
/**
* One of {@link JitsiConnectionError} constants (defined in
* lib-jitsi-meet).
*/
name: string;
/**
* Indicates whether this event is recoverable or not.
*/
recoverable?: boolean;
};
interface IOptions extends IConfigState {
iceServersOverride?: IIceServers;
}
/**
* Create an action for when the signaling connection has been lost.
@@ -147,7 +110,15 @@ export function connectionFailed(
export function constructOptions(state: IReduxState) {
// Deep clone the options to make sure we don't modify the object in the
// redux store.
const options = _.cloneDeep(state['features/base/config']);
const options: IOptions = _.cloneDeep(state['features/base/config']);
const { locationURL } = state['features/base/connection'];
const params = parseURLParams(locationURL || '');
const iceServersOverride = params['iceServers.replace'];
if (iceServersOverride) {
options.iceServersOverride = iceServersOverride;
}
const { bosh } = options;
let { websocket } = options;
@@ -230,6 +201,9 @@ export function _connectInternal(id?: string, password?: string) {
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
connection.addEventListener(
JitsiConnectionEvents.CONNECTION_REDIRECTED,
_onConnectionRedirected);
/**
* Unsubscribe the connection instance from
@@ -298,9 +272,28 @@ export function _connectInternal(id?: string, password?: string) {
resolve(connection);
}
/**
* Rejects external promise when connection fails.
*
* @param {string|undefined} vnode - The vnode to connect to.
* @param {string} focusJid - The focus jid to use.
* @param {string|undefined} username - The username to use when joining. This is after promotion from
* visitor to main participant.
* @private
* @returns {void}
*/
function _onConnectionRedirected(vnode: string, focusJid: string, username: string) {
connection.removeEventListener(JitsiConnectionEvents.CONNECTION_REDIRECTED, _onConnectionRedirected);
dispatch(redirect(vnode, focusJid, username));
}
// in case of configured http url for conference request we need the room name
const name = getBackendSafeRoomName(state['features/base/conference'].room);
connection.connect({
id,
password
password,
name
});
});
};

View File

@@ -58,9 +58,10 @@ export function connect(id?: string, password?: string) {
*
* @param {boolean} [requestFeedback] - Whether to attempt showing a
* request for call feedback.
* @param {string} [feedbackTitle] - The feedback title.
* @returns {Function}
*/
export function hangup(requestFeedback = false) {
export function hangup(requestFeedback = false, feedbackTitle?: string) {
// XXX For web based version we use conference hanging up logic from the old app.
return async (dispatch: IStore['dispatch']) => {
if (LocalRecordingManager.isRecordingLocally()) {
@@ -76,6 +77,6 @@ export function hangup(requestFeedback = false) {
});
}
return APP.conference.hangup(requestFeedback);
return APP.conference.hangup(requestFeedback, feedbackTitle);
};
}

View File

@@ -11,7 +11,7 @@ import {
SET_LOCATION_URL,
SHOW_CONNECTION_INFO
} from './actionTypes';
import { ConnectionFailedError } from './actions.any';
import { ConnectionFailedError } from './types';
export interface IConnectionState {
connecting?: any;

View File

@@ -0,0 +1,113 @@
/**
* The error structure passed to the {@link connectionFailed} action.
*
* Note there was an intention to make the error resemble an Error instance (to
* the extent that jitsi-meet needs it).
*/
export type ConnectionFailedError = {
/**
* The invalid credentials that were used to authenticate and the
* authentication failed.
*/
credentials?: {
/**
* The XMPP user's ID.
*/
jid: string;
/**
* The XMPP user's password.
*/
password: string;
};
/**
* The details about the connection failed event.
*/
details?: Object;
/**
* Error message.
*/
message?: string;
/**
* One of {@link JitsiConnectionError} constants (defined in
* lib-jitsi-meet).
*/
name: string;
/**
* Indicates whether this event is recoverable or not.
*/
recoverable?: boolean;
};
/**
* The value for the username or credential property.
*/
type ReplaceIceServersField = string | null;
/**
* The value for the urls property.
*/
type IceServerUrls = null | string | Array<string>;
/**
* The types of ice servers.
*/
enum IceServerType {
STUN = 'stun',
TURN = 'turn',
TURNS = 'turns'
}
/**
* Represents a single override rule.
*/
interface IReplaceIceServer {
/**
* The value the credential prop will be replaced with.
*
* NOTE: If the value is null we will remove the credential property in entry that matches the target type. If the
* value is undefined or missing we won't change the credential property in the entry that matches the target type.
*/
credential?: ReplaceIceServersField;
/**
* Target type that will be used to match the already received ice server and modify/remove it based on the values
* of credential, urls and username.
*/
targetType: IceServerType;
/**
* The value the urls prop will be replaced with.
*
* NOTE: If the value is null we will remove the whole entry that matches the target type. If the value is undefined
* or missing we won't change the urls property in the entry that matches the target type.
*/
urls?: IceServerUrls;
/**
* The value the username prop will be replaced with.
*
* NOTE: If the value is null we will remove the username property in entry that matches the target type. If the
* value is undefined or missing we won't change the username property in the entry that matches the target type.
*/
username?: ReplaceIceServersField;
}
/**
* An object with rules for changing the existing ice server configuration.
*/
export interface IIceServers {
/**
* An array of rules for replacing parts from the existing ice server configuration.
*/
replace: Array<IReplaceIceServer>;
}

View File

@@ -9,10 +9,12 @@ import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
import { isMobileBrowser } from '../environment/utils';
import JitsiMeetJS, { JitsiMediaDevicesEvents, JitsiTrackErrors } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { updateSettings } from '../settings/actions';
import { getLocalTrack } from '../tracks/functions';
import {
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
@@ -182,13 +184,20 @@ MiddlewareRegistry.register(store => next => action => {
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
}
break;
case SET_VIDEO_INPUT_DEVICE:
case SET_VIDEO_INPUT_DEVICE: {
const localTrack = getLocalTrack(store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO);
// on mobile devices the video stream has to be stopped before replacing it
if (isMobileBrowser() && localTrack && !localTrack.muted) {
localTrack.jitsiTrack.stopStream();
}
if (isPrejoinPageVisible(store.getState())) {
store.dispatch(replaceVideoTrackById(action.deviceId));
} else {
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
}
break;
}
case UPDATE_DEVICE_LIST:
logDeviceList(groupDevicesByKind(action.devices));
if (areDeviceLabelsInitialized(store.getState())) {

View File

@@ -18,3 +18,4 @@ export function isMobileBrowser() {
export function isIosMobileBrowser() {
return Platform.OS === 'ios';
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { SvgFromXml } from 'react-native-svg';
/**
* SVG rendering component.
*
* @returns {JSX.Element}
*/
const SvgXmlIcon = ({ src, ...rest }: {
src: string;
}): JSX.Element => (
<SvgFromXml
override = { rest }
xml = { src } />
);
export default SvgXmlIcon;

View File

@@ -0,0 +1,26 @@
import React, { useMemo } from 'react';
/**
* SVG rendering component.
*
* @returns {JSX.Element}
*/
const SvgXmlIcon = ({ src, ...rest }: {
src: string;
}): JSX.Element => {
const svgDocument = new DOMParser().parseFromString(src, 'image/svg+xml');
const element = svgDocument.documentElement.outerHTML;
const attributes = useMemo(() => Object.entries(rest).map(
([ key, value ]) => `${key}="${value}"`)
.join(' '), [ rest ]);
const html = element.replace('<svg', `<svg ${attributes}`);
return (
<div // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML = {{ __html: html }}
{ ...rest } />
);
};
export default SvgXmlIcon;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import SvgXmlIcon from './SvgXmlIcon';
/**
* Icon wrapper that checks for branding before returning the SVG component.
*
* @returns {JSX.Element}
*/
const withBranding = ({ DefaultIcon, iconName }: {
DefaultIcon: any;
iconName: string;
}) => (props: any) => {
const src = useSelector((state: IReduxState) =>
state['features/dynamic-branding']?.brandedIcons?.[iconName]
);
if (src) {
return (
<SvgXmlIcon
src = { src }
{ ...props } />
);
}
return <DefaultIcon { ...props } />;
};
export default withBranding;

View File

@@ -0,0 +1,224 @@
import { default as IconRecordAccount } from './account-record.svg';
import { default as IconAddUser } from './add-user.svg';
import { default as IconArrowBack } from './arrow-back.svg';
import { default as IconArrowDownLarge } from './arrow-down-large.svg';
import { default as IconArrowDown } from './arrow-down.svg';
import { default as IconArrowLeft } from './arrow-left.svg';
import { default as IconArrowRight } from './arrow-right.svg';
import { default as IconArrowUpLarge } from './arrow-up-large.svg';
import { default as IconArrowUp } from './arrow-up.svg';
import { default as IconBell } from './bell.svg';
import { default as IconBluetooth } from './bluetooth.svg';
import { default as IconCalendar } from './calendar.svg';
import { default as IconCameraRefresh } from './camera-refresh.svg';
import { default as IconCar } from './car.svg';
import { default as IconChatUnread } from './chat-unread.svg';
import { default as IconCheck } from './check.svg';
import { default as IconCloseCircle } from './close-circle.svg';
import { default as IconCloseLarge } from './close-large.svg';
import { default as IconCloudUpload } from './cloud-upload.svg';
import { default as IconCode } from './code.svg';
import { default as IconConnection } from './connection.svg';
import { default as IconRecordContact } from './contact-record.svg';
import { default as IconCopy } from './copy.svg';
import { default as IconDotsHorizontal } from './dots-horizontal.svg';
import { default as IconDownload } from './download.svg';
import { default as IconE2EE } from './e2ee.svg';
import { default as IconEdit } from './edit.svg';
import { default as IconEmotionsAngry } from './emotions-angry.svg';
import { default as IconEmotionsDisgusted } from './emotions-disgusted.svg';
import { default as IconEmotionsFearful } from './emotions-fearful.svg';
import { default as IconEmotionsHappy } from './emotions-happy.svg';
import { default as IconEmotionsNeutral } from './emotions-neutral.svg';
import { default as IconEmotionsSad } from './emotions-sad.svg';
import { default as IconEmotionsSurprised } from './emotions-surprised.svg';
import { default as IconEnlarge } from './enlarge.svg';
import { default as IconEnterFullscreen } from './enter-fullscreen.svg';
import { default as IconEnvelope } from './envelope.svg';
import { default as IconExclamationSolid } from './exclamation-solid.svg';
import { default as IconExclamationTriangle } from './exclamation-triangle.svg';
import { default as IconExitFullscreen } from './exit-fullscreen.svg';
import { default as IconFaceSmile } from './face-smile.svg';
import { default as IconFavoriteSolid } from './favorite-solid.svg';
import { default as IconFavorite } from './favorite.svg';
import { default as IconFeedback } from './feedback.svg';
import { default as IconGear } from './gear.svg';
import { default as IconGoogle } from './google.svg';
import { default as IconHangup } from './hangup.svg';
import { default as IconDeviceHeadphone } from './headset.svg';
import { default as IconHelp } from './help.svg';
import { default as IconHighlight } from './highlight.svg';
import { default as IconRingGroup } from './icon-ring-group.svg';
import { default as IconImage } from './image.svg';
import { default as IconInfoCircle } from './info-circle.svg';
import { default as IconInfo } from './info.svg';
import { default as IconRecordLead } from './lead-record.svg';
import { default as IconMessage } from './message.svg';
import { default as IconMeter } from './meter.svg';
import { default as IconMicSlash } from './mic-slash.svg';
import { default as IconMic } from './mic.svg';
import { default as IconModerator } from './moderator.svg';
import { default as IconConnectionInactive } from './ninja.svg';
import { default as IconNoiseSuppressionOff } from './noise-suppression-off.svg';
import { default as IconNoiseSuppressionOn } from './noise-suppression-on.svg';
import { default as IconOffice365 } from './office365.svg';
import { default as IconRecordOpportunity } from './opportunity-record.svg';
import { default as IconPerformance } from './performance.svg';
import { default as IconPhoneRinging } from './phone-ringing.svg';
import { default as IconPin } from './pin.svg';
import { default as IconPinned } from './pinned.svg';
import { default as IconPlay } from './play.svg';
import { default as IconPlus } from './plus.svg';
import { default as IconRaiseHand } from './raise-hand.svg';
import { default as IconRecord } from './record.svg';
import { default as IconReply } from './reply.svg';
import { default as IconRestore } from './restore.svg';
import { default as IconScreenshare } from './screenshare.svg';
import { default as IconSearch } from './search.svg';
import { default as IconSecurityOff } from './security-off.svg';
import { default as IconSecurityOn } from './security-on.svg';
import { default as IconSend } from './send.svg';
import { default as IconShareDoc } from './share-doc.svg';
import { default as IconShare } from './share.svg';
import { default as IconShortcuts } from './shortcuts.svg';
import { default as IconSip } from './sip.svg';
import { default as IconSites } from './sites.svg';
import { default as IconRemoteControlStart } from './start-remote-control.svg';
import { default as IconRemoteControlStop } from './stop-remote-control.svg';
import { default as IconStopScreenshare } from './stop-screenshare.svg';
import { default as IconStop } from './stop.svg';
import { default as IconSubtitles } from './subtitles.svg';
import { default as IconTileView } from './tile-view.svg';
import { default as IconTrash } from './trash.svg';
import { default as IconUserDeleted } from './user-deleted.svg';
import { default as IconUser } from './user.svg';
import { default as IconUsers } from './users.svg';
import { default as IconVideoOff } from './video-off.svg';
import { default as IconVideo } from './video.svg';
import { default as IconAudioOnlyOff } from './visibility-off.svg';
import { default as IconAudioOnly } from './visibility.svg';
import { default as IconVolumeOff } from './volume-off.svg';
import { default as IconVolumeUp } from './volume-up.svg';
import { default as IconWarningCircle } from './warning-circle.svg';
import { default as IconWarning } from './warning.svg';
import { default as IconWhiteboardHide } from './whiteboard-hide.svg';
import { default as IconWhiteboard } from './whiteboard.svg';
import { default as IconWifi1Bar } from './wifi-1.svg';
import { default as IconWifi2Bars } from './wifi-2.svg';
import { default as IconWifi3Bars } from './wifi-3.svg';
import { default as IconYahoo } from './yahoo.svg';
/**
* Map containing the default icons.
*/
export const DEFAULT_ICON: Record<string, any> = {
IconAddUser,
IconArrowBack,
IconArrowDown,
IconArrowDownLarge,
IconArrowLeft,
IconArrowUp,
IconArrowUpLarge,
IconAudioOnly,
IconAudioOnlyOff,
IconBluetooth,
IconBell,
IconCalendar,
IconCameraRefresh,
IconCar,
IconChatUnread,
IconCheck,
IconCloseCircle,
IconCloseLarge,
IconCloudUpload,
IconCode,
IconConnection,
IconConnectionInactive,
IconCopy,
IconDeviceHeadphone,
IconDotsHorizontal,
IconDownload,
IconE2EE,
IconEdit,
IconEnlarge,
IconEnterFullscreen,
IconEnvelope,
IconEmotionsAngry,
IconEmotionsDisgusted,
IconEmotionsFearful,
IconEmotionsHappy,
IconEmotionsNeutral,
IconEmotionsSad,
IconEmotionsSurprised,
IconExclamationSolid,
IconExclamationTriangle,
IconExitFullscreen,
IconFaceSmile,
IconFavorite,
IconFavoriteSolid,
IconFeedback,
IconGear,
IconGoogle,
IconHangup,
IconHelp,
IconHighlight,
IconImage,
IconInfo,
IconInfoCircle,
IconMessage,
IconMeter,
IconMic,
IconMicSlash,
IconModerator,
IconNoiseSuppressionOff,
IconNoiseSuppressionOn,
IconArrowRight,
IconOffice365,
IconPerformance,
IconPhoneRinging,
IconPin,
IconPinned,
IconPlay,
IconPlus,
IconRaiseHand,
IconRecord,
IconRecordAccount,
IconRecordContact,
IconRecordLead,
IconRecordOpportunity,
IconRemoteControlStart,
IconRemoteControlStop,
IconReply,
IconRestore,
IconRingGroup,
IconScreenshare,
IconSearch,
IconSecurityOff,
IconSecurityOn,
IconSend,
IconShare,
IconShareDoc,
IconShortcuts,
IconSip,
IconSites,
IconStop,
IconStopScreenshare,
IconSubtitles,
IconTileView,
IconTrash,
IconUserDeleted,
IconUsers,
IconUser,
IconVideo,
IconVideoOff,
IconVolumeOff,
IconVolumeUp,
IconWarning,
IconWarningCircle,
IconWhiteboard,
IconWhiteboardHide,
IconWifi1Bar,
IconWifi2Bars,
IconWifi3Bars,
IconYahoo
};

View File

@@ -1,109 +1,235 @@
export { default as IconAddUser } from './add-user.svg';
export { default as IconArrowBack } from './arrow-back.svg';
export { default as IconArrowDown } from './arrow-down.svg';
export { default as IconArrowDownLarge } from './arrow-down-large.svg';
export { default as IconArrowLeft } from './arrow-left.svg';
export { default as IconArrowUp } from './arrow-up.svg';
export { default as IconArrowUpLarge } from './arrow-up-large.svg';
export { default as IconAudioOnly } from './visibility.svg';
export { default as IconAudioOnlyOff } from './visibility-off.svg';
export { default as IconBluetooth } from './bluetooth.svg';
export { default as IconBell } from './bell.svg';
export { default as IconCalendar } from './calendar.svg';
export { default as IconCameraRefresh } from './camera-refresh.svg';
export { default as IconCar } from './car.svg';
export { default as IconChatUnread } from './chat-unread.svg';
export { default as IconCheck } from './check.svg';
export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseLarge } from './close-large.svg';
export { default as IconCloudUpload } from './cloud-upload.svg';
export { default as IconCode } from './code.svg';
export { default as IconConnection } from './connection.svg';
export { default as IconConnectionInactive } from './ninja.svg';
export { default as IconCopy } from './copy.svg';
export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDotsHorizontal } from './dots-horizontal.svg';
export { default as IconDownload } from './download.svg';
export { default as IconE2EE } from './e2ee.svg';
export { default as IconEdit } from './edit.svg';
export { default as IconEnlarge } from './enlarge.svg';
export { default as IconEnterFullscreen } from './enter-fullscreen.svg';
export { default as IconEnvelope } from './envelope.svg';
export { default as IconEmotionsAngry } from './emotions-angry.svg';
export { default as IconEmotionsDisgusted } from './emotions-disgusted.svg';
export { default as IconEmotionsFearful } from './emotions-fearful.svg';
export { default as IconEmotionsHappy } from './emotions-happy.svg';
export { default as IconEmotionsNeutral } from './emotions-neutral.svg';
export { default as IconEmotionsSad } from './emotions-sad.svg';
export { default as IconEmotionsSurprised } from './emotions-surprised.svg';
export { default as IconExclamationSolid } from './exclamation-solid.svg';
export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
export { default as IconExitFullscreen } from './exit-fullscreen.svg';
export { default as IconFaceSmile } from './face-smile.svg';
export { default as IconFavorite } from './favorite.svg';
export { default as IconFavoriteSolid } from './favorite-solid.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconGear } from './gear.svg';
export { default as IconGoogle } from './google.svg';
export { default as IconHangup } from './hangup.svg';
export { default as IconHelp } from './help.svg';
export { default as IconHighlight } from './highlight.svg';
export { default as IconImage } from './image.svg';
export { default as IconInfo } from './info.svg';
export { default as IconInfoCircle } from './info-circle.svg';
export { default as IconMessage } from './message.svg';
export { default as IconMeter } from './meter.svg';
export { default as IconMic } from './mic.svg';
export { default as IconMicSlash } from './mic-slash.svg';
export { default as IconModerator } from './moderator.svg';
export { default as IconNoiseSuppressionOff } from './noise-suppression-off.svg';
export { default as IconNoiseSuppressionOn } from './noise-suppression-on.svg';
export { default as IconArrowRight } from './arrow-right.svg';
export { default as IconOffice365 } from './office365.svg';
export { default as IconPerformance } from './performance.svg';
export { default as IconPhoneRinging } from './phone-ringing.svg';
export { default as IconPin } from './pin.svg';
export { default as IconPinned } from './pinned.svg';
export { default as IconPlay } from './play.svg';
export { default as IconPlus } from './plus.svg';
export { default as IconRaiseHand } from './raise-hand.svg';
export { default as IconRecord } from './record.svg';
export { default as IconRecordAccount } from './account-record.svg';
export { default as IconRecordContact } from './contact-record.svg';
export { default as IconRecordLead } from './lead-record.svg';
export { default as IconRecordOpportunity } from './opportunity-record.svg';
export { default as IconRemoteControlStart } from './start-remote-control.svg';
export { default as IconRemoteControlStop } from './stop-remote-control.svg';
export { default as IconReply } from './reply.svg';
export { default as IconRestore } from './restore.svg';
export { default as IconRingGroup } from './icon-ring-group.svg';
export { default as IconScreenshare } from './screenshare.svg';
export { default as IconSearch } from './search.svg';
export { default as IconSecurityOff } from './security-off.svg';
export { default as IconSecurityOn } from './security-on.svg';
export { default as IconSend } from './send.svg';
export { default as IconShare } from './share.svg';
export { default as IconShareDoc } from './share-doc.svg';
export { default as IconShortcuts } from './shortcuts.svg';
export { default as IconSip } from './sip.svg';
export { default as IconSites } from './sites.svg';
export { default as IconStop } from './stop.svg';
export { default as IconStopScreenshare } from './stop-screenshare.svg';
export { default as IconSubtitles } from './subtitles.svg';
export { default as IconTileView } from './tile-view.svg';
export { default as IconTrash } from './trash.svg';
export { default as IconUserDeleted } from './user-deleted.svg';
export { default as IconUsers } from './users.svg';
export { default as IconUser } from './user.svg';
export { default as IconVideo } from './video.svg';
export { default as IconVideoOff } from './video-off.svg';
export { default as IconVolumeOff } from './volume-off.svg';
export { default as IconVolumeUp } from './volume-up.svg';
export { default as IconWarning } from './warning.svg';
export { default as IconWarningCircle } from './warning-circle.svg';
export { default as IconWhiteboard } from './whiteboard.svg';
export { default as IconWhiteboardHide } from './whiteboard-hide.svg';
export { default as IconWifi1Bar } from './wifi-1.svg';
export { default as IconWifi2Bars } from './wifi-2.svg';
export { default as IconWifi3Bars } from './wifi-3.svg';
export { default as IconYahoo } from './yahoo.svg';
import withBranding from '../components/withBranding';
import { DEFAULT_ICON } from './constants';
const {
IconAddUser,
IconArrowBack,
IconArrowDown,
IconArrowDownLarge,
IconArrowLeft,
IconArrowUp,
IconArrowUpLarge,
IconAudioOnly,
IconAudioOnlyOff,
IconBluetooth,
IconBell,
IconCalendar,
IconCameraRefresh,
IconCar,
IconChatUnread,
IconCheck,
IconCloseCircle,
IconCloseLarge,
IconCloudUpload,
IconCode,
IconConnection,
IconConnectionInactive,
IconCopy,
IconDeviceHeadphone,
IconDotsHorizontal,
IconDownload,
IconE2EE,
IconEdit,
IconEnlarge,
IconEnterFullscreen,
IconEnvelope,
IconEmotionsAngry,
IconEmotionsDisgusted,
IconEmotionsFearful,
IconEmotionsHappy,
IconEmotionsNeutral,
IconEmotionsSad,
IconEmotionsSurprised,
IconExclamationSolid,
IconExclamationTriangle,
IconExitFullscreen,
IconFaceSmile,
IconFavorite,
IconFavoriteSolid,
IconFeedback,
IconGear,
IconGoogle,
IconHangup,
IconHelp,
IconHighlight,
IconImage,
IconInfo,
IconInfoCircle,
IconMessage,
IconMeter,
IconMic,
IconMicSlash,
IconModerator,
IconNoiseSuppressionOff,
IconNoiseSuppressionOn,
IconArrowRight,
IconOffice365,
IconPerformance,
IconPhoneRinging,
IconPin,
IconPinned,
IconPlay,
IconPlus,
IconRaiseHand,
IconRecord,
IconRecordAccount,
IconRecordContact,
IconRecordLead,
IconRecordOpportunity,
IconRemoteControlStart,
IconRemoteControlStop,
IconReply,
IconRestore,
IconRingGroup,
IconScreenshare,
IconSearch,
IconSecurityOff,
IconSecurityOn,
IconSend,
IconShare,
IconShareDoc,
IconShortcuts,
IconSip,
IconSites,
IconStop,
IconStopScreenshare,
IconSubtitles,
IconTileView,
IconTrash,
IconUserDeleted,
IconUsers,
IconUser,
IconVideo,
IconVideoOff,
IconVolumeOff,
IconVolumeUp,
IconWarning,
IconWarningCircle,
IconWhiteboard,
IconWhiteboardHide,
IconWifi1Bar,
IconWifi2Bars,
IconWifi3Bars,
IconYahoo
} = Object.keys(DEFAULT_ICON).reduce((exportedIcons: Record<string, any>, key) => {
return {
...exportedIcons,
[key]: withBranding({
iconName: key,
DefaultIcon: DEFAULT_ICON[key]
})
};
}, {});
export {
IconAddUser,
IconArrowBack,
IconArrowDown,
IconArrowDownLarge,
IconArrowLeft,
IconArrowUp,
IconArrowUpLarge,
IconAudioOnly,
IconAudioOnlyOff,
IconBluetooth,
IconBell,
IconCalendar,
IconCameraRefresh,
IconCar,
IconChatUnread,
IconCheck,
IconCloseCircle,
IconCloseLarge,
IconCloudUpload,
IconCode,
IconConnection,
IconConnectionInactive,
IconCopy,
IconDeviceHeadphone,
IconDotsHorizontal,
IconDownload,
IconE2EE,
IconEdit,
IconEnlarge,
IconEnterFullscreen,
IconEnvelope,
IconEmotionsAngry,
IconEmotionsDisgusted,
IconEmotionsFearful,
IconEmotionsHappy,
IconEmotionsNeutral,
IconEmotionsSad,
IconEmotionsSurprised,
IconExclamationSolid,
IconExclamationTriangle,
IconExitFullscreen,
IconFaceSmile,
IconFavorite,
IconFavoriteSolid,
IconFeedback,
IconGear,
IconGoogle,
IconHangup,
IconHelp,
IconHighlight,
IconImage,
IconInfo,
IconInfoCircle,
IconMessage,
IconMeter,
IconMic,
IconMicSlash,
IconModerator,
IconNoiseSuppressionOff,
IconNoiseSuppressionOn,
IconArrowRight,
IconOffice365,
IconPerformance,
IconPhoneRinging,
IconPin,
IconPinned,
IconPlay,
IconPlus,
IconRaiseHand,
IconRecord,
IconRecordAccount,
IconRecordContact,
IconRecordLead,
IconRecordOpportunity,
IconRemoteControlStart,
IconRemoteControlStop,
IconReply,
IconRestore,
IconRingGroup,
IconScreenshare,
IconSearch,
IconSecurityOff,
IconSecurityOn,
IconSend,
IconShare,
IconShareDoc,
IconShortcuts,
IconSip,
IconSites,
IconStop,
IconStopScreenshare,
IconSubtitles,
IconTileView,
IconTrash,
IconUserDeleted,
IconUsers,
IconUser,
IconVideo,
IconVideoOff,
IconVolumeOff,
IconVolumeUp,
IconWarning,
IconWarningCircle,
IconWhiteboard,
IconWhiteboardHide,
IconWifi1Bar,
IconWifi2Bars,
IconWifi3Bars,
IconYahoo
};

View File

@@ -17,8 +17,6 @@ import logger from './logger';
* @returns {void}
*/
function onFakeLocalStorageChanged() {
console.error(jitsiLocalStorage.serialize([ 'jitsiLocalStorage' ]));
APP.API.notifyLocalStorageChanged(jitsiLocalStorage.serialize([ 'jitsiLocalStorage' ]));
}

View File

@@ -1,5 +1,5 @@
import { IStateful } from '../app/types';
import { ConnectionFailedError } from '../connection/actions.any';
import { ConnectionFailedError } from '../connection/types';
import { toState } from '../redux/functions';
import JitsiMeetJS from './_';

View File

@@ -22,3 +22,4 @@ export const JitsiRecordingConstants = JitsiMeetJS.constants.recording;
export const JitsiSIPVideoGWStatus = JitsiMeetJS.constants.sipVideoGW;
export const JitsiTrackErrors = JitsiMeetJS.errors.track;
export const JitsiTrackEvents = JitsiMeetJS.events.track;
export const RTCStatsEvents = JitsiMeetJS.events.rtcstats;

View File

@@ -1,6 +1,6 @@
import { IStore } from '../../app/types';
import RTCStats from '../../rtcstats/RTCStats';
import { canSendRtcstatsData } from '../../rtcstats/functions';
import { isRTCStatsEnabled } from '../../rtcstats/functions';
import { getCurrentConference } from '../conference/functions';
/**
@@ -60,7 +60,7 @@ export default class JitsiMeetLogStorage {
// Saving the logs in RTCStats is a new feature and so there is no prior behavior that needs to be maintained.
// That said, this is still experimental and needs to be rolled out gradually so we want this to be off by
// default.
return config?.analytics?.rtcstatsStoreLogs && canSendRtcstatsData(this.getState());
return config?.analytics?.rtcstatsStoreLogs && isRTCStatsEnabled(this.getState());
}
/**
@@ -120,9 +120,7 @@ export default class JitsiMeetLogStorage {
conference.sendApplicationLog(logMessage);
} catch (error) {
// NOTE console is intentional here
console.error(
`Failed to store the logs, msg length: ${logMessage.length}`
+ `error: ${JSON.stringify(error)}`);
console.error(`Failed to store the logs, msg length: ${logMessage.length} error:`, error);
}
}
}

View File

@@ -51,7 +51,7 @@ class AudioTrack extends Component<IProps> {
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
_ref: HTMLAudioElement | null;
_ref: React.RefObject<HTMLAudioElement>;
/**
* The current timeout ID for play() retries.
@@ -80,7 +80,7 @@ class AudioTrack extends Component<IProps> {
// Bind event handlers so they are only bound once for every instance.
this._errorHandler = this._errorHandler.bind(this);
this._setRef = this._setRef.bind(this);
this._ref = React.createRef();
this._play = this._play.bind(this);
}
@@ -94,19 +94,22 @@ class AudioTrack extends Component<IProps> {
componentDidMount() {
this._attachTrack(this.props.audioTrack);
if (this._ref) {
if (this._ref?.current) {
const audio = this._ref?.current;
const { _muted, _volume } = this.props;
if (typeof _volume === 'number') {
this._ref.volume = _volume;
audio.volume = _volume;
}
if (typeof _muted === 'boolean') {
this._ref.muted = _muted;
audio.muted = _muted;
}
// @ts-ignore
this._ref.addEventListener('error', this._errorHandler);
audio.addEventListener('error', this._errorHandler);
} else { // This should never happen
logger.error(`The react reference is null for AudioTrack ${this.props?.id}`);
}
}
@@ -121,7 +124,7 @@ class AudioTrack extends Component<IProps> {
this._detachTrack(this.props.audioTrack);
// @ts-ignore
this._ref?.removeEventListener('error', this._errorHandler);
this._ref?.current?.removeEventListener('error', this._errorHandler);
}
/**
@@ -141,19 +144,25 @@ class AudioTrack extends Component<IProps> {
this._attachTrack(nextProps.audioTrack);
}
if (this._ref) {
const currentVolume = this._ref.volume;
if (this._ref?.current) {
const audio = this._ref?.current;
const currentVolume = audio.volume;
const nextVolume = nextProps._volume;
if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
this._ref.volume = nextVolume;
if (nextVolume === 0) {
logger.debug(`Setting audio element ${nextProps?.id} volume to 0`);
}
audio.volume = nextVolume;
}
const currentMuted = this._ref.muted;
const currentMuted = audio.muted;
const nextMuted = nextProps._muted;
if (typeof nextMuted === 'boolean' && currentMuted !== nextMuted) {
this._ref.muted = nextMuted;
logger.debug(`Setting audio element ${nextProps?.id} muted to true`);
audio.muted = nextMuted;
}
}
@@ -173,7 +182,7 @@ class AudioTrack extends Component<IProps> {
<audio
autoPlay = { autoPlay }
id = { id }
ref = { this._setRef } />
ref = { this._ref } />
);
}
@@ -185,12 +194,29 @@ class AudioTrack extends Component<IProps> {
* @returns {void}
*/
_attachTrack(track?: ITrack) {
const { id } = this.props;
if (!track?.jitsiTrack) {
logger.warn(`Attach is called on audio element ${id} without tracks passed!`);
return;
}
track.jitsiTrack.attach(this._ref);
this._play();
if (!this._ref?.current) {
logger.warn(`Attempting to attach track ${track?.jitsiTrack} on AudioTrack ${id} without reference!`);
return;
}
track.jitsiTrack.attach(this._ref.current)
.catch((error: Error) => {
logger.error(
`Attaching the remote track ${track.jitsiTrack} to video with id ${id} has failed with `,
error);
})
.finally(() => {
this._play();
});
}
/**
@@ -202,10 +228,10 @@ class AudioTrack extends Component<IProps> {
* @returns {void}
*/
_detachTrack(track?: ITrack) {
if (this._ref && track && track.jitsiTrack) {
if (this._ref?.current && track && track.jitsiTrack) {
clearTimeout(this._playTimeout);
this._playTimeout = undefined;
track.jitsiTrack.detach(this._ref);
track.jitsiTrack.detach(this._ref.current);
}
}
@@ -229,18 +255,20 @@ class AudioTrack extends Component<IProps> {
* @returns {void}
*/
_play(retries = 0) {
if (!this._ref) {
const { autoPlay, id } = this.props;
if (!this._ref?.current) {
// nothing to play.
logger.warn(`Attempting to call play on AudioTrack ${id} without reference!`);
return;
}
const { autoPlay, id } = this.props;
if (autoPlay) {
// Ensure the audio gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case the audio may not autoplay.
this._ref.play()
this._ref.current.play()
.then(() => {
if (retries !== 0) {
// success after some failures
@@ -249,7 +277,7 @@ class AudioTrack extends Component<IProps> {
logger.info(`Successfully played audio track! retries: ${retries}`);
}
}, e => {
logger.error(`Failed to play audio track! retry: ${retries} ; Error: ${e}`);
logger.error(`Failed to play audio track on audio element ${id}! retry: ${retries} ; Error:`, e);
if (retries < 3) {
this._playTimeout = window.setTimeout(() => this._play(retries + 1), 1000);
@@ -264,17 +292,6 @@ class AudioTrack extends Component<IProps> {
});
}
}
/**
* Sets the reference to the HTML audio element.
*
* @param {HTMLAudioElement} audioElement - The HTML audio element instance.
* @private
* @returns {void}
*/
_setRef(audioElement: HTMLAudioElement | null) {
this._ref = audioElement;
}
}
/**

View File

@@ -1,6 +1,7 @@
import React, { Component, ReactEventHandler } from 'react';
import { ITrack } from '../../../tracks/types';
import logger from '../../logger';
/**
* The type of the React {@code Component} props of {@link Video}.
@@ -227,13 +228,13 @@ class Video extends Component<IProps> {
this._videoElement.onplaying = this._onVideoPlaying;
}
this._attachTrack(this.props.videoTrack);
this._attachTrack(this.props.videoTrack).finally(() => {
if (this._videoElement && this.props.autoPlay) {
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
if (this._videoElement && this.props.autoPlay) {
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
this._videoElement.play()
this._videoElement.play()
.catch(error => {
// Prevent uncaught "DOMException: The play() request was interrupted by a new load request"
// when video playback takes long to start and it starts after the component was unmounted.
@@ -241,7 +242,8 @@ class Video extends Component<IProps> {
throw error;
}
});
}
}
});
}
/**
@@ -271,7 +273,9 @@ class Video extends Component<IProps> {
if (currentJitsiTrack !== nextJitsiTrack) {
this._detachTrack(this.props.videoTrack);
this._attachTrack(nextProps.videoTrack);
this._attachTrack(nextProps.videoTrack).catch((_error: Error) => {
// Ignore the error. We are already logging it.
});
}
if (this.props.style !== nextProps.style || this.props.className !== nextProps.className) {
@@ -321,11 +325,22 @@ class Video extends Component<IProps> {
* @returns {void}
*/
_attachTrack(videoTrack?: Partial<ITrack>) {
const { id } = this.props;
if (!videoTrack?.jitsiTrack) {
return;
logger.warn(`Attach is called on video element ${id} without tracks passed!`);
// returning Promise.resolve just keep the previous logic.
// TODO: Check if it make sense to call play on this element or we can just return promise.reject().
return Promise.resolve();
}
videoTrack.jitsiTrack.attach(this._videoElement);
return videoTrack.jitsiTrack.attach(this._videoElement)
.catch((error: Error) => {
logger.error(
`Attaching the remote track ${videoTrack.jitsiTrack} to video with id ${id} has failed with `,
error);
});
}
/**

View File

@@ -1,10 +1,7 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Button as NativePaperButton,
Text,
TouchableRipple
} from 'react-native-paper';
import { TouchableHighlight } from 'react-native';
import { Button as NativePaperButton, Text } from 'react-native-paper';
import { BUTTON_MODES, BUTTON_TYPES } from '../../constants.native';
import BaseTheme from '../BaseTheme.native';
@@ -12,13 +9,13 @@ import { IButtonProps } from '../types';
import styles from './buttonStyles';
export interface IProps extends IButtonProps {
color?: string | undefined;
contentStyle?: Object | undefined;
labelStyle?: Object | undefined;
mode?: any;
style?: Object | undefined;
useRippleColor?: boolean;
}
const Button: React.FC<IProps> = ({
@@ -32,16 +29,12 @@ const Button: React.FC<IProps> = ({
mode = BUTTON_MODES.CONTAINED,
onClick: onPress,
style,
type,
useRippleColor = true
type
}: IProps) => {
const { t } = useTranslation();
const { DESTRUCTIVE, PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
const { CONTAINED, TEXT } = BUTTON_MODES;
const rippleColor
= useRippleColor ? BaseTheme.palette.action03Active : 'transparent';
let buttonLabelStyles;
let buttonStyles;
let color;
@@ -72,17 +65,16 @@ const Button: React.FC<IProps> = ({
}
if (type === TERTIARY) {
if (useRippleColor && disabled) {
if (disabled) {
buttonLabelStyles = styles.buttonLabelTertiaryDisabled;
}
buttonLabelStyles = styles.buttonLabelTertiary;
return (
<TouchableRipple
<TouchableHighlight
accessibilityLabel = { accessibilityLabel }
disabled = { disabled }
onPress = { onPress }
rippleColor = { rippleColor }
style = { [
buttonStyles,
style
@@ -92,7 +84,7 @@ const Button: React.FC<IProps> = ({
buttonLabelStyles,
labelStyle
] }>{ t(labelKey ?? '') }</Text>
</TouchableRipple>
</TouchableHighlight>
);
}

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { TouchableRipple } from 'react-native-paper';
import { TouchableHighlight } from 'react-native';
import Icon from '../../../icons/components/Icon';
import styles from '../../../react/components/native/styles';
@@ -22,47 +22,47 @@ const IconButton: React.FC<IIconButtonProps> = ({
const { PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
let color;
let rippleColor;
let underlayColor;
let iconButtonContainerStyles;
if (type === PRIMARY) {
color = BaseTheme.palette.icon01;
iconButtonContainerStyles = styles.iconButtonContainerPrimary;
rippleColor = BaseTheme.palette.action01;
underlayColor = BaseTheme.palette.action01;
} else if (type === SECONDARY) {
color = BaseTheme.palette.icon04;
iconButtonContainerStyles = styles.iconButtonContainerSecondary;
rippleColor = BaseTheme.palette.action02;
underlayColor = BaseTheme.palette.action02;
} else if (type === TERTIARY) {
color = iconColor;
iconButtonContainerStyles = styles.iconButtonContainer;
rippleColor = BaseTheme.palette.action03;
underlayColor = BaseTheme.palette.action03;
} else {
color = iconColor;
rippleColor = tapColor;
underlayColor = tapColor;
}
if (disabled) {
color = BaseTheme.palette.icon03;
iconButtonContainerStyles = styles.iconButtonContainerDisabled;
rippleColor = 'transparent';
underlayColor = 'transparent';
}
return (
<TouchableRipple
<TouchableHighlight
accessibilityLabel = { accessibilityLabel }
disabled = { disabled }
onPress = { onPress }
rippleColor = { rippleColor }
style = { [
iconButtonContainerStyles,
style
] }>
] }
underlayColor = { underlayColor }>
<Icon
color = { color }
size = { 20 || size }
src = { src } />
</TouchableRipple>
</TouchableHighlight>
);
};

View File

@@ -139,6 +139,7 @@ export interface IProps {
onClose?: () => void;
size?: 'large' | 'medium';
submit?: () => void;
testId?: string;
title?: string;
titleKey?: string;
}
@@ -152,6 +153,7 @@ const BaseDialog = ({
onClose,
size = 'medium',
submit,
testId,
title,
titleKey
}: IProps) => {
@@ -179,7 +181,9 @@ const BaseDialog = ({
}, [ handleKeyDown ]);
return (
<div className = { cx(classes.container, isUnmounting && 'unmount') }>
<div
className = { cx(classes.container, isUnmounting && 'unmount') }
data-testid = { testId }>
<div className = { classes.backdrop } />
<FocusOn
className = { classes.focusLock }

View File

@@ -96,6 +96,7 @@ const Dialog = ({
onCancel,
onSubmit,
size,
testId,
title,
titleKey
}: IDialogProps) => {
@@ -127,6 +128,7 @@ const Dialog = ({
onClose = { onClose }
size = { size }
submit = { submit }
testId = { testId }
title = { title }
titleKey = { titleKey }>
<div className = { classes.header }>

View File

@@ -559,7 +559,7 @@ export function urlObjectToString(o: { [key: string]: any; }): string | undefine
let { hash } = url;
for (const urlPrefix of [ 'config', 'interfaceConfig', 'devices', 'userInfo', 'appData' ]) {
for (const urlPrefix of [ 'config', 'iceServers', 'interfaceConfig', 'devices', 'userInfo', 'appData' ]) {
const urlParamsArray
= _objectToURLParamsArray(
o[`${urlPrefix}Overwrite`]

View File

@@ -3,8 +3,8 @@ import { WithTranslation } from 'react-i18next';
import { IReduxState, IStore } from '../../app/types';
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants/functions';
import { setLobbyChatActiveState } from '../actions.any';
import { setPrivateMessageRecipient } from '../actions.web';
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../actions.any';
export interface IProps extends WithTranslation {

View File

@@ -1,5 +1,5 @@
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { hideDialog, openDialog } from '../base/dialog/actions';
import AlertDialog from '../base/dialog/components/native/AlertDialog';
import { getParticipantDisplayName } from '../base/participants/functions';
@@ -34,6 +34,38 @@ export function notifyKickedOut(participant: any, submit?: Function) {
};
}
/**
* Notify that we've been kicked out of the conference.
*
* @param {string} reasonKey - The translation key for the reason why the conference failed.
* @param {?Function} submit - The function to execute after submiting the dialog.
* @returns {Function}
*/
export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
return (dispatch: IStore['dispatch']) => {
if (!reasonKey) {
submit?.();
return;
}
// we have to push the opening of the dialog to the queue
// so that we make sure it will be visible after the events
// of conference destroyed are done
setTimeout(() => dispatch(openDialog(AlertDialog, {
contentKey: {
key: reasonKey
},
params: {
},
onSubmit: () => {
submit?.();
dispatch(hideDialog(AlertDialog));
}
})));
};
}
/**
* Dismisses calendar notification about next or ongoing event.
*

View File

@@ -1,41 +1,26 @@
import { IStore } from '../app/types';
import { configureInitialDevices } from '../base/devices/actions.web';
import { getParticipantDisplayName } from '../base/participants/functions';
import { openDialog } from '../base/dialog/actions';
import { getBackendSafeRoomName } from '../base/util/uri';
import { showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
import LeaveReasonDialog from './components/web/LeaveReasonDialog.web';
import logger from './logger';
/**
* Notify that we've been kicked out of the conference.
* Opens {@code LeaveReasonDialog}.
*
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
* instance which initiated the kick event.
* @param {?Function} _ - Used only in native code.
* @returns {Function}
* @param {string} [title] - The dialog title.
*
* @returns {Promise} Resolved when the dialog is closed.
*/
export function notifyKickedOut(participant: any, _?: Function) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!participant || participant?.isReplaced()) {
return;
}
const args = {
participantDisplayName:
getParticipantDisplayName(getState, participant.getId())
};
dispatch(showNotification({
appearance: NOTIFICATION_TYPE.ERROR,
hideErrorSupportLink: true,
descriptionKey: 'dialog.kickMessage',
descriptionArguments: args,
titleKey: 'dialog.kickTitle',
titleArguments: args
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
};
export function openLeaveReasonDialog(title?: string) {
return (dispatch: IStore['dispatch']): Promise<void> => new Promise(resolve => {
dispatch(openDialog(LeaveReasonDialog, {
onClose: resolve,
title
}));
});
}
/**

View File

@@ -36,11 +36,8 @@ import { FILMSTRIP_SIZE } from '../../../filmstrip/constants';
import { isFilmstripVisible } from '../../../filmstrip/functions.native';
import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeInfoContainer';
import LargeVideo from '../../../large-video/components/LargeVideo.native';
import { startKnocking } from '../../../lobby/actions.any';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { shouldEnableAutoKnock } from '../../../mobile/navigation/functions';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
import Captions from '../../../subtitles/components/native/Captions';
@@ -135,11 +132,6 @@ interface IProps extends AbstractProps {
*/
_reducedUI: boolean;
/**
* Indicates if we should auto-knock.
*/
_shouldEnableAutoKnock: boolean;
/**
* Indicates whether the lobby screen should be visible.
*/
@@ -238,17 +230,11 @@ class Conference extends AbstractConference<IProps, State> {
*/
componentDidUpdate(prevProps: IProps) {
const {
_shouldEnableAutoKnock,
_showLobby,
dispatch
_showLobby
} = this.props;
if (!prevProps._showLobby && _showLobby) {
navigate(screen.lobby.root);
if (_shouldEnableAutoKnock) {
dispatch(startKnocking());
}
}
if (prevProps._showLobby && !_showLobby) {
@@ -600,7 +586,6 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
_reducedUI: reducedUI,
_shouldEnableAutoKnock: shouldEnableAutoKnock(state),
_showLobby: getIsLobbyVisible(state),
_startCarMode: startCarMode,
_toolboxVisible: isToolboxVisible(state)

View File

@@ -0,0 +1,62 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import Dialog from '../../../base/ui/components/web/Dialog';
const useStyles = makeStyles()(theme => {
return {
dialog: {
marginBottom: theme.spacing(1)
},
text: {
fontSize: '20px'
}
};
});
/**
* The type of the React {@code Component} props of {@link LeaveReasonDialog}.
*/
interface IProps {
/**
* Callback invoked when {@code LeaveReasonDialog} is unmounted.
*/
onClose: () => void;
/**
* The title to display in the dialog.
*/
title?: string;
}
/**
* A React {@code Component} for displaying a dialog with a reason that ended the conference.
*
* @param {IProps} props - Component's props.
* @returns {JSX}
*/
const LeaveReasonDialog = ({ onClose, title }: IProps) => {
const { classes } = useStyles();
const { t } = useTranslation();
useEffect(() => () => {
onClose?.();
}, []);
return (
<Dialog
cancel = {{ hidden: true }}
onSubmit = { onClose }
size = 'medium'
testId = 'dialog.leaveReason'>
<div className = { classes.dialog }>
{title ? <div className = { classes.text }>{t(title)}</div> : null}
</div>
</Dialog>
);
};
export default LeaveReasonDialog;

View File

@@ -9,10 +9,8 @@ import { IReduxState, IStore } from '../app/types';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
KICKED_OUT
CONFERENCE_LEFT
} from '../base/conference/actionTypes';
import { conferenceLeft } from '../base/conference/actions';
import { getCurrentConference } from '../base/conference/functions';
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
import { hideDialog } from '../base/dialog/actions';
@@ -41,7 +39,7 @@ import { showSalesforceNotification } from '../salesforce/actions';
import { setToolboxEnabled } from '../toolbox/actions.any';
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
import { dismissCalendarNotification, notifyKickedOut } from './actions';
import { dismissCalendarNotification } from './actions';
import { IFRAME_DISABLED_TIMEOUT_MINUTES, IFRAME_EMBED_ALLOWED_LOCATIONS } from './constants';
@@ -64,20 +62,6 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case KICKED_OUT: {
const { dispatch } = store;
dispatch(notifyKickedOut(
action.participant,
() => {
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));
}
));
break;
}
case DISMISS_CALENDAR_NOTIFICATION:
case CONFERENCE_LEFT:
case CONFERENCE_FAILED: {

View File

@@ -1 +1,28 @@
import { appNavigate } from '../app/actions.native';
import { KICKED_OUT } from '../base/conference/actionTypes';
import { conferenceLeft } from '../base/conference/actions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { notifyKickedOut } from './actions.native';
import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case KICKED_OUT: {
const { dispatch } = store;
dispatch(notifyKickedOut(
action.participant,
() => {
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));
}
));
break;
}
}
return next(action);
});

View File

@@ -1,19 +1,35 @@
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import i18next from 'i18next';
import { CONFERENCE_JOINED, KICKED_OUT } from '../base/conference/actionTypes';
import { IJitsiConference } from '../base/conference/reducer';
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';
import { CAMERA_FACING_MODE_MESSAGE } from '../base/tracks/constants';
import './middleware.any';
MiddlewareRegistry.register(_store => next => action => {
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED: {
_addSetCameraFacingModeListener(action.conference);
break;
}
case KICKED_OUT: {
const { dispatch } = store;
const { participant } = action;
const participantDisplayName
= getParticipantDisplayName(store.getState, participant.getId());
dispatch(hangup(true, i18next.t('dialog.kickTitle', { participantDisplayName })));
break;
}
}
return next(action);

View File

@@ -79,10 +79,8 @@ export function submitVideoDeviceSelectionTab(newState: any, isDisplayedOnWelcom
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
}));
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
}
if (newState.localFlipX !== currentState.localFlipX) {
dispatch(updateSettings({
localFlipX: newState.localFlipX

View File

@@ -14,7 +14,7 @@ import {
getDeviceIdByLabel,
groupDevicesByKind
} from '../base/devices/functions.web';
import { isIosMobileBrowser } from '../base/environment/utils';
import { isIosMobileBrowser, isMobileBrowser } from '../base/environment/utils';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import {
@@ -97,7 +97,7 @@ export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDispla
export function getVideoDeviceSelectionDialogProps(stateful: IStateful, isDisplayedOnWelcomePage: boolean) {
// On mobile Safari because of https://bugs.webkit.org/show_bug.cgi?id=179363#c30, the old track is stopped
// by the browser when a new track is created for preview. That's why we are disabling all previews.
const disablePreviews = isIosMobileBrowser();
const disablePreviews = isMobileBrowser();
const state = toState(stateful);
const settings = state['features/base/settings'];

View File

@@ -2,6 +2,7 @@ import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { toState } from '../base/redux/functions';
import logger from './logger';
/**
* Extracts the fqn part from a path, where fqn represents
@@ -64,3 +65,31 @@ export async function getDynamicBrandingUrl(stateful: IStateful) {
export function isDynamicBrandingDataLoaded(state: IReduxState) {
return state['features/dynamic-branding'].customizationReady;
}
/**
* Fetch SVG XMLs from branding icons urls.
*
* @param {Object} customIcons - The map of branded icons.
* @returns {Object}
*/
export const fetchCustomIcons = async (customIcons: Record<string, string>) => {
const localCustomIcons: Record<string, string> = {};
for (const [ key, url ] of Object.entries(customIcons)) {
try {
const response = await fetch(url);
if (response.ok) {
const svgXml = await response.text();
localCustomIcons[key] = svgXml;
} else {
logger.error(`Failed to fetch ${url}. Status: ${response.status}`);
}
} catch (error) {
logger.error(`Error fetching ${url}:`, error);
}
}
return localCustomIcons;
};

View File

@@ -0,0 +1,29 @@
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
import { fetchCustomIcons } from './functions.any';
import logger from './logger';
MiddlewareRegistry.register(() => next => action => {
switch (action.type) {
case SET_DYNAMIC_BRANDING_DATA: {
const { customIcons } = action.value;
if (customIcons) {
fetchCustomIcons(customIcons)
.then(localCustomIcons => {
action.value.brandedIcons = localCustomIcons;
return next(action);
})
.catch((error: any) => {
logger.error('Error fetching branded custom icons:', error);
});
}
break;
}
}
return next(action);
});

View File

@@ -4,6 +4,7 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
import { fetchCustomBrandingData } from './actions.native';
import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
@@ -20,6 +21,7 @@ MiddlewareRegistry.register(store => next => action => {
avatarBackgrounds = [],
backgroundColor,
backgroundImageUrl,
brandedIcons,
didPageUrl,
inviteDomain
} = action.value;
@@ -28,6 +30,7 @@ MiddlewareRegistry.register(store => next => action => {
avatarBackgrounds,
backgroundColor,
backgroundImageUrl,
brandedIcons,
didPageUrl,
inviteDomain
};

View File

@@ -5,6 +5,7 @@ import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
import { fetchCustomBrandingData } from './actions.any';
import { createMuiBrandingTheme } from './functions.web';
import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {

View File

@@ -146,6 +146,7 @@ export interface IDynamicBrandingState {
avatarBackgrounds: string[];
backgroundColor: string;
backgroundImageUrl: string;
brandedIcons?: Record<string, string>;
customizationFailed: boolean;
customizationReady: boolean;
defaultBranding: boolean;
@@ -171,6 +172,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
avatarBackgrounds,
backgroundColor,
backgroundImageUrl,
brandedIcons,
defaultBranding,
didPageUrl,
inviteDomain,
@@ -187,6 +189,7 @@ ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STA
avatarBackgrounds,
backgroundColor,
backgroundImageUrl,
brandedIcons,
defaultBranding,
didPageUrl,
inviteDomain,

View File

@@ -41,14 +41,16 @@ export function cancelFeedback(score: number, message: string) {
* @param {JistiConference} conference - The conference for which the feedback
* would be about. The conference is passed in because feedback can occur after
* a conference has been left, so references to it may no longer exist in redux.
* @param {string} title - The feedback dialog title.
* @returns {Promise} Resolved with value - false if the dialog is enabled and
* resolved with true if the dialog is disabled or the feedback was already
* submitted. Rejected if another dialog is already displayed.
*/
export function maybeOpenFeedbackDialog(conference: IJitsiConference) {
export function maybeOpenFeedbackDialog(conference: IJitsiConference, title?: string) {
type R = {
feedbackSubmitted: boolean;
showThankYou: boolean;
wasDialogShown: boolean;
};
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<R> => {
@@ -67,16 +69,18 @@ export function maybeOpenFeedbackDialog(conference: IJitsiConference) {
return Promise.resolve({
feedbackSubmitted: true,
showThankYou: true
showThankYou: true,
wasDialogShown: false
});
} else if (conference.isCallstatsEnabled() && feedbackPercentage > Math.random() * 100) {
return new Promise(resolve => {
dispatch(openFeedbackDialog(conference, () => {
dispatch(openFeedbackDialog(conference, title, () => {
const { submitted } = getState()['features/feedback'];
resolve({
feedbackSubmitted: submitted,
showThankYou: false
showThankYou: false,
wasDialogShown: true
});
}));
});
@@ -87,7 +91,8 @@ export function maybeOpenFeedbackDialog(conference: IJitsiConference) {
// act on it.
return Promise.resolve({
feedbackSubmitted: false,
showThankYou: true
showThankYou: true,
wasDialogShown: false
});
};
}
@@ -98,14 +103,16 @@ export function maybeOpenFeedbackDialog(conference: IJitsiConference) {
* @param {JitsiConference} conference - The JitsiConference that is being
* rated. The conference is passed in because feedback can occur after a
* conference has been left, so references to it may no longer exist in redux.
* @param {string} [title] - The feedback dialog title.
* @param {Function} [onClose] - An optional callback to invoke when the dialog
* is closed.
* @returns {Object}
*/
export function openFeedbackDialog(conference?: IJitsiConference, onClose?: Function) {
export function openFeedbackDialog(conference?: IJitsiConference, title?: string, onClose?: Function) {
return openDialog(FeedbackDialog, {
conference,
onClose
onClose,
title
});
}

View File

@@ -60,6 +60,10 @@ const useStyles = makeStyles()(theme => {
}
},
title: {
fontSize: '16px'
},
details: {
'& textarea': {
minHeight: '122px'
@@ -98,6 +102,11 @@ interface IProps {
* Callback invoked when {@code FeedbackDialog} is unmounted.
*/
onClose: Function;
/**
* The title to display in the dialog. Usually the reason that triggered the feedback.
*/
title?: string;
}
/**
@@ -108,7 +117,7 @@ interface IProps {
* @param {IProps} props - Component's props.
* @returns {JSX}
*/
const FeedbackDialog = ({ conference, onClose }: IProps) => {
const FeedbackDialog = ({ conference, onClose, title }: IProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -284,6 +293,7 @@ const FeedbackDialog = ({ conference, onClose }: IProps) => {
size = 'large'
titleKey = 'feedback.rateExperience'>
<div className = { classes.dialog }>
{title ? <div className = { classes.title }>{t(title)}</div> : null}
<div className = { classes.rating }>
<div
className = { classes.stars }

View File

@@ -43,6 +43,7 @@ import {
isStageFilmstripTopPanel,
shouldRemoteVideosBeVisible
} from '../../functions';
import { isFilmstripDisabled } from '../../functions.web';
import AudioTracksContainer from './AudioTracksContainer';
import Thumbnail from './Thumbnail';
@@ -74,6 +75,11 @@ interface IProps extends WithTranslation {
*/
_disableSelfView: boolean;
/**
* Whether vertical/horizontal filmstrip is disabled through config.
*/
_filmstripDisabled: boolean;
/**
* The height of the filmstrip.
*/
@@ -333,6 +339,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
const {
_currentLayout,
_disableSelfView,
_filmstripDisabled,
_localScreenShareId,
_mainFilmstripVisible,
_resizableFilmstrip,
@@ -381,7 +388,8 @@ class Filmstrip extends PureComponent <IProps, IState> {
let toolbar = null;
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
&& _currentLayout !== LAYOUTS.TILE_VIEW && (filmstripType === FILMSTRIP_TYPE.MAIN
&& _currentLayout !== LAYOUTS.TILE_VIEW
&& ((filmstripType === FILMSTRIP_TYPE.MAIN && !_filmstripDisabled)
|| (filmstripType === FILMSTRIP_TYPE.STAGE && _topPanelFilmstrip))) {
toolbar = this._renderToggleButton();
}
@@ -885,6 +893,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const { isOpen: shiftRight } = state['features/chat'];
const disableSelfView = getHideSelfView(state);
const { clientWidth, clientHeight } = state['features/base/responsive-ui'];
const filmstripDisabled = isFilmstripDisabled(state);
const collapseTileView = reduceHeight
&& isMobileBrowser()
@@ -893,7 +902,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const shouldReduceHeight = reduceHeight && isMobileBrowser();
const _topPanelVisible = isStageFilmstripTopPanel(state) && topPanelVisible;
let isVisible = visible || filmstripType !== FILMSTRIP_TYPE.MAIN;
const notDisabled = visible && !filmstripDisabled;
let isVisible = notDisabled || filmstripType !== FILMSTRIP_TYPE.MAIN;
if (_topPanelFilmstrip) {
isVisible = _topPanelVisible;
@@ -912,13 +922,14 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_chatOpen: state['features/chat'].isOpen,
_currentLayout,
_disableSelfView: disableSelfView,
_filmstripDisabled: filmstripDisabled,
_hasScroll,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip,
_localScreenShareId: localScreenShare?.id,
_mainFilmstripVisible: visible,
_mainFilmstripVisible: notDisabled,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
_remoteParticipantsLength: _remoteParticipants?.length ?? 0,

View File

@@ -763,6 +763,18 @@ export function isStageFilmstripEnabled(state: IReduxState) {
return Boolean(!filmstrip?.disableStageFilmstrip && interfaceConfig.VERTICAL_FILMSTRIP);
}
/**
* Whether the vertical/horizontal filmstrip is disabled.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isFilmstripDisabled(state: IReduxState) {
const { filmstrip } = state['features/base/config'];
return Boolean(filmstrip?.disabled);
}
/**
* Gets the thumbnail type by filmstrip type.
*

View File

@@ -120,7 +120,7 @@ MiddlewareRegistry.register(store => next => action => {
case SETTINGS_UPDATED: {
if (typeof action.settings?.localFlipX === 'boolean') {
// TODO: This needs to be removed once the large video is Reactified.
VideoLayout.onLocalFlipXChanged();
VideoLayout.onLocalFlipXChanged(action.settings.localFlipX);
}
if (action.settings?.disableSelfView) {
const state = store.getState();

View File

@@ -116,6 +116,6 @@ export default {
},
sendBtn: {
marginRight: BaseTheme.spacing[2]
marginRight: BaseTheme.spacing[3]
}
};

View File

@@ -445,7 +445,7 @@ export function _mapStateToProps(state: IReduxState) {
const { disableLobbyPassword } = getSecurityUiConfig(state);
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
const deviceStatusVisible = isDeviceStatusVisible(state);
const { membersOnly } = state['features/base/conference'];
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
const { isLobbyChatActive, lobbyMessageRecipient, messages } = state['features/chat'];
return {
@@ -461,7 +461,7 @@ export function _mapStateToProps(state: IReduxState) {
_participantId: participantId,
_participantName: localParticipant?.name,
_passwordJoinFailed: passwordJoinFailed,
_renderPassword: !iAmSipGateway && !disableLobbyPassword,
_renderPassword: !iAmSipGateway && !disableLobbyPassword && !lobbyWaitingForHost,
showCopyUrlButton
};
}

View File

@@ -160,7 +160,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
customStyles = {{ input: styles.customInput }}
error = { _passwordJoinFailed }
onChange = { this._onChangePassword }
placeholder = { t('lobby.passwordField') }
placeholder = { t('lobby.enterPasswordButton') }
secureTextEntry = { true }
value = { this.state.password } />
);

View File

@@ -189,7 +189,7 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
className = { `lobby-prejoin-input ${_passwordJoinFailed ? 'error' : ''}` }
id = 'lobby-password-input'
onChange = { this._onChangePassword }
placeholder = { t('lobby.passwordField') }
placeholder = { t('lobby.enterPasswordButton') }
testId = 'lobby.password'
type = 'password'
value = { this.state.password } />

View File

@@ -0,0 +1,22 @@
import { IReduxState } from '../../app/types';
import { getCurrentConference } from '../../base/conference/functions';
import { isAnyDialogOpen } from '../../base/dialog/functions';
import { FULLSCREEN_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { isLocalVideoTrackDesktop } from '../../base/tracks/functions.any';
/**
* Checks whether full-screen state should be used or not.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Whether full-screen state shuld be used or not.
*/
export function shouldUseFullScreen(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state);
const dialogOpen = isAnyDialogOpen(state);
const fullscreenEnabled = getFeatureFlag(state, FULLSCREEN_ENABLED, true);
const isDesktopSharing = isLocalVideoTrackDesktop(state);
return conference ? !audioOnly && !dialogOpen && !isDesktopSharing && fullscreenEnabled : false;
}

View File

@@ -2,14 +2,11 @@ import ImmersiveMode from 'react-native-immersive-mode';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
import { getCurrentConference } from '../../base/conference/functions';
import { isAnyDialogOpen } from '../../base/dialog/functions';
import { FULLSCREEN_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
import { _setImmersiveSubscription } from './actions';
import { shouldUseFullScreen } from './functions';
import logger from './logger';
type BarVisibilityType = {
@@ -48,14 +45,7 @@ MiddlewareRegistry.register(store => next => action => {
});
StateListenerRegistry.register(
/* selector */ state => {
const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state);
const dialogOpen = isAnyDialogOpen(state);
const fullscreenEnabled = getFeatureFlag(state, FULLSCREEN_ENABLED, true);
return conference ? !audioOnly && !dialogOpen && fullscreenEnabled : false;
},
/* selector */ shouldUseFullScreen,
/* listener */ fullScreen => _setFullScreen(fullScreen)
);
@@ -73,13 +63,7 @@ function _onImmersiveChange({ getState }: IStore) {
const { appState } = state['features/background'];
if (appState === 'active') {
const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state);
const dialogOpen = isAnyDialogOpen(state);
const fullscreenEnabled = getFeatureFlag(state, FULLSCREEN_ENABLED, true);
const fullScreen = conference ? !audioOnly && !dialogOpen && fullscreenEnabled : false;
_setFullScreen(fullScreen);
_setFullScreen(shouldUseFullScreen(state));
}
}

View File

@@ -74,7 +74,7 @@ const HeaderNavigationButton = ({ color, disabled, label, onPress, src, style, t
size = { 24 }
src = { src }
style = { [
navigationStyles.headerNavigationButton,
navigationStyles.headerNavigationButtonIcon,
style
] } />
) : (
@@ -87,8 +87,7 @@ const HeaderNavigationButton = ({ color, disabled, label, onPress, src, style, t
btnStyle,
style
] }
type = { BUTTON_TYPES.TERTIARY }
useRippleColor = { false } />
type = { BUTTON_TYPES.TERTIARY } />
)}
</>
);

View File

@@ -17,6 +17,12 @@ const headerNavigationButtonLabel = {
lineHeight: BaseTheme.spacing[3]
};
const headerNavigationButton = {
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[6],
marginLeft: BaseTheme.spacing[3]
};
/**
* Styles of the navigation feature.
*/
@@ -43,7 +49,12 @@ export const navigationStyles = {
},
headerNavigationButton: {
marginLeft: BaseTheme.spacing[2]
...headerNavigationButton
},
headerNavigationButtonIcon: {
...headerNavigationButton,
padding: BaseTheme.spacing[2]
},
headerNavigationButtonDisabled: {

View File

@@ -20,10 +20,10 @@ export default function BreakoutRoomNamePrompt({ breakoutRoomJid, initialRoomNam
const okDisabled = !roomName;
const dispatch = useDispatch();
const onBreakoutRoomNameChange = useCallback((newRoomName: string) => {
setRoomName(newRoomName?.trim());
setRoomName(newRoomName);
}, [ setRoomName ]);
const onSubmit = useCallback(() => {
dispatch(renameBreakoutRoom(breakoutRoomJid, roomName));
dispatch(renameBreakoutRoom(breakoutRoomJid, roomName?.trim()));
}, [ breakoutRoomJid, dispatch, roomName ]);
return (<Dialog

View File

@@ -85,7 +85,8 @@ export default {
position: 'absolute',
right: -3,
top: -3,
textAlign: 'center'
textAlign: 'center',
paddingHorizontal: 2
},
participantsButtonBadge: {

View File

@@ -30,6 +30,33 @@ import MeetingParticipants from './MeetingParticipants';
const useStyles = makeStyles()(theme => {
return {
participantsPane: {
backgroundColor: theme.palette.ui01,
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
transition: 'width .16s ease-in-out',
width: '315px',
zIndex: 0,
display: 'flex',
flexDirection: 'column',
fontWeight: 600,
height: '100%',
[[ '& > *:first-child', '& > *:last-child' ] as any]: {
flexShrink: 0
},
'@media (max-width: 580px)': {
height: '100vh',
position: 'fixed',
left: 0,
right: 0,
top: 0,
width: '100%'
}
},
container: {
boxSizing: 'border-box',
flex: 1,
@@ -87,7 +114,7 @@ const useStyles = makeStyles()(theme => {
});
const ParticipantsPane = () => {
const { classes } = useStyles();
const { classes, cx } = useStyles();
const paneOpen = useSelector(getParticipantsPaneOpen);
const isBreakoutRoomsSupported = useSelector((state: IReduxState) => state['features/base/conference'])
.conference?.getBreakoutRooms()?.isSupported();
@@ -136,49 +163,47 @@ const ParticipantsPane = () => {
}
return (
<div className = 'participants_pane'>
<div className = 'participants_pane-content'>
<div className = { classes.header }>
<ClickableIcon
accessibilityLabel = { t('participantsPane.close', 'Close') }
icon = { IconCloseLarge }
onClick = { onClosePane } />
</div>
<div className = { classes.container }>
<LobbyParticipants />
<br className = { classes.antiCollapse } />
<MeetingParticipants
searchString = { searchString }
setSearchString = { setSearchString } />
{isBreakoutRoomsSupported && <RoomList searchString = { searchString } />}
{showAddRoomButton && <AddBreakoutRoomButton />}
</div>
{showFooter && (
<div className = { classes.footer }>
{showMuteAllButton && (
<Button
accessibilityLabel = { t('participantsPane.actions.muteAll') }
labelKey = { 'participantsPane.actions.muteAll' }
onClick = { onMuteAll }
type = { BUTTON_TYPES.SECONDARY } />
)}
{showMoreActionsButton && (
<div className = { classes.footerMoreContainer }>
<Button
accessibilityLabel = { t('participantsPane.actions.moreModerationActions') }
icon = { IconDotsHorizontal }
id = 'participants-pane-context-menu'
onClick = { onToggleContext }
type = { BUTTON_TYPES.SECONDARY } />
<FooterContextMenu
isOpen = { contextOpen }
onDrawerClose = { onDrawerClose }
onMouseLeave = { onToggleContext } />
</div>
)}
</div>
)}
<div className = { cx('participants_pane', classes.participantsPane) }>
<div className = { classes.header }>
<ClickableIcon
accessibilityLabel = { t('participantsPane.close', 'Close') }
icon = { IconCloseLarge }
onClick = { onClosePane } />
</div>
<div className = { classes.container }>
<LobbyParticipants />
<br className = { classes.antiCollapse } />
<MeetingParticipants
searchString = { searchString }
setSearchString = { setSearchString } />
{isBreakoutRoomsSupported && <RoomList searchString = { searchString } />}
{showAddRoomButton && <AddBreakoutRoomButton />}
</div>
{showFooter && (
<div className = { classes.footer }>
{showMuteAllButton && (
<Button
accessibilityLabel = { t('participantsPane.actions.muteAll') }
labelKey = { 'participantsPane.actions.muteAll' }
onClick = { onMuteAll }
type = { BUTTON_TYPES.SECONDARY } />
)}
{showMoreActionsButton && (
<div className = { classes.footerMoreContainer }>
<Button
accessibilityLabel = { t('participantsPane.actions.moreModerationActions') }
icon = { IconDotsHorizontal }
id = 'participants-pane-context-menu'
onClick = { onToggleContext }
type = { BUTTON_TYPES.SECONDARY } />
<FooterContextMenu
isOpen = { contextOpen }
onDrawerClose = { onDrawerClose }
onMouseLeave = { onToggleContext } />
</div>
)}
</div>
)}
</div>
);
};

View File

@@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react'
import { useTranslation } from 'react-i18next';
import {
BackHandler,
Platform,
StyleProp,
Text,
TextStyle,
@@ -19,7 +18,6 @@ import { getConferenceName } from '../../../base/conference/functions';
import { connect } from '../../../base/connection/actions.native';
import { PREJOIN_PAGE_HIDE_DISPLAY_NAME } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { IconCloseLarge } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { getLocalParticipant } from '../../../base/participants/functions';
import { getFieldValue } from '../../../base/react/functions';
@@ -30,7 +28,7 @@ import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import BrandingImageBackground from '../../../dynamic-branding/components/native/BrandingImageBackground';
import LargeVideo from '../../../large-video/components/LargeVideo.native';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
import { screenHeaderCloseButton } from '../../../mobile/navigation/functions';
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import AudioMuteButton from '../../../toolbox/components/native/AudioMuteButton';
@@ -40,6 +38,7 @@ import { IPrejoinProps } from '../../types';
import { preJoinStyles as styles } from './styles';
const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
const dispatch = useDispatch();
const isFocused = useIsFocused();
@@ -83,22 +82,6 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
return true;
}, [ dispatch ]);
const headerLeft = () => {
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconCloseLarge } />
);
};
const { PRIMARY, TERTIARY } = BUTTON_TYPES;
const joinButtonDisabled = !displayName && isDisplayNameMandatory;
@@ -111,7 +94,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
useLayoutEffect(() => {
navigation.setOptions({
headerLeft,
headerLeft: () => screenHeaderCloseButton(goBack),
headerTitle: t('prejoin.joinMeeting')
});
}, [ navigation ]);

View File

@@ -1,25 +1,26 @@
import React, { useCallback, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform, StyleProp, Text, TextStyle, View, ViewStyle } from 'react-native';
import { StyleProp, Text, TextStyle, View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { appNavigate } from '../../../app/actions.native';
import { IReduxState } from '../../../app/types';
import { getConferenceName } from '../../../base/conference/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge, IconWarning } from '../../../base/icons/svg';
import { IconWarning } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import getUnsafeRoomText from '../../../base/util/getUnsafeRoomText.native';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
import { screenHeaderCloseButton } from '../../../mobile/navigation/functions';
import { navigateRoot } from '../../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { IPrejoinProps } from '../../types';
import { preJoinStyles as styles } from './styles';
const UnsafeRoomWarning: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -41,25 +42,9 @@ const UnsafeRoomWarning: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProp
return true;
}, [ dispatch ]);
const headerLeft = () => {
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
onPress = { goBack } />
);
}
return (
<HeaderNavigationButton
onPress = { goBack }
src = { IconCloseLarge } />
);
};
useLayoutEffect(() => {
navigation.setOptions({
headerLeft,
headerLeft: () => screenHeaderCloseButton(goBack),
headerTitle: t('prejoin.joinMeeting')
});
}, [ navigation ]);

View File

@@ -476,7 +476,11 @@ export function endpointMessageReceived(participantId: string, message: {
if (type === EVENTS.stop) {
dispatch(stopReceiver(false, true));
} else { // forward the message
transport?.sendEvent(message);
try {
transport?.sendEvent(message);
} catch (error) {
logger.error('Error while trying to execute remote control message', error);
}
}
} // else ignore
} else {

View File

@@ -5,166 +5,40 @@ import {
PC_STATE_FAILED
// @ts-expect-error
} from '@jitsi/rtcstats/events';
// @ts-expect-error
import rtcstatsInit from '@jitsi/rtcstats/rtcstats';
// @ts-expect-error
import traceInit from '@jitsi/rtcstats/trace-ws';
/* eslint-enable lines-around-comment */
import { createRTCStatsTraceCloseEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import JitsiMeetJS, { RTCStatsEvents } from '../base/lib-jitsi-meet';
import logger from './logger';
import {
DominantSpeakerData,
E2ERTTData,
FaceLandmarksData,
InitOptions,
VideoTypeData
} from './types';
/**
* Filter out RTCPeerConnection that are created by callstats.io.
*
* @param {*} config - Config object sent to the PC c'tor.
* @returns {boolean}
*/
function connectionFilter(config: any) {
if (config?.iceServers[0] && config.iceServers[0].urls) {
for (const iceUrl of config.iceServers[0].urls) {
if (iceUrl.indexOf('callstats.io') >= 0) {
return true;
}
}
}
}
/**
* Class that controls the rtcstats flow, because it overwrites and proxies global function it should only be
* initialized once.
* Handle lib-jitsi-meet rtcstats events and send jitsi-meet specific statistics.
*/
class RTCStats {
trace: any;
options?: InitOptions;
isPeerConnectionWrapped = false;
connStateEvents: any = [];
private _connStateEvents: Array<any> = [];
private _initialized = false;
/**
* Initialize the rtcstats components, if the options have changed.
* Handles rtcstats events.
*
* @param {Oject} newOptions -.
* @returns {void}
*/
maybeInit(newOptions: InitOptions) {
const oldOptions = this.options;
const changed = !oldOptions || (oldOptions.endpoint !== newOptions.endpoint
|| oldOptions.meetingFqn !== newOptions.meetingFqn
|| oldOptions.pollInterval !== newOptions.pollInterval
|| oldOptions.sendSdp !== newOptions.sendSdp
|| oldOptions.useLegacy !== newOptions.useLegacy);
init() {
this._connStateEvents = [];
if (changed) {
this.reset();
if (newOptions.meetingFqn && newOptions.endpoint) {
this.init(newOptions);
} else {
logger.warn('RTCStats is enabled but it has not been configured.');
}
if (!this._initialized) {
JitsiMeetJS.rtcstats.on(
RTCStatsEvents.RTC_STATS_PC_EVENT,
(pcEvent: any) => this.handleRTCStatsEvent(pcEvent));
this._initialized = true;
}
}
/**
* Wrapper method for the underlying trace object to be used as a static reference from inside the wrapped
* PeerConnection.
*
* @param {any[]} data - The stats entry to send to the rtcstats endpoint.
* @returns {void}
*/
statsEntry(...data: any[]) {
this.trace?.statsEntry(...data);
}
/**
* Initialize the rtcstats components. First off we initialize the trace, which is a wrapped websocket
* that does the actual communication with the server. Secondly, the rtcstats component is initialized,
* it overwrites GUM and PeerConnection global functions and adds a proxy over them used to capture stats.
* Note, lib-jitsi-meet takes references to these methods before initializing so the init method needs to be
* loaded before it does.
*
* @param {Object} options -.
* @param {string} options.endpoint - The Amplitude app key required.
* @param {string} options.meetingFqn - The meeting fqn.
* @param {boolean} options.useLegacy - Switch to legacy chrome webrtc statistics. Parameter will only have
* an effect on chrome based applications.
* @param {number} options.pollInterval - The getstats poll interval in ms.
* @param {boolean} options.sendSdp - Determines if the client sends SDP to the rtcstats server.
* @returns {void}
*/
init(options: InitOptions) {
const { endpoint, meetingFqn, useLegacy, pollInterval, sendSdp } = options;
const traceOptions = {
endpoint,
meetingFqn,
onCloseCallback: this.handleTraceWSClose.bind(this),
useLegacy
};
this.trace = traceInit(traceOptions);
if (!this.isPeerConnectionWrapped) {
const rtcstatsOptions = {
connectionFilter,
pollInterval,
useLegacy,
sendSdp,
eventCallback: this.handleRtcstatsEvent.bind(this)
};
const statsEntryCallback = { statsEntry: this.statsEntry.bind(this) };
rtcstatsInit(statsEntryCallback, rtcstatsOptions);
this.isPeerConnectionWrapped = true;
}
this.options = options;
}
/**
* Check whether or not the RTCStats is initialized.
*
* @returns {boolean}
*/
isInitialized() {
return this.options !== undefined;
}
/**
* Resets the rtcstats.
*
* @returns {void}
*/
reset() {
delete this.options;
if (this.trace) {
this.trace.close();
delete this.trace;
}
}
/**
* Send identity data to rtcstats server, this will be reflected in the identity section of the stats dump.
* It can be generally used to send additional metadata that might be relevant such as amplitude user data
* or deployment specific information.
*
* @param {any} identityData - Metadata object to send as identity.
* @returns {void}
*/
sendIdentityData(identityData: any) {
this.trace?.identity('identity', null, identityData);
}
/**
* Send console logs to rtcstats server.
*
@@ -172,7 +46,7 @@ class RTCStats {
* @returns {void}
*/
sendLogs(logEntries: Array<string | any>) {
this.trace?.statsEntry('logs', null, logEntries);
JitsiMeetJS.rtcstats.sendStatsEntry('logs', logEntries);
}
/**
@@ -182,7 +56,7 @@ class RTCStats {
* @returns {void}
*/
sendDominantSpeakerData(dominantSpeakerData: DominantSpeakerData) {
this.trace?.statsEntry('dominantSpeaker', null, dominantSpeakerData);
JitsiMeetJS.rtcstats.sendStatsEntry('dominantSpeaker', dominantSpeakerData);
}
/**
@@ -191,8 +65,8 @@ class RTCStats {
* @param {Object} e2eRttData - The object that holds the e2e data.
* @returns {void}
*/
sendE2eRttData(e2eRttData: E2ERTTData) {
this.trace?.statsEntry('e2eRtt', null, e2eRttData);
sendE2ERTTData(e2eRttData: E2ERTTData) {
JitsiMeetJS.rtcstats.sendStatsEntry('e2eRtt', e2eRttData);
}
/**
@@ -203,7 +77,7 @@ class RTCStats {
* @returns {void}
*/
sendConferenceTimestamp(timestamp: number) {
this.trace?.statsEntry('conferenceStartTimestamp', null, timestamp);
JitsiMeetJS.rtcstats.sendStatsEntry('conferenceStartTimestamp', timestamp);
}
/**
@@ -213,7 +87,7 @@ class RTCStats {
* @returns {void}
*/
sendVideoTypeData(videoTypeData: VideoTypeData) {
this.trace?.statsEntry('setVideoType', null, videoTypeData);
JitsiMeetJS.rtcstats.sendStatsEntry('setVideoType', videoTypeData);
}
/**
@@ -223,53 +97,32 @@ class RTCStats {
* @returns {void}
*/
sendFaceLandmarksData(faceLandmarksData: FaceLandmarksData) {
this.trace?.statsEntry('faceLandmarks', null, faceLandmarksData);
}
/**
* Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
* connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
* connected and sent once it is established.
*
* @param {boolean} isBreakoutRoom - Flag indicating if the user is in a breakout room.
* @returns {void}
*/
connect(isBreakoutRoom: boolean) {
this.trace?.connect(isBreakoutRoom);
}
/**
* Self explanatory; closes the web socked connection.
* Note, at the point of writing this documentation there was no method to reset the function overwrites,
* thus even if the websocket is closed the global function proxies are still active but send no data,
* this shouldn't influence the normal flow of the application.
*
* @returns {void}
*/
close() {
this.trace?.close();
JitsiMeetJS.rtcstats.sendStatsEntry('faceLandmarks', faceLandmarksData);
}
/**
* RTCStats client can notify the APP of any PeerConnection related event that occurs.
*
* @param {Object} event - Rtcstats event.
* @param {Object} event - The PeerConnection event.
* @param {string} event.type - The event type.
* @param {Object} event.body - Event body.
* @param {string} event.body.isP2P - PeerConnection type.
* @param {string} event.body.state - PeerConnection state change which triggered the event.
* @returns {void}
*/
handleRtcstatsEvent(event: any) {
handleRTCStatsEvent(event: any) {
switch (event.type) {
case PC_CON_STATE_CHANGE: {
const { body: { isP2P, state } = { state: null,
isP2P: null } } = event;
const { body: { isP2P = null, state = null } } = event;
this.connStateEvents.push(event.body);
this._connStateEvents.push(event.body);
// We only report PC related connection issues. If the rtcstats websocket is not connected at this point
// it usually means that none of our services can be reached i.e. there's problem with the internet
// connection and not necessarily with reaching the JVB (due to a firewall or other reasons).
if (state === PC_STATE_FAILED && this.trace.isConnected()) {
if (state === PC_STATE_FAILED) {
const connectionType = isP2P ? 'P2P' : 'JVB';
const wasConnected = this.connStateEvents.some((connectionEvent: { isP2P: any; state: string; }) =>
const wasConnected = this._connStateEvents.some((connectionEvent: { isP2P: any; state: string; }) =>
(connectionEvent.isP2P === isP2P) && (connectionEvent.state === PC_STATE_CONNECTED));
logger.info(`${connectionType} PeerConnection failed, previously connected: ${wasConnected}`);
@@ -283,21 +136,6 @@ class RTCStats {
}
}
}
/**
* The way rtcstats is currently designed the ws wouldn't normally be closed by the application logic but rather
* by the page being closed/reloaded. Using this assumption any onclose event is most likely something abnormal
* that happened on the ws. We then track this in order to determine how many rtcstats connection were closed
* prematurely.
*
* @param {Object} closeEvent - Event sent by ws onclose.
* @returns {void}
*/
handleTraceWSClose(closeEvent: any) {
logger.info('RTCStats trace ws closed', closeEvent);
sendAnalytics(createRTCStatsTraceCloseEvent(closeEvent));
}
}
export default new RTCStats();

View File

@@ -1,112 +1,29 @@
// @ts-expect-error
import { jitsiLocalStorage } from '@jitsi/js-utils';
import { getAmplitudeIdentity } from '../analytics/functions';
import { IStore } from '../app/types';
import { IStateful } from '../base/app/types';
import { getAnalyticsRoomName, getConferenceOptions } from '../base/conference/functions';
import { getLocalParticipant } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import RTCStats from './RTCStats';
import logger from './logger';
/**
* Checks whether rtcstats is enabled or not.
*
* @param {IStateful} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function isRtcstatsEnabled(stateful: IStateful) {
export function isRTCStatsEnabled(stateful: IStateful) {
const state = toState(stateful);
const { analytics } = state['features/base/config'];
return analytics?.rtcstatsEnabled ?? false;
}
/**
* Can the rtcstats service send data.
*
* @param {IStateful} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function canSendRtcstatsData(stateful: IStateful) {
return isRtcstatsEnabled(stateful) && RTCStats.isInitialized();
}
type Identity = {
isBreakoutRoom: boolean;
// Unique identifier for a conference session, not to be confused with meeting name
// i.e. If all participants leave a meeting it will have a different value on the next join.
meetingUniqueId?: string;
roomId?: string;
};
/**
* Connects to the rtcstats service and sends the identity data.
*
* @param {IStore} store - Redux Store.
* @param {Identity} identity - Identity data for the client.
* @returns {void}
*/
export function connectAndSendIdentity({ getState, dispatch }: IStore, identity: Identity) {
const state = getState();
if (canSendRtcstatsData(state)) {
// Once the conference started connect to the rtcstats server and send data.
try {
RTCStats.connect(identity.isBreakoutRoom);
const localParticipant = getLocalParticipant(state);
const options = getConferenceOptions(state);
// The current implementation of rtcstats-server is configured to send data to amplitude, thus
// we add identity specific information so we can correlate on the amplitude side. If amplitude is
// not configured an empty object will be sent.
// The current configuration of the conference is also sent as metadata to rtcstats server.
// This is done in order to facilitate queries based on different conference configurations.
// e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
// conference with a specific version.
let displayName = jitsiLocalStorage.getItem('callStatsUserName');
if (options.statisticsId || options.statisticsDisplayName) {
if (options.statisticsId && options.statisticsDisplayName) {
displayName = `${options.statisticsDisplayName} (${options.statisticsId})`;
} else {
displayName = options.statisticsId || options.statisticsDisplayName;
}
}
RTCStats.sendIdentityData({
...getAmplitudeIdentity(),
...options,
endpointId: localParticipant?.id,
confName: getAnalyticsRoomName(state, dispatch),
displayName,
...identity
});
} catch (error) {
// If the connection failed do not impact jitsi-meet just silently fail.
logger.error('RTCStats connect failed with: ', error);
}
}
}
/**
* Checks if the faceLandmarks data can be sent to the rtcstats server.
*
* @param {IStateful} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function canSendFaceLandmarksRtcstatsData(stateful: IStateful): boolean {
export function canSendFaceLandmarksRTCStatsData(stateful: IStateful): boolean {
const state = toState(stateful);
const { faceLandmarks } = state['features/base/config'];
if (faceLandmarks?.enableRTCStats && canSendRtcstatsData(state)) {
return true;
}
return false;
return Boolean(faceLandmarks?.enableRTCStats && isRTCStatsEnabled(state));
}

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