Compare commits

...

68 Commits

Author SHA1 Message Date
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
Calin-Teodor
86ccc176e8 feat(authentication): authentication log in/log out through Profile section 2023-08-18 17:36:16 +03:00
Дамян Минков
b31041f0ce fix: Fixes start A/V muted after going to welcome page. Fixes #11393. (#13726) 2023-08-18 06:29:04 -05:00
damencho
e434a78de9 fix: Fixes i18next trying to fetch non-existing files. Fixes #13716.
Thanks @hamletmun for the solution.
2023-08-18 06:27:42 -05:00
Mihaela Dumitru
6ddb77e03c feat(dial-in) add sip audio only (#13714) 2023-08-18 10:22:03 +03:00
damencho
f6665d79c0 fix: Fixes password authentication on prejoin. Fixes #13721. 2023-08-17 11:36:34 -05:00
Saúl Ibarra Corretgé
75a7b99a42 fix(WaitForOwner) tweak copy 2023-08-17 15:56:38 +02:00
Pontus Fagerström
3858a40c1c fix(doc): fix typo in jsdoc comment 2023-08-17 11:21:20 +02:00
Saúl Ibarra Corretgé
3d6aa8f2b5 fix(auth) move common actions to actions.any 2023-08-17 11:03:14 +02:00
Saúl Ibarra Corretgé
54436f97c1 fix(auth) open token auth URL in a new window on Electron 2023-08-17 11:03:14 +02:00
Saúl Ibarra Corretgé
dca40dc6cb feat(rn,auth) add support for toekn URL auth 2023-08-17 11:03:14 +02:00
Horatiu Muresan
c19d91a373 feat(external-api) add command for setting camera facing mode (#13541)
- added command for setting the camera facing mode remotely
- enhanced toggleVideo command to optionally accept the facing mode
- fix(startSilent) do not create audio track when start silent
2023-08-17 09:47:48 +03:00
Werner Fleischer
840cfd8ab0 feat: use i18next-http-backend and fix ts-ignore
i18next-xhr-backend is not maintained any more and [superseded](https://github.com/i18next/i18next-xhr-backend/issues/348#issuecomment-663060275)
by i18next-http-backend.
2023-08-16 22:04:36 -05:00
Hristo Terezov
8cf6ba88e1 fix(jitsi-local-storage):Is empty after reload
When using useHostPageLocalStorage on the iframe api and local storage
is not throwing error we were writting the passed data to the original
local storage and then switching to the dummy local storage from
jitsiLocalStorage.
2023-08-16 21:22:37 -05:00
Saúl Ibarra Corretgé
4ac81b030e Revert "fix(lang) update Albanian translation"
This reverts commit 03daaa4832.
2023-08-16 15:19:22 +02:00
damencho
e5cd1b29fe fix: Shows login warning for leaving only when in conference.
We were showing the dialog and when waiting for host is shown on prejoin screen.
2023-08-15 14:47:16 -05:00
Gabriel Borlea
ebc932572f feat(rn, settings, rn): update view 2023-08-15 09:32:40 +02:00
Jaya Allamsetty
f0a6c6e67e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1676.0.0+486efad8...v1678.0.0+77e6803f
2023-08-15 02:13:37 -04:00
JPL
e1454b5c4a fix(rnsdk) copying sounds on iOS
Fixes: #13705
2023-08-14 19:26:29 +02:00
Robert Pintilii
f6e2abdf01 fix(chat) Only display private message dialog for active participants (#13708) 2023-08-14 16:35:13 +03:00
Mihaela Dumitru
e2a02f4b21 fix(toolbar) disable the profile button based on the toolbar logic (#13696) 2023-08-14 09:45:07 +03:00
TTG
934d7db24e fix(lang) update zhCN & zhTW translations 2023-08-13 10:06:38 +02:00
damencho
2b520cbc4c fix: Hides email when gravatar is disabled or avatar is provided.
When avatar is externally provided, set by iframe API or via jwt we hide gravatar setting as it does nothing.
2023-08-11 14:36:04 -05:00
damencho
e37dd73b9e feat: Shows dialog when about to navigate away on login. 2023-08-11 14:36:04 -05:00
damencho
c646319657 feat: Caches is secure room checks.
Consider long room names secure.
2023-08-10 17:28:04 -05:00
Jaya Allamsetty
7a3a5a3f43 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1675.0.0+0cc323d9...v1676.0.0+486efad8
2023-08-10 16:40:54 -04:00
Horatiu Muresan
5345a77092 chore(tileview) Add config for disabling tileview (#13692)
- show fixed number of toolbar buttons in toolbar (including custom buttons) instead of sending to overflow menu
2023-08-10 16:31:32 +03:00
Horatiu Muresan
91de33550d feat(toolbar-buttons) Add optional background color (#13691) 2023-08-10 16:30:14 +03:00
Javier García
85fb7513db Validate special characters that may conflict with sed (#13674) 2023-08-09 12:43:51 -05:00
Mihaela Dumitru
c9d907e3fe fix(shortcuts) toggle value based on current state (#13685) 2023-08-09 20:03:45 +03:00
Mihaela Dumitru
5dfd02151e feat(static) improve message in authError.html (#13681) 2023-08-09 18:21:39 +03:00
Jaya Allamsetty
e446802ac9 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1674.0.0+648d0ddc...v1675.0.0+0cc323d9
2023-08-09 11:11:25 -04:00
Gabriel Borlea
bb71a4bb7d fix(input, rn): add deps to input callbacks (#13683) 2023-08-09 18:00:02 +03:00
140 changed files with 6662 additions and 4323 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';
@@ -441,7 +442,7 @@ export default {
// Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
// "Are you trying to speak?" ) even if the user joins the conference muted.
const initialDevices = config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
const initialDevices = config.startSilent || config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
const requestedAudio = !config.disableInitialGUM;
let requestedVideo = false;
@@ -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

@@ -871,7 +871,7 @@ var config = {
// customParticipantMenuButtons: [],
// An array with custom option buttons for the toolbar
// type: Array<{ icon: string; id: string; text: string; }>
// type: Array<{ icon: string; id: string; text: string; backgroundColor?: string; }>
// customToolbarButtons: [],
// Stats
@@ -1572,6 +1572,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,
@@ -1594,6 +1596,8 @@ var config = {
// Tile view related config options.
// tileView: {
// // Whether tileview should be disabled.
// disabled: false,
// // The optimal number of tiles that are going to be shown in tile view. Depending on the screen size it may
// // not be possible to show the exact number of participants specified here.
// numberOfVisibleTiles: 25,

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

@@ -14,7 +14,7 @@
line-height: 24px;
}
}
.hint-msg{
.hint-msg {
p {
margin: 26px auto;
font-weight: 600;
@@ -31,4 +31,10 @@
background: transparent;
}
}
.forbidden-msg {
p {
font-size: 16px;
margin-top: 15px;
}
}
}

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

@@ -38,6 +38,11 @@ case "$1" in
if [ "$RET" = "false" ] ; then
echo "Application secret is mandatory"
fi
# Not allowed unix special characters in secret: /, \, ", ', `
if echo "$RET" | grep -q '[/\\\"\`]' ; then
echo "Application secret contains invalid characters: /, \\, \", ', \`"
exit 1
fi
APP_SECRET=$RET
PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua"

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -250,13 +250,14 @@
"dialog": {
"Back": "Back",
"Cancel": "Cancel",
"IamHost": "I am the host",
"IamHost": "Log-in",
"Ok": "OK",
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"WaitForHostMsg": "The conference has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"WaitingForHostTitle": "Waiting for the host ...",
"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": {
"Cancel": "Cancel (leave dialog)",
@@ -269,6 +270,8 @@
"addMeetingNote": "Add a note about this meeting",
"addOptionalNote": "Add a note (optional):",
"allow": "Allow",
"allowToggleCameraDialog": "Do you allow {{initiatorName}} to toggle your camera facing mode?",
"allowToggleCameraTitle": "Allow toggle camera?",
"alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
"applicationWindow": "Application window",
@@ -329,6 +332,7 @@
"lockRoom": "Add meeting $t(lockRoomPassword)",
"lockTitle": "Lock failed",
"login": "Login",
"loginQuestion": "Are you sure you want to login and leave the conference?",
"logoutQuestion": "Are you sure you want to logout and leave the conference?",
"logoutTitle": "Logout",
"maxUsersLimitReached": "The limit for maximum number of participants has been reached. The conference is full. Please contact the meeting owner or try again later!",
@@ -409,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",
@@ -547,6 +552,7 @@
"password": "$t(lockRoomPasswordUppercase): ",
"reachedLimit": "You have reached the limit of your plan.",
"sip": "SIP address",
"sipAudioOnly": "SIP audio only address",
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting",
"upgradeOptions": "Please check the upgrade options on"
@@ -653,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",
@@ -1078,6 +1084,7 @@
"alertOk": "OK",
"alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid",
"apply": "Apply",
"buildInfoSection": "Build Information",
"conferenceSection": "Conference",
"disableCallIntegration": "Disable native call integration",
@@ -1088,6 +1095,7 @@
"displayNamePlaceholderText": "Eg: John Doe",
"email": "Email",
"emailPlaceholderText": "email@example.com",
"gavatarMessage": "If your email is associated with a Gravatar account, we will use it to display your profile picture.",
"goTo": "Go to",
"header": "Settings",
"help": "Help",
@@ -1270,8 +1278,8 @@
"linkToSalesforce": "Link to Salesforce",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
"login": "Login",
"logout": "Logout",
"login": "Log-in",
"logout": "Log-out",
"lowerYourHand": "Lower your hand",
"moreActions": "More actions",
"moreOptions": "More options",

View File

@@ -50,8 +50,8 @@ import {
} from '../../react/features/base/participants/functions';
import { updateSettings } from '../../react/features/base/settings/actions';
import { getDisplayName } from '../../react/features/base/settings/functions.web';
import { toggleCamera } from '../../react/features/base/tracks/actions.any';
import { isToggleCameraEnabled } from '../../react/features/base/tracks/functions';
import { setCameraFacingMode } from '../../react/features/base/tracks/actions.web';
import { CAMERA_FACING_MODE_MESSAGE } from '../../react/features/base/tracks/constants';
import {
autoAssignToBreakoutRooms,
closeBreakoutRoom,
@@ -395,12 +395,8 @@ function initCommands() {
sendAnalytics(createApiEvent('film.strip.resize'));
APP.store.dispatch(resizeFilmStrip(options.width));
},
'toggle-camera': () => {
if (!isToggleCameraEnabled(APP.store.getState())) {
return;
}
APP.store.dispatch(toggleCamera());
'toggle-camera': facingMode => {
APP.store.dispatch(setCameraFacingMode(facingMode));
},
'toggle-camera-mirror': () => {
const state = APP.store.getState();
@@ -529,6 +525,18 @@ function initCommands() {
logger.error('Failed sending endpoint text message', err);
}
},
'send-camera-facing-mode-message': (to, facingMode) => {
if (!to) {
logger.warn('Participant id not set');
return;
}
APP.conference.sendEndpointMessage(to, {
name: CAMERA_FACING_MODE_MESSAGE,
facingMode
});
},
'overwrite-names': participantList => {
logger.debug('Overwrite names command received');
@@ -1133,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);
}
}
}
@@ -1467,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

@@ -54,6 +54,7 @@ const commands = {
removeBreakoutRoom: 'remove-breakout-room',
resizeFilmStrip: 'resize-film-strip',
resizeLargeVideo: 'resize-large-video',
sendCameraFacingMode: 'send-camera-facing-mode-message',
sendChatMessage: 'send-chat-message',
sendEndpointTextMessage: 'send-endpoint-text-message',
sendParticipantToRoom: 'send-participant-to-room',
@@ -112,6 +113,7 @@ const events = {
'data-channel-opened': 'dataChannelOpened',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'email-change': 'emailChange',
'error-occurred': 'errorOccurred',
'endpoint-text-message-received': 'endpointTextMessageReceived',
@@ -153,7 +155,6 @@ const events = {
'video-mute-status-changed': 'videoMuteStatusChanged',
'video-quality-changed': 'videoQualityChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged',

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

1844
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",
@@ -58,14 +58,14 @@
"i18n-iso-countries": "6.8.0",
"i18next": "17.0.6",
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"i18next-http-backend": "^2.2.1",
"image-capture": "0.4.0",
"jquery": "3.6.1",
"jquery-i18next": "1.2.1",
"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/v1674.0.0+648d0ddc/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",

View File

@@ -26,10 +26,10 @@ Pod::Spec.new do |s|
s.script_phase = {
:name => 'Copy Sound Files',
:script => '
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds"
TARGET_PATH="$(dirname ${CONFIGURATION_BUILD_DIR})"
PROJECT_NAME=$(basename $(dirname $(dirname ${PROJECT_DIR}))).app
cp -R "${PODS_TARGET_SRCROOT}/sounds/" "${TARGET_PATH}/${PROJECT_NAME}"
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds/"
TARGET_PATH=$(dirname "${CONFIGURATION_BUILD_DIR}")
PROJECT_NAME=$(basename $(dirname $(dirname "${PROJECT_DIR}"))).app
cp -R "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}"
',
}
end

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

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

@@ -5,6 +5,7 @@ import { getTokenAuthUrl } from '../authentication/functions';
import { IStateful } from '../base/app/types';
import { isRoomValid } from '../base/conference/functions';
import { isSupportedBrowser } from '../base/environment/environment';
import { browser } from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import Conference from '../conference/components/web/Conference';
import { getDeepLinkingPage } from '../deep-linking/functions';
@@ -48,9 +49,9 @@ function _getWebConferenceRoute(state: IReduxState) {
// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
if (config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
route.href = getTokenAuthUrl(config, room);
return Promise.resolve(route);

View File

@@ -4,6 +4,8 @@ import { IJitsiConference } from '../base/conference/reducer';
import { hideDialog, openDialog } from '../base/dialog/actions';
import {
LOGIN,
LOGOUT,
SET_TOKEN_AUTH_URL_SUCCESS,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
@@ -137,6 +139,32 @@ export function hideLoginDialog() {
return hideDialog(LoginDialog);
}
/**
* Login.
*
* @returns {{
* type: LOGIN
* }}
*/
export function login() {
return {
type: LOGIN
};
}
/**
* Logout.
*
* @returns {{
* type: LOGOUT
* }}
*/
export function logout() {
return {
type: LOGOUT
};
}
/**
* Opens {@link WaitForOnwerDialog}.
*

View File

@@ -1,8 +1,11 @@
import { Linking, Platform } from 'react-native';
import { appNavigate } from '../app/actions.native';
import { IStore } from '../app/types';
import { conferenceLeft } from '../base/conference/actions';
import { connectionFailed } from '../base/connection/actions.native';
import { set } from '../base/redux/functions';
import { appendURLHashParam } from '../base/util/uri';
import { CANCEL_LOGIN } from './actionTypes';
import { stopWaitForOwner } from './actions.any';
@@ -51,7 +54,7 @@ export function cancelWaitForOwner() {
// recoverable by the feature room-lock and, consequently,
// recoverable-aware features such as mobile's external-api did not
// deliver the CONFERENCE_FAILED to the SDK clients/consumers. Since the
// app/user is going to nativate to WelcomePage, the SDK
// app/user is going to navigate to WelcomePage, the SDK
// clients/consumers need an event.
const { authRequired } = getState()['features/base/conference'];
@@ -65,7 +68,7 @@ export function cancelWaitForOwner() {
};
}
/** .
/**
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}
@@ -73,3 +76,21 @@ export function cancelWaitForOwner() {
export function redirectToDefaultLocation() {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
}
/**
* Opens token auth URL page.
*
* @param {string} tokenAuthServiceUrl - Authentication service URL.
*
* @returns {Function}
*/
export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true');
// Append ios=true or android=true to the token URL.
url = appendURLHashParam(url, Platform.OS, 'true');
return () => {
Linking.openURL(url);
};
}

View File

@@ -1,11 +1,11 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { browser } from '../base/lib-jitsi-meet';
import { appendURLHashParam } from '../base/util/uri';
import {
CANCEL_LOGIN,
LOGIN,
LOGOUT
} from './actionTypes';
import { CANCEL_LOGIN } from './actionTypes';
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
export * from './actions.any';
@@ -38,7 +38,7 @@ export function cancelWaitForOwner() {
};
}
/** .
/**
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}
@@ -48,27 +48,36 @@ export function redirectToDefaultLocation() {
}
/**
* Login.
* Opens token auth URL page.
*
* @returns {{
* type: LOGIN
* }}
* @param {string} tokenAuthServiceUrl - Authentication service URL.
*
* @returns {Function}
*/
export function login() {
return {
type: LOGIN
};
}
export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const redirect = () => {
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true');
/**
* Logout.
*
* @returns {{
* type: LOGOUT
* }}
*/
export function logout() {
return {
type: LOGOUT
if (browser.isElectron()) {
url = appendURLHashParam(url, 'electron', 'true');
window.open(url, '_blank');
} else {
window.location.href = url;
}
};
// Show warning for leaving conference only when in a conference.
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
dispatch(openDialog(LoginQuestionDialog, {
handler: () => {
// Give time for the dialog to close.
setTimeout(() => redirect, 500);
}
}));
} else {
redirect();
}
};
}

View File

@@ -1,16 +1,22 @@
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, openLoginDialog } from '../../actions.native';
import { cancelWaitForOwner, login } from '../../actions.native';
/**
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
*/
interface IProps {
/**
* Whether to show alternative cancel button text.
*/
_alternativeCancelText?: boolean;
/**
* Redux store dispatch function.
*/
@@ -52,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 }
@@ -77,8 +83,24 @@ class WaitForOwnerDialog extends Component<IProps> {
* @returns {void}
*/
_onLogin() {
this.props.dispatch(openLoginDialog());
this.props.dispatch(login());
}
}
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

@@ -0,0 +1,38 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Dialog from '../../../base/ui/components/web/Dialog';
/**
* The type of {@link LoginQuestionDialog}'s React {@code Component} props.
*/
interface IProps {
/**
* The handler.
*/
handler: () => void;
}
/**
* Implements the dialog that warns the user that the login will leave the conference.
*
* @param {Object} props - The props of the component.
* @returns {React$Element}.
*/
const LoginQuestionDialog = ({ handler }: IProps) => {
const { t } = useTranslation();
return (
<Dialog
ok = {{ translationKey: 'dialog.Yes' }}
onSubmit = { handler }
titleKey = { t('dialog.login') }>
<div>
{ t('dialog.loginQuestion') }
</div>
</Dialog>
);
};
export default LoginQuestionDialog;

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

@@ -27,7 +27,7 @@ export const isTokenAuthEnabled = (config: IConfig) =>
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
* constructed.
*/
export const getTokenAuthUrl = (config: IConfig, roomName: string) => {
export const getTokenAuthUrl = (config: IConfig, roomName: string | undefined) => {
const url = config.tokenAuthUrl;

View File

@@ -7,7 +7,6 @@ import {
} from '../base/conference/actionTypes';
import { isRoomValid } from '../base/conference/functions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { hangup } from '../base/connection/actions';
import { hideDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import {
@@ -16,8 +15,6 @@ import {
} from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { getBackendSafeRoomName } from '../base/util/uri';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { openLogoutDialog } from '../settings/actions';
import {
@@ -31,6 +28,7 @@ import {
import {
hideLoginDialog,
openLoginDialog,
openTokenAuthUrl,
openWaitForOwnerDialog,
redirectToDefaultLocation,
setTokenAuthUrlSuccess,
@@ -39,6 +37,8 @@ import {
} from './actions';
import { LoginDialog, WaitForOwnerDialog } from './components';
import { getTokenAuthUrl, isTokenAuthEnabled } from './functions';
import logger from './logger';
/**
* Middleware that captures connection or conference failed errors and controls
@@ -163,33 +163,7 @@ MiddlewareRegistry.register(store => next => action => {
}
case LOGOUT: {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
const { conference } = store.getState()['features/base/conference'];
if (!conference) {
break;
}
dispatch(openLogoutDialog(() => {
const logoutUrl = config.tokenLogoutUrl;
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// user is logging out remove auto redirect indication
dispatch(setTokenAuthUrlSuccess(false));
}
if (logoutUrl) {
window.location.href = logoutUrl;
return;
}
conference.room.moderator.logout(() => dispatch(hangup(true)));
}));
_handleLogout(store);
break;
}
@@ -282,31 +256,45 @@ function _handleLogin({ dispatch, getState }: IStore) {
const config = state['features/base/config'];
const room = getBackendSafeRoomName(state['features/base/conference'].room);
if (isTokenAuthEnabled(config)) {
if (typeof APP === 'undefined') {
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthUnsupported',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
if (!room) {
logger.warn('Cannot handle login, room is undefined!');
dispatch(redirectToDefaultLocation());
return;
}
if (!room) {
return;
}
// FIXME: This method will not preserve the other URL params that were originally passed.
const tokenAuthServiceUrl = getTokenAuthUrl(config, room);
if (tokenAuthServiceUrl) {
// we have already shown the prejoin screen, so no need to show it again after obtaining the token
window.location.href = `${tokenAuthServiceUrl}${
tokenAuthServiceUrl.includes('#') ? '&' : '#'}skipPrejoin=true`;
}
} else {
dispatch(openLoginDialog());
return;
}
if (!isTokenAuthEnabled(config)) {
dispatch(openLoginDialog());
return;
}
// FIXME: This method will not preserve the other URL params that were originally passed.
const tokenAuthServiceUrl = getTokenAuthUrl(config, room);
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');
return;
}
dispatch(openTokenAuthUrl(tokenAuthServiceUrl));
}
/**
* Handles logout challenge. Opens logout dialog and hangs up the conference.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {string} logoutUrl - The url for logging out.
* @returns {void}
*/
function _handleLogout({ dispatch, getState }: IStore) {
const state = getState();
const { conference } = state['features/base/conference'];
if (!conference) {
return;
}
dispatch(openLogoutDialog());
}

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

@@ -1,4 +1,4 @@
type ToolbarButtons = 'camera' |
export type ToolbarButton = 'camera' |
'chat' |
'closedcaptions' |
'desktop' |
@@ -15,6 +15,9 @@ type ToolbarButtons = 'camera' |
'linktosalesforce' |
'livestreaming' |
'microphone' |
'mute-everyone' |
'mute-video-everyone' |
'noisesuppression' |
'participants-pane' |
'profile' |
'raisehand' |
@@ -30,6 +33,7 @@ type ToolbarButtons = 'camera' |
'tileview' |
'toggle-camera' |
'videoquality' |
'whiteboard' |
'__end';
type ButtonsWithNotifyClick = 'camera' |
@@ -270,7 +274,7 @@ export interface IConfig {
};
corsAvatarURLs?: Array<string>;
customParticipantMenuButtons?: Array<{ icon: string; id: string; text: string; }>;
customToolbarButtons?: Array<{ icon: string; id: string; text: string; }>;
customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>;
deeplinking?: IDeeplinkingConfig;
defaultLanguage?: string;
defaultLocalDisplayName?: string;
@@ -397,6 +401,7 @@ export interface IConfig {
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
disableTopPanel?: boolean;
disabled?: boolean;
minParticipantCountForTopPanel?: number;
};
firefox_fake_device?: string;
@@ -573,12 +578,13 @@ export interface IConfig {
testMode?: boolean;
};
tileView?: {
disabled?: boolean;
numberOfVisibleTiles?: number;
};
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenLogoutUrl?: string;
toolbarButtons?: Array<ToolbarButtons>;
toolbarButtons?: Array<ToolbarButton>;
toolbarConfig?: {
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;

View File

@@ -1,3 +1,5 @@
import { ToolbarButton } from './configType';
/**
* The prefix of the {@code localStorage} key into which {@link storeConfig}
* stores and from which {@link restoreConfig} restores.
@@ -13,7 +15,7 @@ export const _CONFIG_STORE_PREFIX = 'config.js';
* @protected
* @type Array<string>
*/
export const TOOLBAR_BUTTONS = [
export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'camera',
'chat',
'closedcaptions',

View File

@@ -7,7 +7,8 @@ import {
IDeeplinkingConfig,
IDeeplinkingMobileConfig,
IDeeplinkingPlatformConfig,
NotifyClickButton
NotifyClickButton,
ToolbarButton
} from './configType';
import { TOOLBAR_BUTTONS } from './constants';
@@ -46,7 +47,7 @@ export function getToolbarButtons(state: IReduxState): Array<string> {
const buttons = Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
if (customButtons) {
buttons.push(...customButtons);
buttons.push(...customButtons as ToolbarButton[]);
}
return buttons;

View File

@@ -16,8 +16,10 @@ import {
IDeeplinkingConfig,
IDeeplinkingMobileConfig,
IDeeplinkingPlatformConfig,
IMobileDynamicLink
IMobileDynamicLink,
ToolbarButton
} from './configType';
import { TOOLBAR_BUTTONS } from './constants';
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
/**
@@ -546,6 +548,11 @@ function _translateLegacyConfig(oldValue: IConfig) {
};
}
if (oldValue.disableProfile) {
newValue.toolbarButtons = (newValue.toolbarButtons || TOOLBAR_BUTTONS)
.filter((button: ToolbarButton) => button !== 'profile');
}
_setDeeplinkingDefaults(newValue.deeplinking as IDeeplinkingConfig);
return newValue;

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

@@ -1,6 +1,6 @@
import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json';
import i18next from 'i18next';
import I18nextXHRBackend from 'i18next-xhr-backend';
import I18nextXHRBackend, { HttpBackendOptions } from 'i18next-http-backend';
import _ from 'lodash';
import LANGUAGES_RESOURCES from '../../../../lang/languages.json';
@@ -61,11 +61,13 @@ export const DEFAULT_LANGUAGE = 'en';
/**
* The options to initialize i18next with.
*
* @type {Object}
* @type {i18next.InitOptions}
*/
const options = {
backend: {
loadPath: 'lang/{{ns}}-{{lng}}.json'
const options: i18next.InitOptions = {
backend: <HttpBackendOptions>{
loadPath: (lng: string[], ns: string[]) =>
// eslint-disable-next-line no-extra-parens
(ns[0] === 'main' ? 'lang/{{ns}}-{{lng}}.json' : 'lang/{{ns}}.json')
},
defaultNS: 'main',
fallbackLng: DEFAULT_LANGUAGE,
@@ -76,6 +78,7 @@ const options = {
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
react: {
// re-render when a new resource bundle is added
// @ts-expect-error. Fixed in i18next 19.6.1.
bindI18nStore: 'added',
useSuspense: false
},
@@ -89,7 +92,7 @@ const options = {
i18next
.use(navigator.product === 'ReactNative' ? {} : I18nextXHRBackend)
.use(languageDetector) // @ts-ignore
.use(languageDetector)
.init(options);
// Add default language which is preloaded from the source code.

View File

@@ -63,6 +63,13 @@ function setupJitsiLocalStorage() {
try {
const localStorageContent = safeJsonParse(urlParams['appData.localStorageContent']);
// We need to disable the local storage before setting the data in case the browser local storage doesn't
// throw exception (in some cases when this happens the local storage may be cleared for every session.
// Example: when loading meet from cross-domain with the IFrame API with Brave with the default
// configuration). Otherwise we will set the data in the browser local storage and then switch to the dummy
// local storage from jitsiLocalStorage and we will loose the data.
jitsiLocalStorage.setLocalStorageDisabled(true);
if (typeof localStorageContent === 'object') {
Object.keys(localStorageContent).forEach(key => {
jitsiLocalStorage.setItem(key, localStorageContent[key]);
@@ -72,7 +79,6 @@ function setupJitsiLocalStorage() {
logger.error('Can\'t parse localStorageContent.', error);
}
jitsiLocalStorage.setLocalStorageDisabled(true);
jitsiLocalStorage.on('changed', onFakeLocalStorageChanged);
}
}

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

@@ -220,21 +220,22 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
const state = getState();
const { room } = action;
const roomIsValid = isRoomValid(room);
const audioMuted = roomIsValid ? getStartWithAudioMuted(state) : _AUDIO_INITIAL_MEDIA_STATE.muted;
const videoMuted = roomIsValid ? getStartWithVideoMuted(state) : _VIDEO_INITIAL_MEDIA_STATE.muted;
sendAnalytics(
createStartMutedConfigurationEvent('local', audioMuted, Boolean(videoMuted)));
logger.log(
`Start muted: ${audioMuted ? 'audio, ' : ''}${
videoMuted ? 'video' : ''}`);
// when going to welcomepage on web(room is not valid) we want to skip resetting the values of startWithA/V
if (roomIsValid || navigator.product === 'ReactNative') {
const audioMuted = roomIsValid ? getStartWithAudioMuted(state) : _AUDIO_INITIAL_MEDIA_STATE.muted;
const videoMuted = roomIsValid ? getStartWithVideoMuted(state) : _VIDEO_INITIAL_MEDIA_STATE.muted;
// Unconditionally express the desires/expectations/intents of the app and
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setVideoMuted(videoMuted));
sendAnalytics(createStartMutedConfigurationEvent('local', audioMuted, Boolean(videoMuted)));
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
// Unconditionally express the desires/expectations/intents of the app and
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setVideoMuted(videoMuted));
}
// startAudioOnly
//

View File

@@ -16,6 +16,11 @@ export interface IProps extends WithTranslation {
*/
afterClick?: Function;
/**
* The button's background color.
*/
backgroundColor?: string;
/**
* The button's key.
*/
@@ -108,6 +113,13 @@ export default class AbstractButton<P extends IProps, S=any> extends Component<P
visible: true
};
/**
* The button's background color.
*
* @abstract
*/
backgroundColor?: string;
/**
* A succinct description of what the button does. Used by accessibility
* tools and torture tests.

View File

@@ -9,6 +9,11 @@ import type { IProps as AbstractToolboxItemProps } from './AbstractToolboxItem';
interface IProps extends AbstractToolboxItemProps {
/**
* The button's background color.
*/
backgroundColor?: string;
/**
* Whether or not the item is displayed in a context menu.
*/
@@ -60,6 +65,7 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
*/
_renderItem() {
const {
backgroundColor,
contextMenu,
disabled,
elementAfter,
@@ -90,6 +96,7 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
return (
<ContextMenuItem
accessibilityLabel = { this.accessibilityLabel }
backgroundColor = { backgroundColor }
disabled = { disabled }
icon = { icon }
onClick = { onClick }
@@ -128,14 +135,18 @@ export default class ToolboxItem extends AbstractToolboxItem<IProps> {
* @returns {ReactElement}
*/
_renderIcon() {
const { customClass, disabled, icon, showLabel, toggled } = this.props;
const { backgroundColor, customClass, disabled, icon, showLabel, toggled } = this.props;
const iconComponent = (<Icon
size = { showLabel ? undefined : 24 }
src = { icon } />);
const elementType = showLabel ? 'span' : 'div';
const className = `${showLabel ? 'overflow-menu-item-icon' : 'toolbox-icon'} ${
toggled ? 'toggled' : ''} ${disabled ? 'disabled' : ''} ${customClass ?? ''}`;
const style = backgroundColor && !showLabel ? { backgroundColor } : {};
return React.createElement(elementType, { className }, iconComponent);
return React.createElement(elementType, {
className,
style
}, iconComponent);
}
}

View File

@@ -13,18 +13,23 @@ import { toggleScreenshotCaptureSummary } from '../../screenshot-capture/actions
import { isScreenshotCaptureEnabled } from '../../screenshot-capture/functions';
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
import { getCurrentConference } from '../conference/functions';
import { openDialog } from '../dialog/actions';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { setScreenshareMuted } from '../media/actions';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import {
addLocalTrack,
replaceLocalTrack
replaceLocalTrack,
toggleCamera
} from './actions.any';
import AllowToggleCameraDialog from './components/web/AllowToggleCameraDialog';
import {
createLocalTracksF,
getLocalDesktopTrack,
getLocalJitsiAudioTrack
getLocalJitsiAudioTrack,
getLocalVideoTrack,
isToggleCameraEnabled
} from './functions';
import { IShareOptions, IToggleScreenSharingOptions } from './types';
@@ -263,3 +268,52 @@ async function _toggleScreenSharing(
APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
}
}
/**
* Sets the camera facing mode(environment/user). If facing mode not provided, it will do a toggle.
*
* @param {string | undefined} facingMode - The selected facing mode.
* @returns {void}
*/
export function setCameraFacingMode(facingMode: string | undefined) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
if (!isToggleCameraEnabled(state)) {
return;
}
if (!facingMode) {
dispatch(toggleCamera());
return;
}
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
if (!tracks || !localVideoTrack) {
return;
}
const currentFacingMode = localVideoTrack.getCameraFacingMode();
if (currentFacingMode !== facingMode) {
dispatch(toggleCamera());
}
};
}
/**
* Signals to open the permission dialog for toggling camera remotely.
*
* @param {Function} onAllow - Callback to be executed if permission to toggle camera was granted.
* @param {string} initiatorId - The participant id of the requester.
* @returns {Object} - The open dialog action.
*/
export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: string) {
return openDialog(AllowToggleCameraDialog, {
onAllow,
initiatorId
});
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../../app/types';
import { translate } from '../../../i18n/functions';
import { getParticipantDisplayName } from '../../../participants/functions';
import Dialog from '../../../ui/components/web/Dialog';
interface IProps extends WithTranslation {
/**
* The participant id of the toggle camera requester.
*/
initiatorId: string;
/**
* Function to be invoked after permission to toggle camera granted.
*/
onAllow: () => void;
}
/**
* Dialog to allow toggling camera remotely.
*
* @returns {JSX.Element} - The allow toggle camera dialog.
*/
const AllowToggleCameraDialog = ({ onAllow, t, initiatorId }: IProps): JSX.Element => {
const initiatorName = useSelector((state: IReduxState) => getParticipantDisplayName(state, initiatorId));
return (
<Dialog
ok = {{ translationKey: 'dialog.allow' }}
onSubmit = { onAllow }
titleKey = 'dialog.allowToggleCameraTitle'>
<div>
{ t('dialog.allowToggleCameraDialog', { initiatorName }) }
</div>
</Dialog>
);
};
export default translate(AllowToggleCameraDialog);

View File

@@ -0,0 +1,4 @@
/**
* The payload name for remotely setting the camera facing mode message.
*/
export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message';

View File

@@ -2,7 +2,7 @@
* Loads the enabled stream effects.
*
* @param {Object} _store - The Redux store.
* @returns {Promsie} - A Promise which resolves with an array of the loaded effects.
* @returns {Promise} - A Promise which resolves with an array of the loaded effects.
*/
export default function loadEffects(_store: Object): Promise<Array<any>> {
return Promise.resolve([]);

View File

@@ -8,7 +8,7 @@ import logger from './logger';
* Loads the enabled stream effects.
*
* @param {Object} store - The Redux store.
* @returns {Promsie} - A Promise which resolves when all effects are created.
* @returns {Promise} - A Promise which resolves when all effects are created.
*/
export default function loadEffects(store: IStore): Promise<any> {
const state = store.getState();

View File

@@ -80,31 +80,31 @@ const Input = forwardRef<TextInput, IProps>(({
const { nativeEvent: { text } } = e;
onChange?.(text);
}, []);
}, [ onChange ]);
const clearInput = useCallback(() => {
onChange?.('');
}, []);
}, [ onChange ]);
const handleBlur = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => {
setFocused(false);
onBlur?.(e);
}, []);
}, [ onBlur ]);
const handleFocus = useCallback((e: NativeSyntheticEvent<TextInputFocusEventData>) => {
setFocused(true);
onFocus?.(e);
}, []);
}, [ onFocus ]);
const handleKeyPress = useCallback((e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
onKeyPress?.(e);
}, []);
}, [ onKeyPress ]);
const handleSubmitEditing = useCallback((e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
const { nativeEvent: { text } } = e;
onSubmitEditing?.(text);
}, []);
}, [ onSubmitEditing ]);
return (<View style = { [ styles.inputContainer, customStyles?.container ] }>
{label && <Text style = { styles.label }>{ label }</Text>}

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

@@ -16,6 +16,11 @@ export interface IProps {
*/
accessibilityLabel: string;
/**
* The context menu item background color.
*/
backgroundColor?: string;
/**
* Component children.
*/
@@ -163,6 +168,7 @@ const useStyles = makeStyles()(theme => {
const ContextMenuItem = ({
accessibilityLabel,
backgroundColor,
children,
className,
controls,
@@ -181,7 +187,7 @@ const ContextMenuItem = ({
textClassName }: IProps) => {
const { classes: styles, cx } = useStyles();
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
const style = backgroundColor ? { backgroundColor } : {};
const onKeyPressHandler = useCallback(e => {
// only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler
if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) {
@@ -223,6 +229,7 @@ const ContextMenuItem = ({
onKeyDown = { disabled ? undefined : onKeyDown }
onKeyPress = { disabled ? undefined : onKeyPressHandler }
role = { onClick ? role : undefined }
style = { style }
tabIndex = { onClick ? tabIndex : undefined }>
{customIcon ? customIcon
: icon && <Icon

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

@@ -260,6 +260,7 @@ export const commonStyles = (theme: Theme) => {
padding: 6,
textAlign: 'center' as const,
pointerEvents: 'all' as const,
display: 'flex',
boxShadow: '0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15)',
'& > div': {

View File

@@ -5,6 +5,8 @@ import zxcvbn from 'zxcvbn';
// The null UUID.
const NIL_UUID = parseUUID(NIL);
const _zxcvbnCache = new Map();
/**
* Checks if the given string is a valid UUID or not.
*
@@ -23,6 +25,24 @@ function isValidUUID(str: string) {
return !_.isEqual(uuid, NIL_UUID);
}
/**
* Checks a room name and caches the result.
*
* @param {string} roomName - The room name.
* @returns {Object}
*/
function _checkRoomName(roomName = '') {
if (_zxcvbnCache.has(roomName)) {
return _zxcvbnCache.get(roomName);
}
const result = zxcvbn(roomName);
_zxcvbnCache.set(roomName, result);
return result;
}
/**
* Returns true if the room name is considered a weak (insecure) one.
*
@@ -30,5 +50,7 @@ function isValidUUID(str: string) {
* @returns {boolean}
*/
export default function isInsecureRoomName(roomName = ''): boolean {
return !isValidUUID(roomName) && zxcvbn(roomName).score < 3;
// room names longer than 200 chars we consider secure
return !isValidUUID(roomName) && (roomName.length < 200 && _checkRoomName(roomName).score < 3);
}

View File

@@ -631,3 +631,29 @@ export function appendURLParam(url: string, name: string, value: string) {
return newUrl.toString();
}
/**
* Adds new hash param to a url string.
* Checks whether to use '?' or '&' as a separator (checks for already existing params).
*
* @param {string} url - The url to modify.
* @param {string} name - The param name to add.
* @param {string} value - The value for the param.
*
* @returns {string} - The modified url.
*/
export function appendURLHashParam(url: string, name: string, value: string) {
const newUrl = new URL(url);
const dummyUrl = new URL('https://example.com');
// Copy current hash-parameters without the '#' as search-parameters.
dummyUrl.search = newUrl.hash.substring(1);
// Set or update value with the searchParams-API.
dummyUrl.searchParams.append(name, value);
// Write back as hash parameters.
newUrl.hash = dummyUrl.searchParams.toString();
return newUrl.toString();
}

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

@@ -130,7 +130,10 @@ MiddlewareRegistry.register(store => next => action => {
// recipient. This logic tries to mitigate this risk.
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
if (shouldSendPrivateMessageTo) {
const participantExists = shouldSendPrivateMessageTo
&& getParticipantById(state, shouldSendPrivateMessageTo);
if (shouldSendPrivateMessageTo && participantExists) {
dispatch(openDialog(ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo

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

@@ -15,6 +15,7 @@ import Chat from '../../../chat/components/web/Chat';
import MainFilmstrip from '../../../filmstrip/components/web/MainFilmstrip';
import ScreenshareFilmstrip from '../../../filmstrip/components/web/ScreenshareFilmstrip';
import StageFilmstrip from '../../../filmstrip/components/web/StageFilmstrip';
import { isFilmstripDisabled } from '../../../filmstrip/functions.web';
import CalleeInfoContainer from '../../../invite/components/callee-info/CalleeInfoContainer';
import LargeVideo from '../../../large-video/components/LargeVideo.web';
import LobbyScreen from '../../../lobby/components/web/LobbyScreen';
@@ -63,6 +64,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
_backgroundAlpha?: number;
/**
* Whether the vertical/horizonal filmstrip is disabled.
*/
_filmstripDisabled: boolean;
/**
* Are any overlays visible?
*/
@@ -199,12 +205,14 @@ class Conference extends AbstractConference<IProps, any> {
*/
render() {
const {
_filmstripDisabled,
_isAnyOverlayVisible,
_layoutClassName,
_notificationsVisible,
_overflowDrawer,
_showLobby,
_showPrejoin,
_shouldDisplayTileView,
t
} = this.props;
@@ -230,7 +238,7 @@ class Conference extends AbstractConference<IProps, any> {
_showPrejoin || _showLobby || (<>
<StageFilmstrip />
<ScreenshareFilmstrip />
<MainFilmstrip />
{ (!_filmstripDisabled || _shouldDisplayTileView) && <MainFilmstrip /> }
</>)
}
</div>
@@ -396,6 +404,7 @@ function _mapStateToProps(state: IReduxState) {
return {
...abstractMapStateToProps(state),
_backgroundAlpha: backgroundAlpha,
_filmstripDisabled: isFilmstripDisabled(state),
_isAnyOverlayVisible: Boolean(getOverlayToRender(state)),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state) ?? ''],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,

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

@@ -0,0 +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

@@ -0,0 +1,61 @@
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 => {
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);
});
/**
* Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that
* will perform various chat related activities.
*
* @param {IJitsiConference} conference - The conference.
* @returns {void}
*/
function _addSetCameraFacingModeListener(conference: IJitsiConference) {
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any) => {
if (args && args.length >= 2) {
const [ sender, eventData ] = args;
if (eventData.name === CAMERA_FACING_MODE_MESSAGE) {
APP.store.dispatch(openAllowToggleCameraDialog(
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(eventData.facingMode)),
/* initiatorId */ sender._id
));
}
}
}
);
}

View File

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

@@ -1,4 +1,4 @@
import { IStore } from '../app/types';
import { IReduxState, IStore } from '../app/types';
import {
getActiveSpeakersToBeDisplayed,
getVirtualScreenshareParticipantOwnerId
@@ -95,3 +95,15 @@ export function updateRemoteParticipantsOnLeave(store: IStore, participantId: st
reorderedParticipants.delete(participantId)
&& store.dispatch(setRemoteParticipants(Array.from(reorderedParticipants)));
}
/**
* Returns whether tileview is completely disabled.
*
* @param {IReduxState} state - Redux state.
* @returns {boolean} - Whether tileview is completely disabled.
*/
export function isTileViewModeDisabled(state: IReduxState) {
const { tileView = {} } = state['features/base/config'];
return tileView.disabled;
}

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

@@ -56,7 +56,7 @@ const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numb
if (countryCode) {
return (
<td className = 'flag-cell'>
{countryCode === 'SIP'
{countryCode === 'SIP' || countryCode === 'SIP_AUDIO_ONLY'
? <Icon src = { IconSip } />
: <i className = { `flag iti-flag ${countryCode}` } />
}
@@ -129,6 +129,8 @@ const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numb
if (countryCode === 'SIP') {
countryName = t('info.sip');
} else if (countryCode === 'SIP_AUDIO_ONLY') {
countryName = t('info.sipAudioOnly');
} else {
countryName = t(`countries:countries.${countryCode}`);

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

@@ -11,6 +11,11 @@ import { navigationStyles } from './styles';
interface IProps {
/**
* Icon button color.
*/
color?: string;
/**
* Is the button disabled?
*/
@@ -42,7 +47,7 @@ interface IProps {
twoActions?: boolean;
}
const HeaderNavigationButton = ({ disabled, label, onPress, src, style, twoActions }: IProps) => {
const HeaderNavigationButton = ({ color, disabled, label, onPress, src, style, twoActions }: IProps) => {
let btnStyle;
let labelStyle;
@@ -64,6 +69,7 @@ const HeaderNavigationButton = ({ disabled, label, onPress, src, style, twoActio
{
src ? (
<IconButton
color = { color }
onPress = { onPress }
size = { 24 }
src = { src }

View File

@@ -10,7 +10,7 @@ export const settingsNavigationContainerRef = React.createRef<NavigationContaine
* @param {Object} params - Params to pass to the destination route.
* @returns {Function}
*/
export function navigate(name: string, params: Object) {
export function navigate(name: string, params?: Object) {
return settingsNavigationContainerRef.current?.navigate(name, params);
}

View File

@@ -3,11 +3,15 @@ import { createStackNavigator } from '@react-navigation/stack';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import LanguageSelectView from '../../../../../settings/components/native/LanguageSelectView';
import ProfileView from '../../../../../settings/components/native/ProfileView';
import SettingsView
from '../../../../../settings/components/native/SettingsView';
import { screen } from '../../../routes';
import {
languageSelectScreenOptions,
navigationContainerTheme,
profileSettingsScreenOptions,
settingsScreenOptions,
welcomeScreenOptions
} from '../../../screenOptions';
@@ -37,10 +41,19 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
const SettingsScreen = useCallback(() =>
(
<SettingsView
addBottomInset = { !isInWelcomePage }
scrollBounces = { isInWelcomePage } />
isInWelcomePage = { isInWelcomePage } />
), []);
const ProfileScreen = useCallback(() =>
(<ProfileView
isInWelcomePage = { isInWelcomePage } />)
, []);
const LanguageSelectScreen = useCallback(() =>
(<LanguageSelectView
isInWelcomePage = { isInWelcomePage } />)
, []);
return (
<NavigationContainer
independent = { true }
@@ -56,6 +69,20 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
}}>
{ SettingsScreen }
</SettingsStack.Screen>
<SettingsStack.Screen
component = { ProfileScreen }
name = { screen.settings.profile }
options = {{
...profileSettingsScreenOptions,
title: t('settingsView.profileSection')
}} />
<SettingsStack.Screen
component = { LanguageSelectScreen }
name = { screen.settings.language }
options = {{
...languageSelectScreenOptions,
title: t('settings.language')
}} />
</SettingsStack.Navigator>
</NavigationContainer>
);

View File

@@ -49,7 +49,11 @@ export function screenHeaderCloseButton(goBack: (e?: GestureResponderEvent | Rea
* {@code true}; otherwise, {@code false}.
*/
export function isPrejoinPageEnabled(stateful: IStateful) {
return getFeatureFlag(toState(stateful), PREJOIN_PAGE_ENABLED, true);
const state = toState(stateful);
const { prejoinConfig } = state['features/base/config'];
const isPrejoinEnabledInConfig = prejoinConfig?.enabled;
return getFeatureFlag(state, PREJOIN_PAGE_ENABLED, isPrejoinEnabledInConfig ?? true);
}
/**

View File

@@ -44,7 +44,9 @@ export const screen = {
help: 'Help',
privacy: 'Privacy',
terms: 'Terms'
}
},
profile: 'Profile',
language: 'Language'
},
unsafeRoomWarning: 'Unsafe Room Warning'
};

View File

@@ -196,6 +196,24 @@ export const preJoinScreenOptions = {
}
};
/**
* Screen options for profile setting.
*/
export const profileSettingsScreenOptions = {
headerStyle: {
backgroundColor: BaseTheme.palette.ui01
},
headerTitleStyle: {
color: BaseTheme.palette.text01
},
headerBackTitleVisible: false
};
/**
* Screen options for language select screen.
*/
export const languageSelectScreenOptions = profileSettingsScreenOptions;
/**
* Screen options for pre-join screen.
*/

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

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