Compare commits

...

39 Commits

Author SHA1 Message Date
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
85 changed files with 3081 additions and 1783 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

@@ -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

@@ -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,
@@ -1572,6 +1576,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

@@ -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

@@ -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);
}
},

1765
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/v1681.0.0+6cd397fa/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",

File diff suppressed because it is too large Load Diff

View File

@@ -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/v1681.0.0+6cd397fa/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"
@@ -98,4 +96,4 @@
"keywords": [
"react-native"
]
}
}

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

@@ -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

@@ -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;
@@ -162,6 +163,10 @@ export interface IJitsiConferenceRoom {
roomjid: string;
}
interface IConferenceFailedError extends Error {
params: Array<any>;
}
/**
* Listen for actions that contain the conference object, so that it can be
* stored for use by other action creators.
@@ -274,7 +279,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 +291,7 @@ function _conferenceFailed(state: IConferenceState, { conference, error }: {
let authRequired;
let membersOnly;
let passwordRequired;
let lobbyWaitingForHost;
switch (error.name) {
case JitsiConferenceErrors.AUTHENTICATION_REQUIRED:
@@ -293,9 +299,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 +322,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 +379,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,7 +1,7 @@
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 JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import {
@@ -230,6 +230,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 +301,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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -11,7 +11,7 @@ import {
import { hangup } from '../base/connection/actions.web';
import { openDialog } from '../base/dialog/actions';
import i18next from '../base/i18n/i18next';
import browser from '../base/lib-jitsi-meet';
import { browser } from '../base/lib-jitsi-meet';
import { updateSettings } from '../base/settings/actions';
import { getLocalVideoTrack } from '../base/tracks/functions.web';
import { appendURLHashParam } from '../base/util/uri';
@@ -65,6 +65,8 @@ export function openLogoutDialog() {
} else {
if (logoutUrl) {
window.location.href = logoutUrl;
return;
}
conference?.room.moderator.logout(() => dispatch(hangup(true)));

View File

@@ -64,17 +64,17 @@ const GeneralSection = () => {
</FormRow>}
<FormRow label = 'settings.language'>
<TouchableHighlight onPress = { navigateToLanguageSelect }>
<View style = { styles.languageButton as ViewStyle }>
<Text
style = { styles.languageText }>{t(`languages:${language}`)}</Text>
<View>
<View style = { styles.languageButtonContainer as ViewStyle }>
<TouchableHighlight onPress = { navigateToLanguageSelect }>
<View style = { styles.languageButton as ViewStyle }>
<Text
style = { styles.languageText }>{t(`languages:${language}`)}</Text>
<Icon
size = { 24 }
src = { IconArrowRight } />
</View>
</View>
</TouchableHighlight>
</TouchableHighlight>
</View>
</FormRow>
</FormSection>
);

View File

@@ -1,16 +1,21 @@
import React, { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import React, { useCallback, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../../../base/i18n/i18next';
import { IconArrowLeft } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
import { goBack, navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import styles from './styles';
const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
const { t } = useTranslation();
const navigation = useNavigation();
const { language: currentLanguage = DEFAULT_LANGUAGE } = i18next;
const setLanguage = useCallback(language => () => {
@@ -18,6 +23,21 @@ const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; })
navigate(screen.settings.main);
}, [ i18next ]);
const headerLeft = () => (
<HeaderNavigationButton
color = { BaseThemeNative.palette.link01 }
onPress = { goBack }
src = { IconArrowLeft }
style = { styles.backBtn }
twoActions = { true } />
);
useLayoutEffect(() => {
navigation.setOptions({
headerLeft
});
}, [ navigation ]);
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }

View File

@@ -73,8 +73,9 @@ export default {
fieldContainer: {
alignItems: 'center',
flexDirection: 'row',
minHeight: 56,
paddingHorizontal: 8
minHeight: BaseTheme.spacing[8],
paddingHorizontal: BaseTheme.spacing[2],
justifyContent: 'space-between'
},
/**
@@ -90,9 +91,10 @@ export default {
*/
fieldLabelContainer: {
alignItems: 'center',
flexShrink: 1,
flexDirection: 'row',
flex: 3,
paddingLeft: BaseTheme.spacing[3]
paddingLeft: BaseTheme.spacing[3],
paddingRight: BaseTheme.spacing[1]
},
/**
@@ -118,8 +120,8 @@ export default {
*/
fieldValueContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
flexShrink: 1,
justifyContent: 'flex-end',
paddingRight: BaseTheme.spacing[3]
},
@@ -156,9 +158,17 @@ export default {
marginTop: BaseTheme.spacing[2]
},
languageButtonContainer: {
borderRadius: BaseTheme.shape.borderRadius,
overflow: 'hidden'
},
languageButton: {
alignItems: 'center',
display: 'flex',
flexDirection: 'row'
flexDirection: 'row',
height: BaseTheme.spacing[7],
justifyContent: 'center'
},
languageOption: {
@@ -178,7 +188,7 @@ export default {
languageText: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginRight: BaseTheme.spacing[3]
marginHorizontal: BaseTheme.spacing[2]
},
/**
@@ -225,10 +235,10 @@ export default {
},
logBtn: {
marginRight: BaseTheme.spacing[2]
marginRight: BaseTheme.spacing[3]
},
backBtn: {
marginLeft: BaseTheme.spacing[2]
marginLeft: BaseTheme.spacing[3]
}
};

View File

@@ -212,15 +212,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
cancel: () => {
const { options } = getVirtualBackgroundTabProps(state, isDisplayedOnWelcomePage);
return submitVirtualBackgroundTab({
options: {
backgroundType: options.backgroundType,
enabled: options.backgroundEffectEnabled,
url: options.virtualSource,
selectedThumbnail: options.selectedThumbnail,
blurValue: options.blurValue
}
}, true);
return submitVirtualBackgroundTab({ options }, true);
},
icon: IconImage
});

View File

@@ -34,7 +34,6 @@ function HangupMenu() {
dispatch(hideSheet());
sendAnalytics(createToolbarEvent('endmeeting'));
dispatch(endConference());
dispatch(appNavigate(undefined));
}, [ hideSheet ]);
const handleLeaveConference = useCallback(() => {

View File

@@ -36,10 +36,50 @@ interface IProps {
const useStyles = makeStyles()(theme => {
return {
drawerMenuContainer: {
height: '100vh',
display: 'flex',
alignItems: 'flex-end'
},
drawer: {
backgroundColor: theme.palette.ui01,
maxHeight: `calc(${DRAWER_MAX_HEIGHT})`,
borderRadius: '24px 24px 0 0'
borderRadius: '24px 24px 0 0',
overflowY: 'auto',
marginBottom: 'env(safe-area-inset-bottom, 0)',
width: '100%',
'& .overflow-menu': {
margin: 'auto',
fontSize: '1.2em',
listStyleType: 'none',
padding: 0,
height: 'calc(80vh - 144px - 64px)',
overflowY: 'auto',
'& .overflow-menu-item': {
boxSizing: 'border-box',
height: '48px',
padding: '12px 16px',
alignItems: 'center',
color: theme.palette.text01,
cursor: 'pointer',
display: 'flex',
fontSize: '16px',
'& div': {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
'&.disabled': {
cursor: 'initial',
color: '#3b475c'
}
}
}
}
};
});
@@ -56,7 +96,7 @@ function Drawer({
isOpen,
onClose
}: IProps) {
const { classes: styles } = useStyles();
const { classes, cx } = useStyles();
/**
* Handles clicks within the menu, preventing the propagation of the click event.
@@ -96,11 +136,11 @@ function Drawer({
return (
isOpen ? (
<div
className = 'drawer-menu-container'
className = { classes.drawerMenuContainer }
onClick = { handleOutsideClick }
onKeyDown = { handleEscKey }>
<div
className = { `drawer-menu ${styles.drawer} ${className}` }
className = { cx(classes.drawer, className) }
onClick = { handleInsideClick }>
<FocusOn
returnFocus = {

View File

@@ -1,4 +1,5 @@
import React, { ReactNode } from 'react';
import { makeStyles } from 'tss-react/mui';
import DialogPortal from './DialogPortal';
@@ -15,6 +16,29 @@ interface IProps {
className?: string;
}
const useStyles = makeStyles()(theme => {
return {
portal: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
zIndex: 351,
borderRadius: '16px 16px 0 0',
'&.notification-portal': {
zIndex: 901
},
'&::after': {
content: '',
backgroundColor: theme.palette.ui01,
marginBottom: 'env(safe-area-inset-bottom, 0)'
}
}
};
});
/**
* Component meant to render a drawer at the bottom of the screen,
* by creating a portal containing the component's children.
@@ -22,8 +46,10 @@ interface IProps {
* @returns {ReactElement}
*/
function JitsiPortal({ children, className }: IProps) {
const { classes, cx } = useStyles();
return (
<DialogPortal className = { `drawer-portal ${className ?? ''}` }>
<DialogPortal className = { cx(classes.portal, className) }>
{ children }
</DialogPortal>
);

View File

@@ -18,7 +18,7 @@ import {
getParticipantDisplayName,
isLocalParticipantModerator
} from '../../../base/participants/functions';
import { getBreakoutRooms, getCurrentRoomId } from '../../../breakout-rooms/functions';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { IRoom } from '../../../breakout-rooms/types';
import PrivateMessageButton from '../../../chat/components/native/PrivateMessageButton';
@@ -67,6 +67,11 @@ interface IProps {
*/
_disableRemoteMute: boolean;
/**
* Whether or not the current room is a breakout room.
*/
_isBreakoutRoom: boolean;
/**
* Whether the participant is present in the room or not.
*/
@@ -130,6 +135,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
_disablePrivateChat,
_disableRemoteMute,
_disableGrantModerator,
_isBreakoutRoom,
_isParticipantAvailable,
_moderator,
_rooms,
@@ -160,7 +166,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{ !_disableKick && <KickButton { ...buttonProps } /> }
{ !_disableGrantModerator && <GrantModeratorButton { ...buttonProps } /> }
{ !_disableGrantModerator && !_isBreakoutRoom && <GrantModeratorButton { ...buttonProps } /> }
<PinButton { ...buttonProps } />
{ !_disablePrivateChat && <PrivateMessageButton { ...buttonProps } /> }
<ConnectionStatusButton { ...connectionStatusButtonProps } />
@@ -235,12 +241,14 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
const shouldDisableKick = disableKick || !kickOutEnabled;
const moderator = isLocalParticipantModerator(state);
const _iAmVisitor = state['features/visitors'].iAmVisitor;
const _isBreakoutRoom = isInBreakoutRoom(state);
return {
_currentRoomId,
_disableKick: Boolean(shouldDisableKick),
_disableRemoteMute: Boolean(disableRemoteMute),
_disablePrivateChat: Boolean(disablePrivateChat) || _iAmVisitor,
_isBreakoutRoom,
_isParticipantAvailable: Boolean(isParticipantAvailable),
_moderator: moderator,
_participantDisplayName: getParticipantDisplayName(state, participantId),

View File

@@ -3,7 +3,7 @@ import { createVirtualBackgroundEffect } from '../stream-effects/virtual-backgro
import { BACKGROUND_ENABLED, SET_VIRTUAL_BACKGROUND } from './actionTypes';
import logger from './logger';
import { IVirtualBackgroundOptions } from './types';
import { IVirtualBackground } from './reducer';
/**
* Signals the local participant activate the virtual background video or not.
@@ -12,16 +12,16 @@ import { IVirtualBackgroundOptions } from './types';
* @param {Object} jitsiTrack - Represents the jitsi track that will have backgraund effect applied.
* @returns {Promise}
*/
export function toggleBackgroundEffect(options: IVirtualBackgroundOptions, jitsiTrack: any) {
export function toggleBackgroundEffect(options: IVirtualBackground, jitsiTrack: any) {
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
await dispatch(backgroundEnabled(options.enabled));
await dispatch(backgroundEnabled(options.backgroundEffectEnabled));
await dispatch(setVirtualBackground(options));
const state = getState();
const virtualBackground = state['features/virtual-background'];
if (jitsiTrack) {
try {
if (options.enabled) {
if (options.backgroundEffectEnabled) {
await jitsiTrack.setEffect(await createVirtualBackgroundEffect(virtualBackground, dispatch));
} else {
await jitsiTrack.setEffect(undefined);
@@ -46,10 +46,10 @@ export function toggleBackgroundEffect(options: IVirtualBackgroundOptions, jitsi
* type: string,
* }}
*/
export function setVirtualBackground(options?: IVirtualBackgroundOptions) {
export function setVirtualBackground(options?: IVirtualBackground) {
return {
type: SET_VIRTUAL_BACKGROUND,
virtualSource: options?.url,
virtualSource: options?.virtualSource,
blurValue: options?.blurValue,
backgroundType: options?.backgroundType,
selectedThumbnail: options?.selectedThumbnail
@@ -65,7 +65,7 @@ export function setVirtualBackground(options?: IVirtualBackgroundOptions) {
* backgroundEffectEnabled: boolean
* }}
*/
export function backgroundEnabled(backgroundEffectEnabled: boolean) {
export function backgroundEnabled(backgroundEffectEnabled?: boolean) {
return {
type: BACKGROUND_ENABLED,
backgroundEffectEnabled

View File

@@ -105,10 +105,10 @@ function UploadImageButton({
}
]);
setOptions({
backgroundEffectEnabled: true,
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
enabled: true,
url,
selectedThumbnail: uuId
selectedThumbnail: uuId,
virtualSource: url
});
};
logger.info('New virtual background image uploaded!');

View File

@@ -15,6 +15,7 @@ import { showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { toggleBackgroundEffect } from '../actions';
import logger from '../logger';
import { IVirtualBackground } from '../reducer';
/**
* The type of the React {@code PureComponent} props of {@link VirtualBackgroundPreview}.
@@ -39,7 +40,7 @@ export interface IProps extends WithTranslation {
/**
* Represents the virtual background set options.
*/
options: any;
options: IVirtualBackground;
/**
* The id of the selected video device.

View File

@@ -19,6 +19,7 @@ import Spinner from '../../base/ui/components/web/Spinner';
import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
import { toDataURL } from '../functions';
import logger from '../logger';
import { IVirtualBackground } from '../reducer';
import UploadImageButton from './UploadImageButton';
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
@@ -72,7 +73,7 @@ interface IProps extends WithTranslation {
/**
* Virtual background options.
*/
options: any;
options: IVirtualBackground;
/**
* Returns the selected thumbnail identifier.
@@ -254,8 +255,8 @@ function VirtualBackgrounds({
const enableBlur = useCallback(async () => {
onOptionsChange({
backgroundEffectEnabled: true,
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
enabled: true,
blurValue: 25,
selectedThumbnail: 'blur'
});
@@ -272,8 +273,8 @@ function VirtualBackgrounds({
const enableSlideBlur = useCallback(async () => {
onOptionsChange({
backgroundEffectEnabled: true,
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
enabled: true,
blurValue: 8,
selectedThumbnail: 'slight-blur'
});
@@ -290,7 +291,7 @@ function VirtualBackgrounds({
const removeBackground = useCallback(async () => {
onOptionsChange({
enabled: false,
backgroundEffectEnabled: false,
selectedThumbnail: 'none'
});
logger.info('"None" option set for virtual background preview!');
@@ -310,10 +311,10 @@ function VirtualBackgrounds({
if (image) {
onOptionsChange({
backgroundType: 'image',
enabled: true,
url: image.src,
selectedThumbnail: image.id
backgroundEffectEnabled: true,
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
selectedThumbnail: image.id,
virtualSource: image.src
});
logger.info('Uploaded image set for virtual background preview!');
}
@@ -328,10 +329,10 @@ function VirtualBackgrounds({
const url = await toDataURL(image.src);
onOptionsChange({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
backgroundEffectEnabled: true,
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
selectedThumbnail: image.id,
virtualSource: url
});
logger.info('Image set for virtual background preview!');
} catch (err) {

View File

@@ -1,6 +0,0 @@
import { IVirtualBackground } from './reducer';
export interface IVirtualBackgroundOptions extends IVirtualBackground {
enabled: boolean;
url?: string;
}