Compare commits

..

21 Commits

Author SHA1 Message Date
titus.moldovan
63754f01a5 chore(deps) updates react-native-webrtc 2021-10-15 12:58:12 +03:00
Zoltán Ulrich
dcf677bdf0 fix(ios) Fixes RCTBridge not being released when JitsiMeet is trying to destroy its bridge on some OS versions 2021-10-14 13:05:37 +02:00
titus.moldovan
eaea2cdef0 chore(versions) bump iOS SDK version 2021-10-08 12:29:41 +03:00
Saúl Ibarra Corretgé
bcad87e894 chore(versions) bump iOS app version 2021-10-07 11:47:22 +02:00
Saúl Ibarra Corretgé
d500493649 chore(deps) update lib-jitsi-meet to fix iOS 15 issue
Not unmuting when "everyone starts muted".
2021-10-07 11:45:53 +02:00
tmoldovan8x8
8ad3ef0022 chore(versions) bump Android SDK version 2021-10-05 09:24:39 +03:00
Saúl Ibarra Corretgé
d0664d9ea0 chore(versions) bump iOS SDK version 2021-10-01 15:49:11 +02:00
Saúl Ibarra Corretgé
45c40d7afc chore(versions) bump iOS app version 2021-10-01 15:48:09 +02:00
Saúl Ibarra Corretgé
e33edfd2a5 fix(rn,conference) fix unmute when "everyone starts muted" is set
Since iOS 15 we really need the audio stream to exist, so make sure we don't
destroy it even when "everyone starts muted" is set, we'll just mute it.

Fixes: https://github.com/jitsi/jitsi-meet/issues/10053
2021-10-01 15:47:22 +02:00
Saúl Ibarra Corretgé
96b2a07172 chore(versions) bump iOS app version 2021-09-30 15:18:26 +02:00
Saúl Ibarra Corretgé
0e8c01af75 fix(ios) fix crash on WebSocket errors
Bacckport
748aa13747

Fixes:

~~~
Fatal Exception: NSInvalidArgumentException
0  CoreFoundation                 0x129708 __exceptionPreprocess
1  libobjc.A.dylib                0x287a8 objc_exception_throw
2  CoreFoundation                 0x19b9c8 -[__NSCFString characterAtIndex:].cold.1
3  CoreFoundation                 0x1a7a20 -[__NSPlaceholderDictionary initWithCapacity:].cold.1
4  CoreFoundation                 0x164c0 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]
5  CoreFoundation                 0x8de0 +[NSDictionary dictionaryWithObjects:forKeys:count:]
6  JitsiMeetSDK                   0x5704b4 -[RCTWebSocketModule webSocket:didFailWithError:] + 168 (RCTWebSocketModule.m:168)
7  JitsiMeetSDK                   0x54a5ec __33-[RCTSRWebSocket _failWithError:]_block_invoke_2 + 622 (RCTSRWebSocket.m:622)
8  libdispatch.dylib              0x2a84 _dispatch_call_block_and_release
9  libdispatch.dylib              0x481c _dispatch_client_callout
10 libdispatch.dylib              0xc004 _dispatch_lane_serial_drain
11 libdispatch.dylib              0xcc00 _dispatch_lane_invoke
12 libdispatch.dylib              0x174bc _dispatch_workloop_worker_thread
13 libsystem_pthread.dylib        0x37a4 _pthread_wqthread
14 libsystem_pthread.dylib        0xa74c start_wqthread
~~~
2021-09-30 15:12:46 +02:00
tmoldovan8x8
3049ad6dbf feat(android) target sdk 31 2021-09-30 14:06:37 +02:00
Saúl Ibarra Corretgé
9a30ad87a7 chore(versions) bump Android sdk version 2021-09-29 14:49:03 +02:00
Saúl Ibarra Corretgé
11c05560c4 chore(versions) bump Android app version 2021-09-29 14:15:43 +02:00
titus.moldovan
d92fa59c74 fix(android) disables uncompressed native libs usage 2021-09-29 14:15:11 +02:00
Saúl Ibarra Corretgé
4eb1d16faa chore(deps): update react-native-webrtc@1.92.2
Fixes a crash on Android
2021-09-29 14:13:47 +02:00
Saúl Ibarra Corretgé
91adc70724 chore(rn,versions) bump sdk version 2021-09-24 11:09:25 +02:00
Saúl Ibarra Corretgé
15b083c182 fix(ios) create audio track early
In iOS 15 we observe that not creating the audio track early may result in not
getting audio after unmuting for the first time.

Creating the audio track early means the first unmute doesn't need to add the
track to the conference, resulting in a much faster operation.

Note that creating the track early doesn't mean we will start unmuted, the track
will be muted.
2021-09-24 11:01:33 +02:00
titus.moldovan
26fdaf7b8d fix(rn) add default value when retrieving e2ee redux state 2021-09-24 10:11:52 +02:00
Saúl Ibarra Corretgé
64a9874d63 chore(rn,versions) bump sdk version 2021-09-23 14:13:51 +02:00
Saúl Ibarra Corretgé
753cffc458 fix(ios) fix black screen when using Bluetooth in iOS 15
Fixes: https://github.com/jitsi/jitsi-meet/issues/9996

On iOS 15 Bluetooth devices are reported twice for some reason, one with the
normal type "Bluetooth" but another type without a know (to me) type, and the
uid ends in "-reference".

While we send those unkwno devices to JS, we were not filtering them properly.
This patch skips them altogether.
2021-09-23 14:11:49 +02:00
60 changed files with 851 additions and 1112 deletions

View File

@@ -78,7 +78,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.2.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
if (!rootProject.ext.libreBuild) {
implementation 'com.google.android.gms:play-services-auth:16.0.1'
@@ -109,6 +109,7 @@ gradle.projectsEvaluated {
def dropboxActivity = """
<activity
android:configChanges="keyboard|orientation"
android:exported="true"
android:launchMode="singleTask"
android:name="com.dropbox.core.android.AuthActivity">
<intent-filter>

View File

@@ -4,6 +4,7 @@
android:installLocation="auto">
<application
android:allowBackup="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
@@ -13,6 +14,7 @@
android:resource="@xml/app_restrictions" />
<activity
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:name=".MainActivity"

View File

@@ -18,9 +18,9 @@ buildscript {
ext {
buildToolsVersion = "30.0.3"
compileSdkVersion = 30
compileSdkVersion = 31
minSdkVersion = 23
targetSdkVersion = 30
targetSdkVersion = 31
supportLibVersion = "28.0.0"
// The Maven artifact groupdId of the third-party react-native modules which

View File

@@ -24,6 +24,7 @@ android.enableDexingArtifactTransform.desugaring=false
android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
appVersion=21.4.0
sdkVersion=3.10.0
appVersion=21.4.1
sdkVersion=3.10.2

View File

@@ -82,7 +82,7 @@ class OngoingNotification {
}
Intent notificationIntent = new Intent(context, context.getClass());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ONGOING_CONFERENCE_CHANNEL_ID);
@@ -125,7 +125,7 @@ class OngoingNotification {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(action.getName());
PendingIntent pendingIntent
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
String title = context.getString(titleId);
return new NotificationCompat.Action(0, title, pendingIntent);
}

View File

@@ -1503,14 +1503,14 @@ export default {
*
* @param {boolean} didHaveVideo indicates if there was a camera video being
* used, before switching to screen sharing.
* @param {boolean} ignoreDidHaveVideo indicates if the camera video should be
* ignored when switching screen sharing off.
* @param {boolean} wasVideoMuted indicates if the video was muted, before
* switching to screen sharing.
* @return {Promise} resolved after the screen sharing is turned off, or
* rejected with some error (no idea what kind of error, possible GUM error)
* in case it fails.
* @private
*/
async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) {
async _turnScreenSharingOff(didHaveVideo) {
this._untoggleScreenSharing = null;
this.videoSwitchInProgress = true;
@@ -1554,7 +1554,7 @@ export default {
APP.store.dispatch(setScreenAudioShareState(false));
if (didHaveVideo && !ignoreDidHaveVideo) {
if (didHaveVideo) {
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
.then(([ stream ]) => {
logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`);
@@ -1605,10 +1605,9 @@ export default {
* @param {Array<string>} [options.desktopSharingSources] - Array with the
* sources that have to be displayed in the desktop picker window ('screen',
* 'window', etc.).
* @param {boolean} ignoreDidHaveVideo - if true ignore if video was on when sharing started.
* @return {Promise.<T>}
*/
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}, ignoreDidHaveVideo) {
async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) {
logger.debug(`toggleScreenSharing: ${toggle}`);
if (this.videoSwitchInProgress) {
return Promise.reject('Switch in progress.');
@@ -1634,7 +1633,7 @@ export default {
}
return this._untoggleScreenSharing
? this._untoggleScreenSharing(ignoreDidHaveVideo)
? this._untoggleScreenSharing()
: Promise.resolve();
},
@@ -2477,8 +2476,8 @@ export default {
});
APP.UI.addListener(
UIEvents.TOGGLE_SCREENSHARING, ({ enabled, audioOnly, ignoreDidHaveVideo }) => {
this.toggleScreenSharing(enabled, { audioOnly }, ignoreDidHaveVideo);
UIEvents.TOGGLE_SCREENSHARING, ({ enabled, audioOnly }) => {
this.toggleScreenSharing(enabled, { audioOnly });
}
);
},

View File

@@ -545,19 +545,6 @@ var config = {
// '__end'
// ],
// Holds values related to toolbar visibility control.
// toolbarConfig: {
// // Moved from interfaceConfig.INITIAL_TOOLBAR_TIMEOUT
// // The initial numer of miliseconds for the toolbar buttons to be visible on screen.
// initialTimeout: 20000,
// // Moved from interfaceConfig.TOOLBAR_TIMEOUT
// // Number of miliseconds for the toolbar buttons to be visible on screen.
// timeout: 4000,
// // Moved from interfaceConfig.TOOLBAR_ALWAYS_VISIBLE
// // Whether toolbar should be always visible or should hide after x miliseconds.
// alwaysVisible: false
// },
// Toolbar buttons which have their click event exposed through the API on
// `toolbarButtonClicked` event instead of executing the normal click routine.
// buttonsWithNotifyClick: [
@@ -909,10 +896,6 @@ var config = {
*/
// dynamicBrandingUrl: '',
// When true the user cannot add more images to be used as virtual background.
// Only the default ones from will be available.
// disableAddingBackgroundImages: false,
// Sets the background transparency level. '0' is fully transparent, '1' is opaque.
// backgroundAlpha: 1,

View File

@@ -213,7 +213,6 @@
}
#usermsg {
-ms-overflow-style: none;
border: 0px none;
border-radius:0;
box-shadow: none;
@@ -222,13 +221,8 @@
padding: 10px;
overflow-y: auto;
resize: none;
scrollbar-width: none;
width: 100%;
word-break: break-word;
&::-webkit-scrollbar {
display: none;
}
}
#usermsg:hover {

View File

@@ -75,7 +75,3 @@
top: 0;
height: 48px;
}
.shift-right .details-container {
margin-left: calc(#{$sidebarWidth} / 2);
}

View File

@@ -26,6 +26,11 @@ var interfaceConfig = {
CLOSE_PAGE_GUEST_HINT: false, // A html text to be shown to guests on the close page, false disables it
// Connection indicators (
// CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
// CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT,
// CONNECTION_INDICATOR_DISABLED) got moved to config.js.
DEFAULT_BACKGROUND: '#474747',
DEFAULT_LOCAL_DISPLAY_NAME: 'me',
DEFAULT_LOGO_URL: 'images/watermark.svg',
@@ -34,6 +39,9 @@ var interfaceConfig = {
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
// Deprecated. Please use disableModeratorIndicator from config.js
// DISABLE_FOCUS_INDICATOR: false,
/**
* If true, notifications regarding joining/leaving are no longer displayed.
*/
@@ -89,6 +97,7 @@ var interfaceConfig = {
*/
HIDE_INVITE_MORE_HEADER: false,
INITIAL_TOOLBAR_TIMEOUT: 20000,
JITSI_WATERMARK_LINK: 'https://jitsi.org',
LANG_DETECTION: true, // Allow i18n to detect the system language
@@ -174,6 +183,16 @@ var interfaceConfig = {
*/
SUPPORT_URL: 'https://community.jitsi.org/',
TOOLBAR_ALWAYS_VISIBLE: false,
/**
* DEPRECATED!
* This config was moved to config.js as `toolbarButtons`.
*/
// TOOLBAR_BUTTONS: [],
TOOLBAR_TIMEOUT: 4000,
// Browsers, in addition to those which do not fully support WebRTC, that
// are not supported and should show the unsupported browser page.
UNSUPPORTED_BROWSERS: [],
@@ -238,28 +257,6 @@ var interfaceConfig = {
PHONE_NUMBER_REGEX
*/
// -----------------DEPRECATED CONFIGS BELOW THIS LINE-----------------------------
// Connection indicators (
// CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
// CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT,
// CONNECTION_INDICATOR_DISABLED) got moved to config.js.
// Please use disableModeratorIndicator from config.js
// DISABLE_FOCUS_INDICATOR: false,
// Moved to config.js as `toolbarConfig.initialTimeout`.
// INITIAL_TOOLBAR_TIMEOUT: 20000,
// Moved to config.js as `toolbarConfig.alwaysVisible`.
// TOOLBAR_ALWAYS_VISIBLE: false,
// This config was moved to config.js as `toolbarButtons`.
// TOOLBAR_BUTTONS: [],
// Moved to config.js as `toolbarConfig.timeout`.
// TOOLBAR_TIMEOUT: 4000,
// Allow all above example options to include a trailing comma and
// prevent fear when commenting out the last value.
// eslint-disable-next-line sort-keys

View File

@@ -299,7 +299,7 @@ PODS:
- react-native-video/Video (= 5.1.1)
- react-native-video/Video (5.1.1):
- React-Core
- react-native-webrtc (1.92.1):
- react-native-webrtc (1.92.2):
- React-Core
- react-native-webview (11.0.2):
- React-Core
@@ -584,7 +584,7 @@ SPEC CHECKSUMS:
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8
react-native-webrtc: 77b969fe6bc5b7c93b455f93a13698812fb8bb4e
react-native-webrtc: 41526e4060dac373c18676f866962d4180ee47b9
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@
}
- (void)destroyReactNativeBridge {
[_bridgeWrapper invalidate];
_bridgeWrapper = nil;
}

View File

@@ -34,4 +34,6 @@
@property (nonatomic, readonly, strong) RCTBridge *bridge;
- (void)invalidate;
@end

View File

@@ -33,6 +33,10 @@
return self;
}
- (void)invalidate {
[_bridge invalidate];
}
#pragma mark helper methods for getting the packager URL
#if DEBUG

View File

@@ -248,8 +248,6 @@
"micPermissionDeniedError": "Vous n'avez pas autorisé l'utilisation de votre microphone. Vous pouvez toujours participer à la conférence, mais les autres ne vont pas vous entendre. Utilisez le bouton du microphone dans la barre d'adresse pour résoudre ce problème.",
"micTimeoutError": "Impossible de démarrer la source audio. Délai dépassé!",
"micUnknownError": "Vous ne pouvez pas utiliser le microphone pour une raison inconnue.",
"moderationAudioLabel": "Autoriser les participants à réactiver leur micro",
"moderationVideoLabel": "Autoriser les participants à démarrer leur vidéo",
"muteEveryoneElseDialog": "Une fois leur micro coupé, vous ne pourrez plus le réactiver, mais ils pourront l'activer par eux-mêmes à tout moment.",
"muteEveryoneElseTitle": "Couper le micro de tout le monde sauf de {{whom}} ?",
"muteEveryoneDialog": "Êtes-vous sûr de vouloir couper les micros de tout le monde ? Vous ne pourrez plus réactiver leur micro, mais ils pourront l'activer par eux-mêmes à tout moment.",
@@ -565,21 +563,12 @@
"close": "Fermer",
"headings": {
"lobby": "Salle d'attente ({{count}})",
"participantsList": "Participants de la réunion ({{count}})",
"waitingLobby": "Dans la salle d'attente ({{count}})"
"participantsList": "Participants de la réunion ({{count}})"
},
"actions": {
"allow": "Autoriser les participant à:",
"blockEveryoneMicCamera": "Bloquer tous les micros et caméras",
"invite": "Inviter quelqu'un",
"askUnmute": "Demander de réactiver le micro",
"mute": "Couper le micro",
"muteAll": "Couper le micro de tout le monde",
"muteEveryoneElse": "Couper le micro de tous les autres",
"startModeration": "Réactiver son micro ou démarrer sa vidéo",
"stopEveryonesVideo": "Couper toutes les caméras",
"stopVideo": "Couper la vidéo",
"unblockEveryoneMicCamera": "Débloquer tous les micros et caméras"
"muteAll": "couper le micro de tout le monde",
"stopVideo": "couper la vidéo"
}
},
"passwordSetRemotely": "défini par un autre participant",

View File

@@ -17,17 +17,21 @@
"inviteMoreMailSubject": "Rejónher la conferéncia {{appName}}",
"inviteMorePrompt": "Convidar mai de monde",
"linkCopied": "Ligam copiat al quichapapièrs",
"loading": "Recèrca de monde e de numèro de telefòn",
"loadingNumber": "Validacion del numèro de telefòn",
"loadingPeople": "Recèrca de monde de convidar",
"noResults": "Pas cap de resultat trobat",
"noValidNumbers": "Picatz lo numèro de telefòn",
"outlookEmail": "Outlook Email",
"searchNumbers": "Apondre de numèros de telefòn",
"searchPeople": "Cercar de monde",
"searchPeopleAndNumbers": "Cercar de monde o apondre lor numèros de telefòn",
"shareInvite": "Partejar invitacion conferéncia",
"shareLink": "Partejar lo ligam de la conferéncia per convidar de monde",
"shareStream": "Partejar la ligam de la difusion en dirècte",
"telephone": "Telefòn : {{number}}",
"title": "Convidatz de monde a vòstra conferéncia",
"yahooEmail": "Yahoo Email",
"phoneNumbers": "numèros de telefòn",
"searching": "Recèrca...",
"sipAddresses": "adreças sip"
"yahooEmail": "Yahoo Email"
},
"audioDevices": {
"bluetooth": "Bluetooth",
@@ -75,18 +79,12 @@
"smileysPanel": "Panèl d’Emoji",
"title": "Messatjariá",
"titleWithPolls": "Messatjariá",
"you": "vos",
"enter": "Dintrar dins la sala",
"tabs": {
"chat": "Messatjariá",
"polls": "Sondatges"
}
"you": "vos"
},
"chromeExtensionBanner": {
"buttonText": "Installar l’extension Chrome",
"dontShowAgain": "Me mostrar pas mai aquò",
"installExtensionText": "Installar l’extension per l’integracion de Google Calendar e Office 365",
"close": "Tampar"
"installExtensionText": "Installar l’extension per l’integracion de Google Calendar e Office 365"
},
"connectingOverlay": {
"joiningRoom": "Connexion a vòstra reünion…"
@@ -104,7 +102,8 @@
"FETCH_SESSION_ID": "Obtencion de session-id...",
"GET_SESSION_ID_ERROR": "Obténer l’error session-id : {{code}}",
"GOT_SESSION_ID": "Obtencion de session-id... Fach",
"LOW_BANDWIDTH": "La vidèo per {{displayName}} es estada copada per estalviar la banda passanta"
"LOW_BANDWIDTH": "La vidèo per {{displayName}} es estada copada per estalviar la banda passanta",
"RECONNECTING": "Un problèma ret s'es produita. Reconnexion en cors..."
},
"connectionindicator": {
"address": "Adreça :",
@@ -157,6 +156,7 @@
"ifHaveApp": "S’avètz ja l’aplicacion :",
"joinInApp": "Rejónher la conferéncia en utilizant l’aplicacion",
"launchWebButton": "Lançar del navigador",
"openApp": "Telecargar l’aplicacion",
"title": "Aviada de vòstra conferéncia dins {{app}}…",
"tryAgainButton": "Tornar ensajar del burèu"
},
@@ -214,12 +214,17 @@
"e2eeWarning": "AVERTIMENT : pas totes los participants d'aquesta conferéncia semblan poder suportar lo chiframent del cap a la fin. Se l'activatz poiràn pas vos veire nimai vos entendre.",
"enterDisplayName": "Volgatz picar vòstre nom aquí",
"error": "Error",
"externalInstallationMsg": "Avètz d'installar nòstra extension de partiment d'ecran.",
"externalInstallationTitle": "Extension requesida",
"goToStore": "Anar al webstore",
"gracefulShutdown": "Lo servici es actualament en mantenença. Ensajatz tornamai pus tard.",
"grantModeratorDialog": "Volètz vertadièrament far venir aqueste participant moderator ?",
"grantModeratorTitle": "Passar moderator",
"IamHost": "Soi l’òste",
"incorrectPassword": "Nom de compte o senhal incorrècte",
"incorrectRoomLockPassword": "Senhal incorrècte",
"inlineInstallationMsg": "Avètz d'installar nòstra extension de partiment d'ecran.",
"inlineInstallExtension": "Installar ara",
"internalError": "Òu ! Quicòm a pas foncionat. L'error seguenta s'es producha : {{error}}",
"internalErrorTitle": "Error intèrna",
"kickMessage": "Podètz contactat {{participantDisplayName}} per mai de detalhs.",
@@ -229,6 +234,7 @@
"kickTitle": "Ai ! {{participantDisplayName}} vos a forabandit de la conferéncia",
"liveStreaming": "La difusion en dirècte es estada arrestada",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossible pendent un enregistrament actiu",
"liveStreamingDisabledForGuestTooltip": "Los convidats pòdon pas aviar una difusion en dirècte.",
"liveStreamingDisabledTooltip": "Difusion en dirècte desactivada.",
"lockMessage": "Impossible de verrolhar la conferéncia.",
"lockRoom": "Ajustar un $t(lockRoomPasswordUppercase) a la conferéncia",
@@ -263,6 +269,7 @@
"readMore": "mai",
"recording": "Enregistrament",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible pendent una difusion activa",
"recordingDisabledForGuestTooltip": "Los convits pòdon pas lançar d’enregistraments.",
"recordingDisabledTooltip": "L’enregistrament es desactivat.",
"rejoinNow": "Participar ara",
"remoteControlAllowedMessage": "{{user}} a acceptat vòstra demanda de contraròtle alonhat !",
@@ -282,6 +289,10 @@
"screenSharingAudio": "Partejar l’àudio",
"screenSharingFailed": "Ops ! Quicòm a trucat, avèm pas pogut començar lo partiment d'ecran!",
"screenSharingFailedTitle": "Fracàs del partiment d'ecran !",
"screenSharingFailedToInstall": "Òu ! Fracàs de l'installacion de partatge d'ecran.",
"screenSharingFailedToInstallTitle": "Fracàs de l'installacion de partatge d'ecran",
"screenSharingFirefoxPermissionDeniedError": "Quicòm a fach mèuca quand èrem a ensajar de partejar vòstre ecran. Mercés de verificar qu’avètz donat l’autorizacion de lo partejar.",
"screenSharingFirefoxPermissionDeniedTitle": "Ops ! Avèm pas pogut aviar lo partatge d’ecran.",
"screenSharingPermissionDeniedError": "Òps ! Quicòm s'es pas ben passat amb l'autorizacion de vòstra autorizacion de partatge d'ecran. Mercés de recargar e tornar ensajar.",
"sendPrivateMessage": "Avètz recentament recebut un messatge privat. Avètz ensajat d’i respondre en privat, o volètz enviar lo messatge al grop ?",
"sendPrivateMessageCancel": "Enviar al grop",
@@ -294,6 +305,7 @@
"shareVideoTitle": "Partejar una vidèo",
"shareYourScreen": "Partejar vòstre ecran",
"shareYourScreenDisabled": "Lo partiment d’ecran es desactivat.",
"shareYourScreenDisabledForGuest": "Los convits pòdon pas partejar l’ecran.",
"startLiveStreaming": "Aviar una difusion en dirècte",
"startRecording": "Arrestar l'enregistrament",
"startRemoteControlErrorMessage": "Una error s'es produsida en ensajar de començar la session de contraròtle a distància !",
@@ -313,47 +325,9 @@
"userPassword": "senhal utilizaire",
"WaitForHostMsg": "La conferéncia <b>{{room}}</b> a pas encara començat. Se sètz l’òst volgatz ben vos identificar. Autrament esperatz qu’arribe l’òste.",
"WaitForHostMsgWOk": "La conferéncia <b>{{room}}</b> a pas encara començat. Se sètz l’òst volgatz ben clicar Ok per vos identificar. Autrament esperatz qu’arribe l’òste.",
"WaitingForHost": "En espèra de l’òste...",
"Yes": "Òc",
"yourEntireScreen": "Vòstre ecran complet",
"authenticationRequired": "Autentificacion requerida",
"login": "Connexion",
"muteEveryoneElsesVideoDialog": "Un còp la camèra desactivada, poiretz pas la reactivar, mas pòdon la reactivar quand vòlgan.",
"WaitingForHostTitle": "En espèra de l’òste...",
"embedMeeting": "Integrar la conferéncia",
"hideShareAudioHelper": "Afichar pas mai aquesta fenèstra",
"micTimeoutError": "Aviada impossibla de la font àudio. Relambi despassat !",
"moderationAudioLabel": "Permetre als convidats de se copar lo son",
"moderationVideoLabel": "Permetre als convidats d’aviar lor vidèo",
"muteEveryoneDialogModerationOn": "Los participants pòdon enviar una requèsta per parlar quand vòlgan.",
"muteEveryoneElsesVideoTitle": "Arrestar la vidèo de tot lo monde levat {{whom}} ?",
"muteEveryonesVideoTitle": "Arrestar la vidèo de tot lo monde ?",
"muteParticipantsVideoButton": "Arrestar la camèra",
"muteParticipantsVideoTitle": "Desactivar la camèra d’aqueste participant ?",
"noDropboxToken": "Cap de geton Dropbox pas valid",
"password": "Senhal",
"sessionRestarted": "Sonada reaviada pel pont",
"shareAudio": "Contunhar",
"shareAudioTitle": "Cossí partejar l’àudio",
"shareAudioWarningTitle": "Devètz arrestar lo partiment d’ecran abans lo partiment d’àudio",
"shareAudioWarningH1": "Se volètz partejar pas que l’àudio :",
"shareAudioWarningD1": "devètz arrestar lo partiment d’ecran abans lo partiment d’àudio.",
"shareMediaWarningGenericH2": "Se volètz partejar vòstre ecran e l’àudio",
"shareScreenWarningTitle": "Devètz arrestar lo partiment àudio abans lo partiment de l’ecran",
"shareScreenWarningH1": "Se volètz partejar pas que l’ecran :",
"shareScreenWarningD1": "devètz arrestar lo partiment àudio abans lo partiment de l’ecran.",
"sharedVideoLinkPlaceholder": "Ligam YouTube o ligam vidèo dirèct",
"userIdentifier": "Identificador utilizaire",
"cameraTimeoutError": "Aviada impossibla de la font vidèo. Relambi despassat !",
"muteEveryonesVideoDialog": "Los participants pòdon activar lor vidèo quand vòlgan.",
"muteEveryonesVideoDialogModerationOn": "Los participants pòdon enviar una requèsta per activar lor vidèo quand vòlgan.",
"muteEveryonesVideoDialogOk": "Desactivar",
"permissionErrorTitle": "Autorizacion requerida",
"videoLink": "ligam vidèo",
"viewUpgradeOptions": "Veire las opcions de mesa a nivèl",
"viewUpgradeOptionsTitle": "Avètz descobèrt una foncionalitat premium !",
"e2eeDisabledDueToMaxModeDescription": "Impossible d'activar lo chiframent del cap a la fin a causa d'un nombre tròp bèl de participants dins la conferéncia.",
"remoteUserControls": "Contraròtle a distància de l'utilizaire {{username}}",
"e2eeWillDisableDueToMaxModeDescription": "AVERTIMENT : lo chiframent del cap a la fin serà automaticament desactivat se mai de participants rejonhent la conferéncia."
"yourEntireScreen": "Vòstre ecran complet"
},
"dialOut": {
"statusMessage": "ara es {{status}}"
@@ -374,8 +348,7 @@
"good": "Bona",
"rateExperience": "Mercés de donar una nòta a vòstra experiéncia",
"veryBad": "Fòrça marrida",
"veryGood": "Fòrça bona",
"star": "Estela"
"veryGood": "Fòrça bona"
},
"helpView": {
"header": "Centre d’ajuda"
@@ -415,10 +388,7 @@
"numbers": "Sonar de numèros",
"password": "$t(lockRoomPasswordUppercase) :",
"title": "Partejar",
"tooltip": "Partejar lo ligam e las informacions d’aquesta conferéncia",
"copyNumber": "Copiar lo numèro",
"inviteTextiOSPersonal": "{{name}} vos convida a la conferéncia.",
"sip": "adreça SIP"
"tooltip": "Partejar lo ligam e las informacions d’aquesta conferéncia"
},
"inlineDialogFailure": {
"msg": "Avèm un pauc patit a manténer la connexion.",
@@ -449,7 +419,7 @@
"toggleScreensharing": "Caplevar entre camèra e partatge d'ecran",
"toggleShortcuts": "Mostrar o amagar los acorchis clavièr",
"videoMute": "Aviar o arrestar vòstra camèra",
"toggleParticipantsPane": "Afichar o amagar lo panèl dels participants"
"videoQuality": "Gerir la qualitat de las sonadas"
},
"liveStreaming": {
"busy": "Sèm a ensajar de liurar de ressorças flux. Mercés de tornar ensajar dins una estona.",
@@ -514,11 +484,7 @@
"passwordField": "Picatz lo senhal de la conferéncia",
"passwordJoinButton": "Rejónher",
"reject": "Regetar",
"toggleLabel": "Activar la sala d'espèra",
"admit": "Acceptar",
"admitAll": "Tot acceptar",
"rejectAll": "Tot regetar",
"errorMissingPassword": "Mercés de picar lo senhal de la conferéncia"
"toggleLabel": "Activar la sala d'espèra"
},
"localRecording": {
"clientState": {
@@ -563,6 +529,7 @@
"disconnected": "desconnectat",
"focus": "Focus de conferéncia",
"focusFail": "{{component}} es pas disponible - ensajatz tornamai dins {{ms}} sec",
"grantedTo": "Dreches moderator acordats a {{to}} !",
"invitedOneMember": "{{name}} es estat convidat",
"invitedThreePlusMembers": "{{name}} e {{count}} autres son estats convidats",
"invitedTwoMembers": "{{first}} e {{second}} son estats convidats",
@@ -588,22 +555,7 @@
"startSilentTitle": "Avètz jonch sens cap de sortida àudio !",
"suboptimalBrowserWarning": "Planhèm que vòstra experiéncia de la conferéncia siá pas de las bonas. Sèm a cercar de solucions per melhorar aquò, d’aquel temps, ensajatz un dels <a href='{{recommendedBrowserPageLink}}' target='_blank'>navegators compatibles</a>.",
"suboptimalExperienceTitle": "Avertiment del navegador",
"unmute": "Restablir lo son",
"allowAction": "Autorizar",
"videoMutedRemotelyTitle": "{{moderator}} a copat vòstra camèra",
"moderationInEffectTitle": "Lo moderator a amudit vòstre microfòn",
"moderationInEffectVideoTitle": "Lo moderator a blocat vòstra camèra",
"videoMutedRemotelyDescription": "La podètz totjorn tornar activar.",
"moderationInEffectCSTitle": "Lo moderator a blocat lo partiment d’ecran",
"moderationRequestFromModerator": "L’òste volriá que restabliguèssetz vòstre son",
"moderationRequestFromParticipant": "Vòl parlar",
"moderationStartedTitle": "Moderacion començada",
"moderationStoppedTitle": "Moderacion arrestada",
"moderationToggleDescription": "per {{participantDisplayName}}",
"raiseHandAction": "Levar la man",
"reactionSounds": "Desactivar los sons",
"groupTitle": "Notificacions",
"hostAskedUnmute": "Lo moderator vos que parletz"
"unmute": "Restablir lo son"
},
"passwordDigitsOnly": "Fins a {{number}} chifras",
"passwordSetRemotely": "causit per qualqu'un mai",
@@ -797,6 +749,7 @@
"chat": "Passar a la fenèstra chat",
"document": "Tampar los documents partejats",
"download": "Telecargar nòstra aplicacion",
"e2ee": "Chiframent del cap a la fin",
"embedMeeting": "Conferéncia integrada",
"feedback": "Daissar un comentari",
"fullScreen": "Passar al ecran complèt",
@@ -865,8 +818,8 @@
"muteEveryone": "Rendre mut tot lo monde",
"noAudioSignalDesc": "S’avètz pas volontàriament copat lo son a partir dels paramètres sistèma o material, pensatz de cambiar d’aparelh.",
"noAudioSignalDescSuggestion": "S’avètz pas volontàriament copat lo son a partir dels paramètres sistèma o material, pensatz d’utilizar un autre aparelh suggerit.",
"noAudioSignalDialInDesc": "Podètz tanben sonar en utilizant :",
"noAudioSignalDialInLinkDesc": "Numèros de sonada",
"noAudioSignalDialInDesc": "",
"noAudioSignalDialInLinkDesc": "",
"noAudioSignalTitle": "I a pas cap de son en entrada del microfòn !",
"noisyAudioInputDesc": "Sembla que vòstre microfòn mene bruch, pensatz de lo copar o de lo cambiar.",
"noisyAudioInputTitle": "Vòstre microfòn sembla brusent !",
@@ -884,25 +837,15 @@
"speakerStats": "Estatisticas parladors",
"startScreenSharing": "Aviar lo partatge d’ecran",
"startSubtitles": "Aviar los sostítols",
"startvideoblur": "Trebolar mon rèire-plan",
"stopScreenSharing": "Arrestar lo partatge d’ecran",
"stopSharedVideo": "Arrestar la vidèo Youtube",
"stopSubtitles": "Arrestar los sostítols",
"stopvideoblur": "Desactivar lo borrolatge del rèire-plan",
"talkWhileMutedPopup": "Ensajatz de parlar ? Vòstre microfòn es copat.",
"tileViewToggle": "Activar/Desactivar la vista en mosaïc",
"toggleCamera": "Passar a la camèra",
"videomute": "Aviar / Arrestar la camèra",
"openReactionsMenu": "Dobrir lo menú de reaccions",
"participants": "Participants",
"shareaudio": "Partejar l’àudio",
"silence": "Amudir",
"surprised": "Suspresa",
"selectBackground": "Seleccionar un rèireplan",
"audioSettings": "Paramètres àudio",
"videoSettings": "Paramètres vidèo",
"laugh": "Rire",
"muteEveryonesVideo": "Desactivar la camèra de tot lo monde",
"stopAudioSharing": "Arrestar lo partiment àudio",
"closeReactionsMenu": "Tampar lo menú de reaccions"
"videomute": "Aviar / Arrestar la camèra"
},
"transcribing": {
"ccButtonTooltip": "Aviar / Arrestat los sostítols",
@@ -950,6 +893,8 @@
"ld": "Bassa definicion",
"ldTooltip": "Difusion vidèo en bassa definicion",
"lowDefinition": "Bassa definicion",
"onlyAudioAvailable": "Pas que l’àudio es disponible",
"onlyAudioSupported": "Sèm compatibles solament amb l’àudio dins aqueste navigator.",
"sd": "SD",
"sdTooltip": "Difusion vidèo en definicion estandard",
"standardDefinition": "Definicion estandard"
@@ -966,10 +911,7 @@
"muted": "Mut",
"remoteControl": "Contraròtle alonhat",
"show": "Mostrar davant",
"videomute": "Lo participant a arrestat la camèra",
"domuteVideo": "Desactivar la camèra",
"domuteVideoOfOthers": "Desactivar la camèra dels demai",
"videoMuted": "Camèra desactivada"
"videomute": "Lo participant a arrestat la camèra"
},
"welcomepage": {
"accessibilityLabel": {
@@ -990,6 +932,7 @@
"goSmall": "Crear",
"headerTitle": "Jitsi Meet",
"info": "Infor",
"jitsiMeet": "Jitsi Meet",
"jitsiOnMobile": "Jitsi sus mobil –telecargatz nòstra aplicacion e començatz de conferéncias de pertot",
"join": "CREAR / REJÓNHER",
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservatz una URL de conferéncia</a> a l'avança ont sètz l'unic moderator.",
@@ -1001,95 +944,10 @@
"roomname": "Sasissètz un nom de sala",
"roomNameAllowedChars": "Lo nom de la conferéncia deu pas conténer aqueles caractèrs : ?, &, :, ', \", %, #.",
"roomnameHint": "Picatz lo nom o l’URL de la sala que volètz jónher. Podètz inventar un nom, cal pas que lo monde que volètz convidar lo sàpian.",
"secureMeetings": "Conferéncias seguras e de nauta qualitat",
"sendFeedback": "Mandar vòstra opinion",
"startMeeting": "Començar la reünion",
"terms": "Tèrmes",
"title": "Conferéncias vidèo securizadas amb plen de foncionalitats e complètament gratuitas",
"addMeetingName": "Nomenar la conferéncia",
"mobileDownLoadLinkIos": "Telecargar l'aplicacion per iOS",
"mobileDownLoadLinkAndroid": "Telecargar l'aplicacion per Android",
"mobileDownLoadLinkFDroid": "Telecargar l'aplicacion per F-Droid",
"headerSubtitle": "Conferéncias securizadas e de Nauta qualitat",
"logo": {
"policyLogo": "Logotipe de la politica",
"calendar": "Logotipe Calendar",
"microsoftLogo": "Logotipe Microsoft",
"logoDeepLinking": "Logotipe Jitsi meet",
"desktopPreviewThumbnail": "Vinheta d'apercebut del burèu",
"googleLogo": "Logotipe Google"
}
},
"virtualBackground": {
"addBackground": "Apondre un rèireplan",
"image7": "Solelh levant",
"backgroundEffectError": "Aplicacion fracassada de l’efeich al rèireplan.",
"apply": "Aplicar",
"title": "Rèireplans virtuals",
"blur": "Fosc",
"slightBlur": "Fosc leugièr",
"removeBackground": "Suprimir lo rèireplan",
"pleaseWait": "Esperatz...",
"none": "Cap",
"uploadedImage": "Imatge enviat {{index}}",
"deleteImage": "Suprimir l’imatge",
"image1": "Plaja",
"image2": "Paret blanca neutra",
"image3": "Sala voida blanca",
"image4": "Lampadari negre",
"image5": "Montanha",
"image6": "Forèst ",
"desktopShareError": "Creacion impossibla d’un partiment de burèu",
"desktopShare": "Partiment de burèu",
"webAssemblyWarning": "WebAssembly pas pres en carga"
},
"participantsPane": {
"headings": {
"lobby": "Sala d’espèra ({{count}})",
"participantsList": "Participants de la conferéncia ({{count}})",
"waitingLobby": "Dins la sala d'espèra ({{count}})"
},
"close": "Tancar",
"header": "Participants",
"actions": {
"allow": "Permetre als convidats de :",
"audioModeration": "Se tornar lo son",
"blockEveryoneMicCamera": "Blocar lo microfòn e la camèra del monde",
"invite": "Convidar qualqu'un",
"askUnmute": "Demandar a restablir lo son",
"mute": "Amudir",
"muteAll": "Amudir tot lo monde",
"muteEveryoneElse": "Amudir tot los demai",
"stopEveryonesVideo": "Arrestar la vidèo de tot lo monde",
"stopVideo": "Arrestar la vidèo",
"unblockEveryoneMicCamera": "Desblocar lo microfòn e la camèra de tot lo monde",
"videoModeration": "Aviar lor vidèo"
}
},
"jitsiHome": "{{logo}} Logotipe, mena a la pagina d'acuèlh",
"polls": {
"create": {
"addOption": "Apondre una opcion",
"answerPlaceholder": "Opcion {{index}}",
"create": "Crear un sondatge",
"cancel": "Anullar",
"pollOption": "Opcion sondarge {{index}}",
"pollQuestion": "Question del sondatge",
"questionPlaceholder": "Pausar una question",
"removeOption": "Suprimir l'opcion",
"send": "Enviar"
},
"answer": {
"skip": "Sautar",
"submit": "Enviar"
},
"results": {
"vote": "Votar",
"changeVote": "Cambiar lo vote",
"hideDetailedResults": "Rescondre los detalhs",
"showDetailedResults": "Mostrar los detalhs"
},
"notification": {
"title": "Un sondatge novèl es estat apondut a la conferéncia"
}
"title": "Conferéncias vidèo securizadas amb plen de foncionalitats e complètament gratuitas"
}
}

9
package-lock.json generated
View File

@@ -11117,8 +11117,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649",
"from": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649",
"version": "github:jitsi/lib-jitsi-meet#1bef6319fbdfdfb03b51d3f7efa348a111ac5805",
"from": "github:jitsi/lib-jitsi-meet#1bef6319fbdfdfb03b51d3f7efa348a111ac5805",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#4669790bb9020cc8f10c1d1f3823c26b08497547",
@@ -15201,9 +15201,8 @@
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
},
"react-native-webrtc": {
"version": "1.92.1",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.92.1.tgz",
"integrity": "sha512-cbFU1acL9aud/ohk/40jwahSNac4PwBKdmRPgXb9WqaGwxAr4CgSsrBRmruCISMdw0Pec+ZoZtYjXVuHzF51sg==",
"version": "github:react-native-webrtc/react-native-webrtc#aeb735154c9393bbfde40bf02b797e6eeb91f63a",
"from": "github:react-native-webrtc/react-native-webrtc#aeb735154c9393bbfde40bf02b797e6eeb91f63a",
"requires": {
"base64-js": "^1.1.2",
"cross-os": "^1.3.0",

View File

@@ -59,7 +59,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#3b8baa9d3be2839510abaa954357d0b0ab023649",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#1bef6319fbdfdfb03b51d3f7efa348a111ac5805",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@@ -92,7 +92,7 @@
"react-native-url-polyfill": "1.2.0",
"react-native-video": "5.1.1",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.92.1",
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#aeb735154c9393bbfde40bf02b797e6eeb91f63a",
"react-native-webview": "11.0.2",
"react-native-youtube-iframe": "2.1.1",
"react-redux": "7.1.0",

View File

@@ -1,3 +1,22 @@
diff --git a/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m b/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m
index d9387c4..a487da0 100644
--- a/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m
+++ b/node_modules/react-native/Libraries/WebSocket/RCTWebSocketModule.m
@@ -165,10 +165,10 @@ - (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error
NSNumber *socketID = [webSocket reactTag];
_contentHandlers[socketID] = nil;
_sockets[socketID] = nil;
- [self sendEventWithName:@"websocketFailed" body:@{
- @"message": error.localizedDescription,
- @"id": socketID
- }];
+ NSDictionary *body =
+ @{@"message" : error.localizedDescription ?: @"Undefined, error is nil",
+ @"id" : socketID ?: @(-1)};
+ [self sendEventWithName:@"websocketFailed" body:body];
}
- (void)webSocket:(RCTSRWebSocket *)webSocket
diff --git a/node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm b/node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm
index bd48f44..d243ed0 100644
--- a/node_modules/react-native/React/CxxBridge/RCTCxxBridge.mm

View File

@@ -2,7 +2,6 @@
import '../authentication/middleware';
import '../base/devices/middleware';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';
import '../keyboard-shortcuts/middleware';

View File

@@ -135,8 +135,11 @@ function _addConferenceListeners(conference, dispatch, state) {
// Remove the tracks from peerconnection as well.
for (const track of localTracks) {
if ((audioMuted && track.jitsiTrack.getType() === MEDIA_TYPE.AUDIO)
|| (videoMuted && track.jitsiTrack.getType() === MEDIA_TYPE.VIDEO)) {
const trackType = track.jitsiTrack.getType();
// Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
|| (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
dispatch(replaceLocalTrack(track.jitsiTrack, null, conference));
}
}

View File

@@ -84,7 +84,6 @@ export default [
'disableAEC',
'disableAGC',
'disableAP',
'disableAddingBackgroundImages',
'disableAudioLevels',
'disableChatSmileys',
'disableDeepLinking',
@@ -189,7 +188,6 @@ export default [
'subject',
'testing',
'toolbarButtons',
'toolbarConfig',
'useHostPageLocalStorage',
'useTurnUdp',
'videoQuality.persist',

View File

@@ -227,28 +227,6 @@ function _translateLegacyConfig(oldValue: Object) {
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
}
if (!oldValue.toolbarConfig) {
oldValue.toolbarConfig = {};
}
if (typeof oldValue.toolbarConfig.alwaysVisible !== 'boolean'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE === 'boolean') {
newValue.toolbarConfig.alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE;
}
if (typeof oldValue.toolbarConfig.initialTimeout !== 'number'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.INITIAL_TOOLBAR_TIMEOUT === 'number') {
newValue.toolbarConfig.initialTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
}
if (typeof oldValue.toolbarConfig.timeout !== 'number'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.TOOLBAR_TIMEOUT === 'number') {
newValue.toolbarConfig.timeout = interfaceConfig.TOOLBAR_TIMEOUT;
}
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key]);
if (filteredConferenceInfo.length) {

View File

@@ -261,20 +261,17 @@ export function showNoDataFromSourceVideoError(jitsiTrack) {
*
* @param {boolean} enabled - The state to toggle screen sharing to.
* @param {boolean} audioOnly - Only share system audio.
* @param {boolean} ignoreDidHaveVideo - Wether or not to ignore if video was on when sharing started.
* @returns {{
* type: TOGGLE_SCREENSHARING,
* on: boolean,
* audioOnly: boolean,
* ignoreDidHaveVideo: boolean
* audioOnly: boolean
* }}
*/
export function toggleScreensharing(enabled, audioOnly = false, ignoreDidHaveVideo = false) {
export function toggleScreensharing(enabled, audioOnly = false) {
return {
type: TOGGLE_SCREENSHARING,
enabled,
audioOnly,
ignoreDidHaveVideo
audioOnly
};
}

View File

@@ -149,11 +149,10 @@ MiddlewareRegistry.register(store => next => action => {
return;
}
const { enabled, audioOnly, ignoreDidHaveVideo } = action;
const { enabled, audioOnly } = action;
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, { enabled,
audioOnly,
ignoreDidHaveVideo });
audioOnly });
}
break;
@@ -179,7 +178,7 @@ MiddlewareRegistry.register(store => next => action => {
} else if (jitsiTrack.isLocal() && !(jitsiTrack.videoType === VIDEO_TYPE.DESKTOP)) {
APP.conference.setVideoMuteStatus();
} else if (jitsiTrack.isLocal() && muted && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
store.dispatch(toggleScreensharing(false, false, true));
store.dispatch(toggleScreensharing(false));
} else {
APP.UI.setVideoMuted(participantID);
}

View File

@@ -7,8 +7,8 @@
* @param {string} path - The URL path.
* @returns {string}
*/
export function extractFqnFromPath() {
const parts = window.location.pathname.split('/');
export function extractFqnFromPath(path: string) {
const parts = path.split('/');
const len = parts.length;
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
@@ -28,7 +28,7 @@ export function getDynamicBrandingUrl(state: Object) {
}
const baseUrl = state['features/base/config'].brandingDataUrl;
const fqn = extractFqnFromPath();
const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
if (baseUrl && fqn) {
return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;

View File

@@ -1,18 +0,0 @@
// @flow
import { APP_WILL_MOUNT } from '../base/app';
import { MiddlewareRegistry } from '../base/redux';
import { fetchCustomBrandingData } from './actions';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT: {
store.dispatch(fetchCustomBrandingData());
break;
}
}
return next(action);
});

View File

@@ -1,7 +1,6 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { type Image } from '../virtual-background/constants';
import {
SET_DYNAMIC_BRANDING_DATA,
@@ -114,15 +113,7 @@ const DEFAULT_STATE = {
* @public
* @type {boolean}
*/
useDynamicBrandingData: false,
/**
* An array of images to be used as virtual backgrounds instead of the default ones.
*
* @public
* @type {Array<Object>}
*/
virtualBackgrounds: []
useDynamicBrandingData: false
};
/**
@@ -140,8 +131,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
inviteDomain,
logoClickUrl,
logoImageUrl,
premeetingBackground,
virtualBackgrounds
premeetingBackground
} = action.value;
return {
@@ -156,8 +146,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
premeetingBackground,
customizationFailed: false,
customizationReady: true,
useDynamicBrandingData: true,
virtualBackgrounds: formatImages(virtualBackgrounds || [])
useDynamicBrandingData: true
};
}
case SET_DYNAMIC_BRANDING_FAILED: {
@@ -177,30 +166,3 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
return state;
});
/**
* Transforms the branding images into an array of Images objects ready
* to be used as virtual backgrounds.
*
* @param {Array<string>} images -
* @private
* @returns {{Props}}
*/
function formatImages(images: Array<string> | Array<Object>): Array<Image> {
return images.map((img, i) => {
let src;
let tooltip;
if (typeof img === 'object') {
({ src, tooltip } = img);
} else {
src = img;
}
return {
id: `branding-${i}`,
src,
tooltip
};
});
}

View File

@@ -131,7 +131,7 @@ export function sendJaasFeedbackMetadata(conference: Object, feedback: Object) {
return Promise.resolve();
}
const meetingFqn = extractFqnFromPath();
const meetingFqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
const feedbackData = {
...feedback,
sessionId: conference.sessionId,

View File

@@ -5,6 +5,7 @@ import React, { Component } from 'react';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { setColorAlpha } from '../../base/util';
import { fetchCustomBrandingData } from '../../dynamic-branding';
import { SharedVideo } from '../../shared-video/components/web';
import { Captions } from '../../subtitles/';
@@ -27,6 +28,11 @@ type Props = {
*/
_customBackgroundImageUrl: string,
/**
* Fetches the branding data.
*/
_fetchCustomBrandingData: Function,
/**
* Prop that indicates whether the chat is open.
*/
@@ -46,6 +52,14 @@ type Props = {
* @extends Component
*/
class LargeVideo extends Component<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this.props._fetchCustomBrandingData();
}
/**
* Implements React's {@link Component#render()}.
@@ -153,4 +167,8 @@ function _mapStateToProps(state) {
};
}
export default connect(_mapStateToProps)(LargeVideo);
const _mapDispatchToProps = {
_fetchCustomBrandingData: fetchCustomBrandingData
};
export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);

View File

@@ -3,202 +3,10 @@
import { type Dispatch } from 'redux';
import {
conferenceWillJoin,
getCurrentConference,
sendLocalParticipant,
setPassword
getCurrentConference
} from '../base/conference';
import { getLocalParticipant } from '../base/participants';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED,
SET_LOBBY_VISIBILITY
} from './actionTypes';
/**
* Tries to join with a preset password.
*
* @param {string} password - The password to join with.
* @returns {Function}
*/
export function joinWithPassword(password: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
dispatch(setPassword(conference, conference.join, password));
};
}
/**
* Action to be dispatched when a knocking poarticipant leaves before any response.
*
* @param {string} id - The ID of the participant.
* @returns {{
* id: string,
* type: KNOCKING_PARTICIPANT_LEFT
* }}
*/
export function knockingParticipantLeft(id: string) {
return {
id,
type: KNOCKING_PARTICIPANT_LEFT
};
}
/**
* Action to be executed when a participant starts knocking or an already knocking participant gets updated.
*
* @param {Object} participant - The knocking participant.
* @returns {{
* participant: Object,
* type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
* }}
*/
export function participantIsKnockingOrUpdated(participant: Object) {
return {
participant,
type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
};
}
/**
* Approves (lets in) or rejects a knocking participant.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approved - True if the participant is approved, false otherwise.
* @returns {Function}
*/
export function setKnockingParticipantApproval(id: string, approved: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
if (conference) {
if (approved) {
conference.lobbyApproveAccess(id);
} else {
conference.lobbyDenyAccess(id);
}
}
};
}
/**
* Action used to admit multiple participants in the conference.
*
* @param {Array<Object>} participants - A list of knocking participants.
* @returns {void}
*/
export function admitMultiple(participants: Array<Object>) {
return (dispatch: Function, getState: Function) => {
const conference = getCurrentConference(getState);
participants.forEach(p => {
conference.lobbyApproveAccess(p.id);
});
};
}
/**
* Approves the request of a knocking participant to join the meeting.
*
* @param {string} id - The id of the knocking participant.
* @returns {Function}
*/
export function approveKnockingParticipant(id: string) {
return (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
conference && conference.lobbyApproveAccess(id);
};
}
/**
* Denies the request of a knocking participant to join the meeting.
*
* @param {string} id - The id of the knocking participant.
* @returns {Function}
*/
export function rejectKnockingParticipant(id: string) {
return (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
conference && conference.lobbyDenyAccess(id);
};
}
/**
* Action to set the knocking state of the participant.
*
* @param {boolean} knocking - The new state.
* @returns {{
* state: boolean,
* type: SET_KNOCKING_STATE
* }}
*/
export function setKnockingState(knocking: boolean) {
return {
knocking,
type: SET_KNOCKING_STATE
};
}
/**
* Action to set the new state of the lobby mode.
*
* @param {boolean} enabled - The new state to set.
* @returns {{
* enabled: boolean,
* type: SET_LOBBY_MODE_ENABLED
* }}
*/
export function setLobbyModeEnabled(enabled: boolean) {
return {
enabled,
type: SET_LOBBY_MODE_ENABLED
};
}
/**
* Action to be dispatched when we failed to join with a password.
*
* @param {boolean} failed - True of recent password join failed.
* @returns {{
* failed: boolean,
* type: SET_PASSWORD_JOIN_FAILED
* }}
*/
export function setPasswordJoinFailed(failed: boolean) {
return {
failed,
type: SET_PASSWORD_JOIN_FAILED
};
}
/**
* Starts knocking and waiting for approval.
*
* @returns {Function}
*/
export function startKnocking() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { membersOnly } = state['features/base/conference'];
const localParticipant = getLocalParticipant(state);
dispatch(conferenceWillJoin(membersOnly));
// We need to update the conference object with the current display name, if approved
// we want to send that display name, it was not updated in case when pre-join is disabled
sendLocalParticipant(state, membersOnly);
membersOnly.joinLobby(localParticipant.name, localParticipant.email);
dispatch(setKnockingState(true));
};
}
import { SET_LOBBY_VISIBILITY } from './actionTypes';
/**
* Action to toggle lobby mode on or off.

View File

@@ -1,24 +1,10 @@
// @flow
import { type Dispatch } from 'redux';
import { appNavigate } from '../app/actions';
import { openDialog } from '../base/dialog';
import { DisableLobbyModeDialog, EnableLobbyModeDialog } from './components/native';
export * from './actions.any';
/**
* Cancels the ongoing knocking and abandons the join flow.
*
* @returns {Function}
*/
export function cancelKnocking() {
return async (dispatch: Dispatch<any>) => {
dispatch(appNavigate(undefined));
};
}
export * from './actions.web';
/**
* Action to show the dialog to disable lobby mode.

View File

@@ -2,22 +2,223 @@
import { type Dispatch } from 'redux';
import { maybeRedirectToWelcomePage } from '../app/actions';
import { appNavigate, maybeRedirectToWelcomePage } from '../app/actions';
import {
conferenceWillJoin,
getCurrentConference,
sendLocalParticipant,
setPassword
} from '../base/conference';
import { getLocalParticipant } from '../base/participants';
export * from './actions.any';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
declare var APP: Object;
/**
* Cancels the ongoing knocking and abandons the join flow.
* Cancels the ongoing knocking and abandones the join flow.
*
* @returns {Function}
*/
export function cancelKnocking() {
return async (dispatch: Dispatch<any>) => {
// when we are redirecting the library should handle any
// unload and clean of the connection.
APP.API.notifyReadyToClose();
dispatch(maybeRedirectToWelcomePage());
if (typeof APP !== 'undefined') {
// when we are redirecting the library should handle any
// unload and clean of the connection.
APP.API.notifyReadyToClose();
dispatch(maybeRedirectToWelcomePage());
return;
}
dispatch(appNavigate(undefined));
};
}
/**
* Tries to join with a preset password.
*
* @param {string} password - The password to join with.
* @returns {Function}
*/
export function joinWithPassword(password: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
dispatch(setPassword(conference, conference.join, password));
};
}
/**
* Action to be dispatched when a knocking poarticipant leaves before any response.
*
* @param {string} id - The ID of the participant.
* @returns {{
* id: string,
* type: KNOCKING_PARTICIPANT_LEFT
* }}
*/
export function knockingParticipantLeft(id: string) {
return {
id,
type: KNOCKING_PARTICIPANT_LEFT
};
}
/**
* Action to be executed when a participant starts knocking or an already knocking participant gets updated.
*
* @param {Object} participant - The knocking participant.
* @returns {{
* participant: Object,
* type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
* }}
*/
export function participantIsKnockingOrUpdated(participant: Object) {
return {
participant,
type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED
};
}
/**
* Approves (lets in) or rejects a knocking participant.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approved - True if the participant is approved, false otherwise.
* @returns {Function}
*/
export function setKnockingParticipantApproval(id: string, approved: boolean) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
if (conference) {
if (approved) {
conference.lobbyApproveAccess(id);
} else {
conference.lobbyDenyAccess(id);
}
}
};
}
/**
* Action used to admit multiple participants in the conference.
*
* @param {Array<Object>} participants - A list of knocking participants.
* @returns {void}
*/
export function admitMultiple(participants: Array<Object>) {
return (dispatch: Function, getState: Function) => {
const conference = getCurrentConference(getState);
participants.forEach(p => {
conference.lobbyApproveAccess(p.id);
});
};
}
/**
* Approves the request of a knocking participant to join the meeting.
*
* @param {string} id - The id of the knocking participant.
* @returns {Function}
*/
export function approveKnockingParticipant(id: string) {
return (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
conference && conference.lobbyApproveAccess(id);
};
}
/**
* Denies the request of a knocking participant to join the meeting.
*
* @param {string} id - The id of the knocking participant.
* @returns {Function}
*/
export function rejectKnockingParticipant(id: string) {
return (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
conference && conference.lobbyDenyAccess(id);
};
}
/**
* Action to set the knocking state of the participant.
*
* @param {boolean} knocking - The new state.
* @returns {{
* state: boolean,
* type: SET_KNOCKING_STATE
* }}
*/
export function setKnockingState(knocking: boolean) {
return {
knocking,
type: SET_KNOCKING_STATE
};
}
/**
* Action to set the new state of the lobby mode.
*
* @param {boolean} enabled - The new state to set.
* @returns {{
* enabled: boolean,
* type: SET_LOBBY_MODE_ENABLED
* }}
*/
export function setLobbyModeEnabled(enabled: boolean) {
return {
enabled,
type: SET_LOBBY_MODE_ENABLED
};
}
/**
* Action to be dispatched when we failed to join with a password.
*
* @param {boolean} failed - True of recent password join failed.
* @returns {{
* failed: boolean,
* type: SET_PASSWORD_JOIN_FAILED
* }}
*/
export function setPasswordJoinFailed(failed: boolean) {
return {
failed,
type: SET_PASSWORD_JOIN_FAILED
};
}
/**
* Starts knocking and waiting for approval.
*
* @returns {Function}
*/
export function startKnocking() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { membersOnly } = state['features/base/conference'];
const localParticipant = getLocalParticipant(state);
dispatch(conferenceWillJoin(membersOnly));
// We need to update the conference object with the current display name, if approved
// we want to send that display name, it was not updated in case when pre-join is disabled
sendLocalParticipant(state, membersOnly);
membersOnly.joinLobby(localParticipant.name, localParticipant.email);
dispatch(setKnockingState(true));
};
}

View File

@@ -33,7 +33,7 @@ export const LobbyParticipantItem = ({
openDrawerForParticipant
}: Props) => {
const { id } = p;
const [ admit, reject ] = useLobbyActions({ participantID: id });
const [ admit ] = useLobbyActions({ participantID: id });
const { t } = useTranslation();
return (
@@ -48,11 +48,6 @@ export const LobbyParticipantItem = ({
raisedHand = { p.raisedHand }
videoMediaState = { MEDIA_STATE.NONE }
youText = { t('chat.you') }>
<ParticipantActionButton
onClick = { reject }
primary = { false }>
{t('lobby.reject')}
</ParticipantActionButton>
<ParticipantActionButton
onClick = { admit }
primary = { true }>

View File

@@ -55,6 +55,7 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
const { webhookProxyUrl: url } = state['features/base/config'];
const { conference } = state['features/base/conference'];
const { jwt } = state['features/base/jwt'];
const { locationURL } = state['features/base/connection'];
const localParticipant = getLocalParticipant(state);
const headers = {
@@ -64,7 +65,7 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
const reqBody = {
meetingFqn: extractFqnFromPath(),
meetingFqn: extractFqnFromPath(locationURL.pathname),
sessionId: conference.sessionId,
submitted: Date.now(),
reactions,

View File

@@ -10,10 +10,10 @@ import {
/**
* Starts a search by criteria.
*
* @param {string | null} criteria - The search criteria.
* @param {string} criteria - The search criteria.
* @returns {Object}
*/
export function initSearch(criteria: string | null) {
export function initSearch(criteria: string) {
return {
type: INIT_SEARCH,
criteria

View File

@@ -10,7 +10,7 @@ import { connect } from '../../base/redux';
import { escapeRegexp } from '../../base/util';
import { initUpdateStats, initSearch } from '../actions';
import { SPEAKER_STATS_RELOAD_INTERVAL } from '../constants';
import { getSpeakerStats } from '../functions';
import { getSpeakerStats, getSearchCriteria } from '../functions';
import SpeakerStatsItem from './SpeakerStatsItem';
import SpeakerStatsLabels from './SpeakerStatsLabels';
@@ -36,7 +36,7 @@ type Props = {
/**
* The search criteria.
*/
_criteria: string | null,
_criteria: string,
/**
* The JitsiConference from which stats will be pulled.
@@ -140,9 +140,24 @@ class SpeakerStats extends Component<Props> {
const dominantSpeakerTime = statsModel.getTotalDominantSpeakerTime();
const hasLeft = statsModel.hasLeft();
let displayName;
if (statsModel.isLocalStats()) {
const { t } = this.props;
const meString = t('me');
displayName = this.props._localDisplayName;
displayName
= displayName ? `${displayName} (${meString})` : meString;
} else {
displayName
= this.props._stats[userId].getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
return (
<SpeakerStatsItem
displayName = { statsModel.getDisplayName() }
displayName = { displayName }
dominantSpeakerTime = { dominantSpeakerTime }
hasLeft = { hasLeft }
isDominantSpeaker = { isDominantSpeaker }
@@ -172,40 +187,7 @@ class SpeakerStats extends Component<Props> {
* @private
*/
_updateStats() {
this.props.dispatch(initUpdateStats(() => this._getSpeakerStats()));
}
/**
* Update the internal state with the latest speaker stats.
*
* @returns {Object}
* @private
*/
_getSpeakerStats() {
const stats = { ...this.props.conference.getSpeakerStats() };
for (const userId in stats) {
if (stats[userId]) {
if (stats[userId].isLocalStats()) {
const { t } = this.props;
const meString = t('me');
stats[userId].setDisplayName(
this.props._localDisplayName
? `${this.props._localDisplayName} (${meString})`
: meString
);
}
if (!stats[userId].getDisplayName()) {
stats[userId].setDisplayName(
interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME
);
}
}
}
return stats;
this.props.dispatch(initUpdateStats(() => this.props.conference.getSpeakerStats()));
}
}
@@ -216,7 +198,8 @@ class SpeakerStats extends Component<Props> {
* @private
* @returns {{
* _localDisplayName: ?string,
* _stats: Object
* _stats: Object,
* _criteria: string,
* }}
*/
function _mapStateToProps(state) {
@@ -230,7 +213,8 @@ function _mapStateToProps(state) {
* @type {string|undefined}
*/
_localDisplayName: localParticipant && localParticipant.name,
_stats: getSpeakerStats(state)
_stats: getSpeakerStats(state),
_criteria: getSearchCriteria(state)
};
}

View File

@@ -43,10 +43,10 @@ export function getSpeakerStats(state: Object) {
* Gets speaker stats search criteria.
*
* @param {*} state - The redux state.
* @returns {string | null} - The search criteria.
* @returns {string} - The search criteria.
*/
export function getSearchCriteria(state: Object) {
return state['features/speaker-stats']?.criteria;
return state['features/speaker-stats']?.criteria ?? '';
}
/**
@@ -161,14 +161,16 @@ export function filterBySearchCriteria(state: Object, stats: ?Object) {
const filteredStats = _.cloneDeep(stats ?? getSpeakerStats(state));
const criteria = getSearchCriteria(state);
if (criteria !== null) {
if (criteria) {
const searchRegex = new RegExp(criteria, 'gi');
for (const id in filteredStats) {
if (filteredStats[id].hasOwnProperty('_isLocalStats')) {
const name = filteredStats[id].getDisplayName();
filteredStats[id].hidden = !name || !name.match(searchRegex);
if (!name || !name.match(searchRegex)) {
filteredStats[id].hidden = true;
}
}
}
}

View File

@@ -18,7 +18,7 @@ import {
const INITIAL_STATE = {
stats: {},
pendingReorder: true,
criteria: null
criteria: ''
};
ReducerRegistry.register('features/speaker-stats', (state = _getInitialState(), action) => {

View File

@@ -55,6 +55,16 @@ export const SET_OVERFLOW_MENU_VISIBLE = 'SET_OVERFLOW_MENU_VISIBLE';
*/
export const SET_TOOLBAR_HOVERED = 'SET_TOOLBAR_HOVERED';
/**
* The type of the action which sets the permanent visibility of the Toolbox.
*
* {
* type: SET_TOOLBOX_ALWAYS_VISIBLE,
* alwaysVisible: boolean
* }
*/
export const SET_TOOLBOX_ALWAYS_VISIBLE = 'SET_TOOLBOX_ALWAYS_VISIBLE';
/**
* The type of the (redux) action which enables/disables the Toolbox.
*
@@ -77,6 +87,17 @@ export const SET_TOOLBOX_ENABLED = 'SET_TOOLBOX_ENABLED';
*/
export const SET_TOOLBOX_TIMEOUT = 'SET_TOOLBOX_TIMEOUT';
/**
* The type of the action which sets the delay in milliseconds after which
* the Toolbox visibility is to be changed.
*
* {
* type: SET_TOOLBOX_TIMEOUT_MS,
* timeoutMS: number
* }
*/
export const SET_TOOLBOX_TIMEOUT_MS = 'SET_TOOLBOX_TIMEOUT_MS';
/**
* The type of the (redux) action which shows/hides the Toolbox.
*

View File

@@ -1,45 +0,0 @@
// @flow
import type { Dispatch } from 'redux';
import {
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_VISIBLE
} from './actionTypes';
/**
* Enables/disables the toolbox.
*
* @param {boolean} enabled - True to enable the toolbox or false to disable it.
* @returns {{
* type: SET_TOOLBOX_ENABLED,
* enabled: boolean
* }}
*/
export function setToolboxEnabled(enabled: boolean): Object {
return {
type: SET_TOOLBOX_ENABLED,
enabled
};
}
/**
* Shows/hides the toolbox.
*
* @param {boolean} visible - True to show the toolbox or false to hide it.
* @returns {Function}
*/
export function setToolboxVisible(visible: boolean): Object {
return (dispatch: Dispatch<any>, getState: Function) => {
const { toolbarConfig: { alwaysVisible } } = getState()['features/base/config'];
if (!visible && alwaysVisible) {
return;
}
dispatch({
type: SET_TOOLBOX_VISIBLE,
visible
});
};
}

View File

@@ -1,28 +1,160 @@
// @flow
/* @flow */
import type { Dispatch } from 'redux';
import {
CLEAR_TOOLBOX_TIMEOUT,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ALWAYS_VISIBLE,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
import { TOGGLE_TOOLBOX_VISIBLE } from './actionTypes';
export * from './actions.any';
/**
* Signals that toolbox timeout should be cleared.
*
* @returns {{
* type: CLEAR_TOOLBOX_TIMEOUT
* }}
*/
export function clearToolboxTimeout(): Object {
return {
type: CLEAR_TOOLBOX_TIMEOUT
};
}
/**
* Shows/hides the overflow menu.
*
* @param {boolean} visible - True to show it or false to hide it.
* @returns {{
* type: SET_OVERFLOW_MENU_VISIBLE,
* visible: boolean
* }}
*/
export function setOverflowMenuVisible(visible: boolean): Object {
return {
type: SET_OVERFLOW_MENU_VISIBLE,
visible
};
}
/**
* Signals that toolbar is hovered value should be changed.
*
* @param {boolean} hovered - Flag showing whether toolbar is hovered.
* @returns {{
* type: SET_TOOLBAR_HOVERED,
* hovered: boolean
* }}
*/
export function setToolbarHovered(hovered: boolean): Object {
return {
type: SET_TOOLBAR_HOVERED,
hovered
};
}
/**
* Signals that always visible toolbars value should be changed.
*
* @param {boolean} alwaysVisible - Value to be set in redux store.
* @returns {{
* type: SET_TOOLBOX_ALWAYS_VISIBLE,
* alwaysVisible: boolean
* }}
*/
export function setToolboxAlwaysVisible(alwaysVisible: boolean): Object {
return {
type: SET_TOOLBOX_ALWAYS_VISIBLE,
alwaysVisible
};
}
/* eslint-disable flowtype/space-before-type-colon */
/**
* Enables/disables the toolbox.
*
* @param {boolean} enabled - True to enable the toolbox or false to disable it.
* @returns {{
* type: SET_TOOLBOX_ENABLED,
* enabled: boolean
* }}
*/
export function setToolboxEnabled(enabled: boolean): Object {
return {
type: SET_TOOLBOX_ENABLED,
enabled
};
}
/**
* Dispatches an action which sets new timeout and clears the previous one.
*
* @param {Function} handler - Function to be invoked after the timeout.
* @param {number} timeoutMS - Delay.
* @returns {{
* type: SET_TOOLBOX_TIMEOUT,
* handler: Function,
* timeoutMS: number
* }}
*/
export function setToolboxTimeout(handler: Function, timeoutMS: number)
: Object {
return {
type: SET_TOOLBOX_TIMEOUT,
handler,
timeoutMS
};
}
/* eslint-enable flowtype/space-before-type-colon */
/**
* Dispatches an action which sets new toolbox timeout value.
*
* @param {number} timeoutMS - Delay.
* @returns {{
* type: SET_TOOLBOX_TIMEOUT_MS,
* timeoutMS: number
* }}
*/
export function setToolboxTimeoutMS(timeoutMS: number): Object {
return {
type: SET_TOOLBOX_TIMEOUT_MS,
timeoutMS
};
}
/**
* Shows/hides the toolbox.
*
* @param {boolean} visible - True to show the toolbox or false to hide it.
* @returns {{
* type: SET_TOOLBOX_VISIBLE,
* visible: boolean
* }}
*/
export function setToolboxVisible(visible: boolean): Object {
return {
type: SET_TOOLBOX_VISIBLE,
visible
};
}
/**
* Action to toggle the toolbox visibility.
*
* @returns {Function}
* @returns {{
* type: TOGGLE_TOOLBOX_VISIBLE
* }}
*/
export function toggleToolboxVisible() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { visible } = state['features/toolbox'];
if (visible && alwaysVisible) {
return;
}
dispatch({
type: TOGGLE_TOOLBOX_VISIBLE
});
return {
type: TOGGLE_TOOLBOX_VISIBLE
};
}

View File

@@ -2,22 +2,23 @@
import type { Dispatch } from 'redux';
import { overwriteConfig } from '../base/config';
import { isLayoutTileView } from '../video-layout';
import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_TIMEOUT
SET_OVERFLOW_DRAWER
} from './actionTypes';
import { setToolboxVisible } from './actions';
import { getToolbarTimeout } from './functions';
import {
clearToolboxTimeout,
setToolboxTimeout,
setToolboxTimeoutMS,
setToolboxVisible
} from './actions.native';
export * from './actions.any';
declare var interfaceConfig: Object;
export * from './actions.native';
/**
* Docks/undocks the Toolbox.
@@ -27,9 +28,7 @@ export * from './actions.any';
*/
export function dockToolbox(dock: boolean): Function {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { visible } = state['features/toolbox'];
const toolbarTimeout = getToolbarTimeout(state);
const { timeoutMS, visible } = getState()['features/toolbox'];
if (dock) {
// First make sure the toolbox is shown.
@@ -40,7 +39,7 @@ export function dockToolbox(dock: boolean): Function {
dispatch(
setToolboxTimeout(
() => dispatch(hideToolbox()),
toolbarTimeout));
timeoutMS));
} else {
dispatch(showToolbox());
}
@@ -74,9 +73,11 @@ export function fullScreenChanged(fullScreen: boolean) {
export function hideToolbox(force: boolean = false): Function {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { hovered } = state['features/toolbox'];
const toolbarTimeout = getToolbarTimeout(state);
const {
alwaysVisible,
hovered,
timeoutMS
} = state['features/toolbox'];
if (alwaysVisible) {
return;
@@ -94,7 +95,7 @@ export function hideToolbox(force: boolean = false): Function {
dispatch(
setToolboxTimeout(
() => dispatch(hideToolbox()),
toolbarTimeout));
timeoutMS));
} else {
dispatch(setToolboxVisible(false));
}
@@ -127,13 +128,9 @@ export function showToolbox(timeout: number = 0): Object {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const {
toolbarConfig: { initialTimeout, alwaysVisible },
toolbarConfig
} = state['features/base/config'];
const toolbarTimeout = getToolbarTimeout(state);
const {
alwaysVisible,
enabled,
timeoutMS,
visible,
overflowDrawer
} = state['features/toolbox'];
@@ -146,17 +143,11 @@ export function showToolbox(timeout: number = 0): Object {
// If the Toolbox is always visible, there's no need for a timeout
// to toggle its visibility.
if (!alwaysVisible) {
if (typeof initialTimeout === 'number') {
// reset `initialTimeout` once it is consumed once
dispatch(overwriteConfig({ toolbarConfig: {
...toolbarConfig,
initialTimeout: null
} }));
}
dispatch(
setToolboxTimeout(
() => dispatch(hideToolbox()),
timeout || initialTimeout || toolbarTimeout));
timeout || timeoutMS));
dispatch(setToolboxTimeoutMS(interfaceConfig.TOOLBAR_TIMEOUT));
}
}
};
@@ -189,73 +180,9 @@ export function hideToolboxOnTileView() {
const state = getState();
const { overflowDrawer } = state['features/toolbox'];
if (!overflowDrawer && isLayoutTileView(state)) {
dispatch(hideToolbox(true));
}
};
}
/**
* Signals that toolbox timeout should be cleared.
*
* @returns {{
* type: CLEAR_TOOLBOX_TIMEOUT
* }}
*/
export function clearToolboxTimeout(): Object {
return {
type: CLEAR_TOOLBOX_TIMEOUT
};
}
/**
* Shows/hides the overflow menu.
*
* @param {boolean} visible - True to show it or false to hide it.
* @returns {{
* type: SET_OVERFLOW_MENU_VISIBLE,
* visible: boolean
* }}
*/
export function setOverflowMenuVisible(visible: boolean): Object {
return {
type: SET_OVERFLOW_MENU_VISIBLE,
visible
};
}
/**
* Signals that toolbar is hovered value should be changed.
*
* @param {boolean} hovered - Flag showing whether toolbar is hovered.
* @returns {{
* type: SET_TOOLBAR_HOVERED,
* hovered: boolean
* }}
*/
export function setToolbarHovered(hovered: boolean): Object {
return {
type: SET_TOOLBAR_HOVERED,
hovered
};
}
/**
* Dispatches an action which sets new timeout and clears the previous one.
*
* @param {Function} handler - Function to be invoked after the timeout.
* @param {number} timeoutMS - Delay.
* @returns {{
* type: SET_TOOLBOX_TIMEOUT,
* handler: Function,
* timeoutMS: number
* }}
*/
export function setToolboxTimeout(handler: Function, timeoutMS: number): Object {
return {
type: SET_TOOLBOX_TIMEOUT,
handler,
timeoutMS
};
}

View File

@@ -29,5 +29,3 @@ export const THRESHOLDS = [
];
export const NOT_APPLICABLE = 'N/A';
export const TOOLBAR_TIMEOUT = 4000;

View File

@@ -60,14 +60,12 @@ export function getMovableButtons(width: number): Set<string> {
*/
export function isToolboxVisible(stateful: Object | Function) {
const state = toState(stateful);
const { toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { enabled, visible } = state['features/toolbox'];
const { alwaysVisible, enabled, visible } = state['features/toolbox'];
const participantCount = getParticipantCountWithFake(state);
const alwaysVisibleFlag = getFeatureFlag(state, TOOLBOX_ALWAYS_VISIBLE, false);
const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true);
return enabledFlag && enabled
&& (alwaysVisible || visible || participantCount === 1 || alwaysVisibleFlag);
return enabledFlag && enabled && (alwaysVisible || visible || participantCount === 1 || alwaysVisibleFlag);
}
/**

View File

@@ -3,8 +3,6 @@
import { getToolbarButtons } from '../base/config';
import { hasAvailableDevices } from '../base/devices';
import { TOOLBAR_TIMEOUT } from './constants';
/**
* Helper for getting the height of the toolbox.
*
@@ -39,8 +37,9 @@ export function isButtonEnabled(name: string, state: Object) {
* otherwise.
*/
export function isToolboxVisible(state: Object) {
const { iAmSipGateway, toolbarConfig: { alwaysVisible } } = state['features/base/config'];
const { iAmSipGateway } = state['features/base/config'];
const {
alwaysVisible,
timeoutID,
visible
} = state['features/toolbox'];
@@ -102,15 +101,3 @@ export function showOverflowDrawer(state: Object) {
export function isToolboxEnabled(state: Object) {
return state['features/toolbox'].enabled;
}
/**
* Returns the toolbar timeout from config or the default value.
*
* @param {Object} state - The state from the Redux store.
* @returns {number} - Toolbar timeout in miliseconds.
*/
export function getToolbarTimeout(state: Object) {
const { toolbarConfig: { timeout } } = state['features/base/config'];
return timeout || TOOLBAR_TIMEOUT;
}

View File

@@ -8,66 +8,121 @@ import {
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ALWAYS_VISIBLE,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
declare var interfaceConfig: Object;
/**
* Initial state of toolbox's part of Redux store.
* Returns initial state for toolbox's part of Redux store.
*
* @private
* @returns {{
* alwaysVisible: boolean,
* enabled: boolean,
* hovered: boolean,
* overflowDrawer: boolean,
* overflowMenuVisible: boolean,
* timeoutID: number,
* timeoutMS: number,
* visible: boolean
* }}
*/
const INITIAL_STATE = {
function _getInitialState() {
// Does the toolbar eventually fade out, or is it always visible?
let alwaysVisible = false;
/**
* The indicator which determines whether the Toolbox is enabled.
*
* @type {boolean}
*/
enabled: true,
// Toolbar (initial) visibility.
let visible = false;
/**
* The indicator which determines whether a Toolbar in the Toolbox is
* hovered.
*
* @type {boolean}
*/
hovered: false,
// Default toolbox timeout for mobile app.
let timeoutMS = 5000;
/**
* The indicator which determines whether the overflow menu(s) are to be displayed as drawers.
*
* @type {boolean}
*/
overflowDrawer: false,
if (typeof interfaceConfig !== 'undefined') {
if (interfaceConfig.INITIAL_TOOLBAR_TIMEOUT) {
timeoutMS = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
}
if (typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE !== 'undefined') {
alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE;
}
}
/**
* The indicator which determines whether the OverflowMenu is visible.
*
* @type {boolean}
*/
overflowMenuVisible: false,
// When the toolbar is always visible, it must initially be visible too.
if (alwaysVisible === true) {
visible = true;
}
/**
* A number, non-zero value which identifies the timer created by a call
* to setTimeout().
*
* @type {number|null}
*/
timeoutID: null,
return {
/**
* The indicator which determines whether the Toolbox should always be
* visible. When false, the toolbar will fade out after timeoutMS.
*
* @type {boolean}
*/
alwaysVisible,
/**
* The indicator which determines whether the Toolbox is enabled.
*
* @type {boolean}
*/
enabled: true,
/**
* The indicator that determines whether the Toolbox is visible.
*
* @type {boolean}
*/
visible: false
};
/**
* The indicator which determines whether a Toolbar in the Toolbox is
* hovered.
*
* @type {boolean}
*/
hovered: false,
/**
* The indicator which determines whether the overflow menu(s) are to be displayed as drawers.
*
* @type {boolean}
*/
overflowDrawer: false,
/**
* The indicator which determines whether the OverflowMenu is visible.
*
* @type {boolean}
*/
overflowMenuVisible: false,
/**
* A number, non-zero value which identifies the timer created by a call
* to setTimeout() with timeoutMS.
*
* @type {number|null}
*/
timeoutID: null,
/**
* The delay in milliseconds before timeoutID executes (after its
* initialization).
*
* @type {number}
*/
timeoutMS,
/**
* The indicator that determines whether the Toolbox is visible.
*
* @type {boolean}
*/
visible
};
}
ReducerRegistry.register(
'features/toolbox',
(state: Object = INITIAL_STATE, action: Object) => {
(state: Object = _getInitialState(), action: Object) => {
switch (action.type) {
case CLEAR_TOOLBOX_TIMEOUT:
return {
@@ -99,6 +154,13 @@ ReducerRegistry.register(
hovered: action.hovered
};
case SET_TOOLBOX_ALWAYS_VISIBLE:
return {
...state,
alwaysVisible: action.alwaysVisible,
visible: action.alwaysVisible === true ? true : state.visible
};
case SET_TOOLBOX_ENABLED:
return {
...state,
@@ -108,14 +170,21 @@ ReducerRegistry.register(
case SET_TOOLBOX_TIMEOUT:
return {
...state,
timeoutID: action.timeoutID
timeoutID: action.timeoutID,
timeoutMS: action.timeoutMS
};
case SET_TOOLBOX_TIMEOUT_MS:
return {
...state,
timeoutMS: action.timeoutMS
};
case SET_TOOLBOX_VISIBLE:
return set(state, 'visible', action.visible);
return set(state, 'visible', state.alwaysVisible || action.visible);
case TOGGLE_TOOLBOX_VISIBLE:
return set(state, 'visible', !state.visible);
return set(state, 'visible', state.alwaysVisible || !state.visible);
}
return state;

View File

@@ -23,7 +23,6 @@ import {
getRemoteParticipants,
muteRemoteParticipant
} from '../base/participants';
import { toggleScreensharing } from '../base/tracks';
import { isModerationNotificationDisplayed } from '../notifications';
declare var APP: Object;
@@ -35,10 +34,9 @@ const logger = getLogger(__filename);
*
* @param {boolean} enable - Whether to mute or unmute.
* @param {MEDIA_TYPE} mediaType - The type of the media channel to mute.
* @param {boolean} stopScreenSharing - Whether or not to stop the screensharing.
* @returns {Function}
*/
export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE, stopScreenSharing: boolean = false) {
export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) {
return (dispatch: Dispatch<any>, getState: Function) => {
const isAudio = mediaType === MEDIA_TYPE.AUDIO;
@@ -57,10 +55,6 @@ export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE, stopScreenShar
return;
}
if (enable && stopScreenSharing) {
dispatch(toggleScreensharing(false, false, true));
}
sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable }));
dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true)
: setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true));
@@ -103,7 +97,7 @@ export function muteAllParticipants(exclude: Array<string>, mediaType: MEDIA_TYP
const localId = getLocalParticipant(state).id;
if (!exclude.includes(localId)) {
dispatch(muteLocal(true, mediaType, true));
dispatch(muteLocal(true, mediaType));
}
getRemoteParticipants(state).forEach((p, id) => {

View File

@@ -105,7 +105,7 @@ export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRe
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO));
if (this.state.audioModerationEnabled) {
dispatch(requestEnableAudioModeration());
} else if (this.state.audioModerationEnabled !== undefined) {
} else {
dispatch(requestDisableAudioModeration());
}

View File

@@ -106,7 +106,7 @@ export default class AbstractMuteEveryonesVideoDialog<P: Props>
dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO));
if (this.state.moderationEnabled) {
dispatch(requestEnableVideoModeration());
} else if (this.state.moderationEnabled !== undefined) {
} else {
dispatch(requestDisableVideoModeration());
}

View File

@@ -1,125 +0,0 @@
// @flow
import React, { useCallback, useRef } from 'react';
import uuid from 'uuid';
import { translate } from '../../base/i18n';
import { Icon, IconPlusCircle } from '../../base/icons';
import { VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
import { resizeImage } from '../functions';
import logger from '../logger';
type Props = {
/**
* Callback used to set the 'loading' state of the parent component.
*/
setLoading: Function,
/**
* Callback used to set the options.
*/
setOptions: Function,
/**
* Callback used to set the storedImages array.
*/
setStoredImages: Function,
/**
* A list of images locally stored.
*/
storedImages: Array<Image>,
/**
* If a label should be displayed alongside the button.
*/
showLabel: boolean,
/**
* Used for translation.
*/
t: Function
}
/**
* Component used to upload an image.
*
* @param {Object} Props - The props of the component.
* @returns {React$Node}
*/
function UploadImageButton({
setLoading,
setOptions,
setStoredImages,
showLabel,
storedImages,
t
}: Props) {
const uploadImageButton: Object = useRef(null);
const uploadImageKeyPress = useCallback(e => {
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
uploadImageButton.current.click();
}
}, [ uploadImageButton.current ]);
const uploadImage = useCallback(async e => {
const reader = new FileReader();
const imageFile = e.target.files;
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const url = await resizeImage(reader.result);
const uuId = uuid.v4();
setStoredImages([
...storedImages,
{
id: uuId,
src: url
}
]);
setOptions({
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
enabled: true,
url,
selectedThumbnail: uuId
});
};
logger.info('New virtual background image uploaded!');
reader.onerror = () => {
setLoading(false);
logger.error('Failed to upload virtual image!');
};
}, [ storedImages ]);
return (
<>
{showLabel && <label
aria-label = { t('virtualBackground.uploadImage') }
className = 'file-upload-label'
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }
tabIndex = { 0 } >
<Icon
className = { 'add-background' }
size = { 20 }
src = { IconPlusCircle } />
{t('virtualBackground.addBackground')}
</label>}
<input
accept = 'image/*'
className = 'file-upload-btn'
id = 'file-upload'
onChange = { uploadImage }
ref = { uploadImageButton }
type = 'file' />
</>
);
}
export default translate(UploadImageButton);

View File

@@ -3,11 +3,12 @@
import Spinner from '@atlaskit/spinner';
import Bourne from '@hapi/bourne';
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import uuid from 'uuid';
import { Dialog, hideDialog, openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons';
import { Icon, IconCloseSmall, IconPlusCircle, IconShareDesktop } from '../../base/icons';
import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
import { VIDEO_TYPE } from '../../base/media';
@@ -17,19 +18,61 @@ import { Tooltip } from '../../base/tooltip';
import { getLocalVideoTrack } from '../../base/tracks';
import { showErrorNotification } from '../../notifications';
import { toggleBackgroundEffect } from '../actions';
import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
import { toDataURL } from '../functions';
import { VIRTUAL_BACKGROUND_TYPE } from '../constants';
import { resizeImage, toDataURL } from '../functions';
import logger from '../logger';
import UploadImageButton from './UploadImageButton';
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
type Props = {
/**
* The list of Images to choose from.
*/
_images: Array<Image>,
type Image = {
tooltip?: string,
id: string,
src: string
}
// The limit of virtual background uploads is 24. When the number
// of uploads is 25 we trigger the deleteStoredImage function to delete
// the first/oldest uploaded background.
const backgroundsLimit = 25;
const images: Array<Image> = [
{
tooltip: 'image1',
id: '1',
src: 'images/virtual-background/background-1.jpg'
},
{
tooltip: 'image2',
id: '2',
src: 'images/virtual-background/background-2.jpg'
},
{
tooltip: 'image3',
id: '3',
src: 'images/virtual-background/background-3.jpg'
},
{
tooltip: 'image4',
id: '4',
src: 'images/virtual-background/background-4.jpg'
},
{
tooltip: 'image5',
id: '5',
src: 'images/virtual-background/background-5.jpg'
},
{
tooltip: 'image6',
id: '6',
src: 'images/virtual-background/background-6.jpg'
},
{
tooltip: 'image7',
id: '7',
src: 'images/virtual-background/background-7.jpg'
}
];
type Props = {
/**
* The current local flip x status.
@@ -46,11 +89,6 @@ type Props = {
*/
_selectedThumbnail: string,
/**
* If the upload button should be displayed or not.
*/
_showUploadButton: boolean,
/**
* Returns the selected virtual background object.
*/
@@ -90,15 +128,11 @@ const onError = event => {
*/
function _mapStateToProps(state): Object {
const { localFlipX } = state['features/base/settings'];
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
return {
_localFlipX: Boolean(localFlipX),
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
_virtualBackground: state['features/virtual-background'],
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
_showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
};
}
@@ -111,11 +145,9 @@ const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackg
* @returns {ReactElement}
*/
function VirtualBackground({
_images,
_jitsiTrack,
_localFlipX,
_jitsiTrack,
_selectedThumbnail,
_showUploadButton,
_virtualBackground,
dispatch,
initialOptions,
@@ -126,7 +158,7 @@ function VirtualBackground({
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
const [ loading, setLoading ] = useState(false);
const uploadImageButton: Object = useRef(null);
const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
? _virtualBackground.virtualSource
: null);
@@ -154,7 +186,7 @@ function VirtualBackground({
// Preventing localStorage QUOTA_EXCEEDED_ERR
err && setStoredImages(storedImages.slice(1));
}
if (storedImages.length === BACKGROUNDS_LIMIT) {
if (storedImages.length === backgroundsLimit) {
setStoredImages(storedImages.slice(1));
}
}, [ storedImages ]);
@@ -289,27 +321,61 @@ function VirtualBackground({
const setImageBackground = useCallback(async e => {
const imageId = e.currentTarget.getAttribute('data-imageid');
const image = _images.find(img => img.id === imageId);
const image = images.find(img => img.id === imageId);
if (image) {
try {
const url = await toDataURL(image.src);
const url = await toDataURL(image.src);
setOptions({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
});
logger.info('Image set for virtual background preview!');
} catch (err) {
logger.error('Could not fetch virtual background image:', err);
}
setOptions({
backgroundType: 'image',
enabled: true,
url,
selectedThumbnail: image.id
});
logger.info('Image setted for virtual background preview!');
setLoading(false);
}
}, []);
const uploadImage = useCallback(async e => {
const reader = new FileReader();
const imageFile = e.target.files;
reader.readAsDataURL(imageFile[0]);
reader.onload = async () => {
const url = await resizeImage(reader.result);
const uuId = uuid.v4();
setStoredImages([
...storedImages,
{
id: uuId,
src: url
}
]);
setOptions({
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
enabled: true,
url,
selectedThumbnail: uuId
});
};
logger.info('New virtual background image uploaded!');
reader.onerror = () => {
setLoading(false);
logger.error('Failed to upload virtual image!');
};
}, [ dispatch, storedImages ]);
const uploadImageKeyPress = useCallback(e => {
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
uploadImageButton.current.click();
}
}, [ uploadImageButton.current ]);
const setImageBackgroundKeyPress = useCallback(e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
@@ -382,13 +448,25 @@ function VirtualBackground({
</div>
) : (
<div>
{_showUploadButton
&& <UploadImageButton
setLoading = { setLoading }
setOptions = { setOptions }
setStoredImages = { setStoredImages }
showLabel = { previewIsLoaded }
storedImages = { storedImages } />}
{previewIsLoaded && <label
aria-label = { t('virtualBackground.uploadImage') }
className = 'file-upload-label'
htmlFor = 'file-upload'
onKeyPress = { uploadImageKeyPress }
tabIndex = { 0 } >
<Icon
className = { 'add-background' }
size = { 20 }
src = { IconPlusCircle } />
{t('virtualBackground.addBackground')}
</label> }
<input
accept = 'image/*'
className = 'file-upload-btn'
id = 'file-upload'
onChange = { uploadImage }
ref = { uploadImageButton }
type = 'file' />
<div
className = 'virtual-background-dialog'
role = 'radiogroup'
@@ -457,7 +535,7 @@ function VirtualBackground({
src = { IconShareDesktop } />
</div>
</Tooltip>
{_images.map(image => (
{images.map(image => (
<Tooltip
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
key = { image.id }

View File

@@ -1,5 +1,3 @@
// @flow
/**
* An enumeration of the different virtual background types.
*
@@ -11,54 +9,3 @@ export const VIRTUAL_BACKGROUND_TYPE = {
BLUR: 'blur',
NONE: 'none'
};
export type Image = {
tooltip?: string,
id: string,
src: string
}
// The limit of virtual background uploads is 24. When the number
// of uploads is 25 we trigger the deleteStoredImage function to delete
// the first/oldest uploaded background.
export const BACKGROUNDS_LIMIT = 25;
export const IMAGES: Array<Image> = [
{
tooltip: 'image1',
id: '1',
src: 'images/virtual-background/background-1.jpg'
},
{
tooltip: 'image2',
id: '2',
src: 'images/virtual-background/background-2.jpg'
},
{
tooltip: 'image3',
id: '3',
src: 'images/virtual-background/background-3.jpg'
},
{
tooltip: 'image4',
id: '4',
src: 'images/virtual-background/background-4.jpg'
},
{
tooltip: 'image5',
id: '5',
src: 'images/virtual-background/background-5.jpg'
},
{
tooltip: 'image6',
id: '6',
src: 'images/virtual-background/background-6.jpg'
},
{
tooltip: 'image7',
id: '7',
src: 'images/virtual-background/background-7.jpg'
}
];

View File

@@ -22,7 +22,7 @@ local function get_poll_message(stanza)
return nil;
end
local data = json.decode(json_data);
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
if data.type ~= "new-poll" and data.type ~= "answer-poll" then
return nil;
end
return data;
@@ -42,7 +42,7 @@ end
module:hook("muc-room-created", function(event)
local room = event.room;
if is_healthcheck_room(room.jid) then return end
module:log("debug", "setting up polls in room %s", room.jid);
module:log("debug", "setting up polls in room "..tostring(room));
room.polls = {
by_id = {};
order = {};

View File

@@ -128,9 +128,9 @@ function Util:get_public_key(keyId)
local content = self.cache:get(keyId);
if content == nil then
-- If the key is not found in the cache.
module:log("debug", "Cache miss for key: %s", keyId);
module:log("debug", "Cache miss for key: "..keyId);
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
module:log("debug", "Fetching public key from: %s", keyurl);
module:log("debug", "Fetching public key from: "..keyurl);
content = http_get_with_retry(keyurl, nr_retries);
if content ~= nil then
self.cache:set(keyId, content);
@@ -138,7 +138,7 @@ function Util:get_public_key(keyId)
return content;
else
-- If the key is in the cache, use it.
module:log("debug", "Cache hit for key: %s", keyId);
module:log("debug", "Cache hit for key: "..keyId);
return content;
end
end