Compare commits
65 Commits
8759
...
jitsi-meet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fce39be9d2 | ||
|
|
6c5a9ea199 | ||
|
|
196192c97f | ||
|
|
71f358c62a | ||
|
|
7aa7e76ccd | ||
|
|
cd77b6bbe4 | ||
|
|
e94df6799e | ||
|
|
2e92818b53 | ||
|
|
8a3129f7bf | ||
|
|
eb03642ea6 | ||
|
|
c436e48956 | ||
|
|
58db02bab8 | ||
|
|
abc1f3d33b | ||
|
|
b2166d9874 | ||
|
|
901a13a99a | ||
|
|
1e15d9421b | ||
|
|
9252bbb036 | ||
|
|
f1bae8bc10 | ||
|
|
5a54511d2c | ||
|
|
61764273b2 | ||
|
|
e39f38f75b | ||
|
|
2d25f48c72 | ||
|
|
2cb727fc58 | ||
|
|
c069c0d7c3 | ||
|
|
5de69d501d | ||
|
|
599c88a71d | ||
|
|
5476321df6 | ||
|
|
076b6a2a7e | ||
|
|
8b9df0cd37 | ||
|
|
44f5de3db4 | ||
|
|
fb69225d42 | ||
|
|
32df284277 | ||
|
|
253679cfb9 | ||
|
|
057c19f4dd | ||
|
|
6159a23c55 | ||
|
|
1685c39c5d | ||
|
|
2cecc61b97 | ||
|
|
df2262ae53 | ||
|
|
d61deab163 | ||
|
|
e7eab72c0c | ||
|
|
c1e803c6e3 | ||
|
|
dc1f20e059 | ||
|
|
61ee9af304 | ||
|
|
d75de3642e | ||
|
|
1ae1729545 | ||
|
|
8cea505417 | ||
|
|
b0a96b32d2 | ||
|
|
dac9b5e244 | ||
|
|
d15cfd845a | ||
|
|
91e4ac1665 | ||
|
|
fda42e5230 | ||
|
|
142d4441c1 | ||
|
|
5814c4dda7 | ||
|
|
8c1dc03363 | ||
|
|
ff6fc198f1 | ||
|
|
f1a0012fc1 | ||
|
|
85522aea25 | ||
|
|
000c370c64 | ||
|
|
a762d585b8 | ||
|
|
ded8f22363 | ||
|
|
c3e1c9d568 | ||
|
|
8901132af9 | ||
|
|
71f92f6e17 | ||
|
|
76166df81a | ||
|
|
eb2ba39289 |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -152,12 +152,13 @@ jobs:
|
||||
xcodebuild clean \
|
||||
-workspace ios/jitsi-meet.xcworkspace \
|
||||
-scheme JitsiMeetSDK
|
||||
xcodebuild -downloadPlatform iOS -buildVersion 18.2
|
||||
xcodebuild archive \
|
||||
-workspace ios/jitsi-meet.xcworkspace \
|
||||
-scheme JitsiMeetSDK \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-destination='generic/platform=iOS' \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath ios/sdk/out/ios-device \
|
||||
SKIP_INSTALL=NO \
|
||||
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
|
||||
|
||||
@@ -46,7 +46,6 @@ dependencies {
|
||||
|
||||
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'androidx.startup:startup-runtime:1.1.0'
|
||||
implementation 'com.google.j2objc:j2objc-annotations:3.0.0'
|
||||
@@ -87,6 +86,7 @@ dependencies {
|
||||
implementation project(':react-native-svg')
|
||||
implementation project(':react-native-video')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':react-native-worklets-core')
|
||||
|
||||
// Use `api` here so consumers can use WebRTCModuleOptions.
|
||||
api project(':react-native-webrtc')
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
import com.squareup.duktape.Duktape;
|
||||
|
||||
@ReactModule(name = JavaScriptSandboxModule.NAME)
|
||||
class JavaScriptSandboxModule extends ReactContextBaseJavaModule {
|
||||
public static final String NAME = "JavaScriptSandbox";
|
||||
|
||||
public JavaScriptSandboxModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the given code in a Duktape VM.
|
||||
* @param code - The code that needs to evaluated.
|
||||
* @param promise - Resolved with the output in case of success or rejected with an exception
|
||||
* in case of failure.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void evaluate(String code, Promise promise) {
|
||||
Duktape vm = Duktape.create();
|
||||
try {
|
||||
Object res = vm.evaluate(code);
|
||||
promise.resolve(res.toString());
|
||||
} catch (Throwable tr) {
|
||||
promise.reject(tr);
|
||||
} finally {
|
||||
vm.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,15 @@ import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.facebook.react.modules.core.PermissionListener;
|
||||
@@ -87,6 +93,28 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
launch(context, options);
|
||||
}
|
||||
|
||||
public static void addTopBottomInsets(@NonNull Window w, @NonNull View v) {
|
||||
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) return;
|
||||
|
||||
View decorView = w.getDecorView();
|
||||
|
||||
decorView.post(() -> {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
|
||||
if (insets != null) {
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
|
||||
params.topMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top;
|
||||
params.bottomMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||
v.setLayoutParams(params);
|
||||
|
||||
decorView.setOnApplyWindowInsetsListener((view, windowInsets) -> {
|
||||
view.setBackgroundColor(JitsiMeetView.BACKGROUND_COLOR);
|
||||
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Overrides
|
||||
//
|
||||
|
||||
@@ -107,6 +135,7 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
JitsiMeetActivityDelegate.onHostResume(this);
|
||||
|
||||
setContentView(R.layout.activity_jitsi_meet);
|
||||
addTopBottomInsets(getWindow(),findViewById(android.R.id.content));
|
||||
this.jitsiView = findViewById(R.id.jitsiView);
|
||||
|
||||
registerForBroadcastMessages();
|
||||
|
||||
@@ -36,7 +36,7 @@ public class JitsiMeetView extends FrameLayout {
|
||||
/**
|
||||
* Background color. Should match the background color set in JS.
|
||||
*/
|
||||
private static final int BACKGROUND_COLOR = 0xFF040404;
|
||||
public static final int BACKGROUND_COLOR = 0xFF040404;
|
||||
|
||||
/**
|
||||
* React Native root view.
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -65,7 +64,6 @@ class ReactInstanceManagerHolder {
|
||||
new AudioModeModule(reactContext),
|
||||
new DropboxModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new JavaScriptSandboxModule(reactContext),
|
||||
new LocaleDetector(reactContext),
|
||||
new LogBridgeModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
@@ -110,6 +108,7 @@ class ReactInstanceManagerHolder {
|
||||
new com.horcrux.svg.SvgPackage(),
|
||||
new org.wonday.orientation.OrientationPackage(),
|
||||
new com.splashview.SplashViewPackage(),
|
||||
new com.worklets.WorkletsCorePackage(),
|
||||
new ReactPackageAdapter() {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
|
||||
@@ -52,3 +52,5 @@ include ':react-native-webrtc'
|
||||
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
include ':react-native-worklets-core'
|
||||
project(':react-native-worklets-core').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-worklets-core/android')
|
||||
|
||||
@@ -8,9 +8,13 @@ module.exports = {
|
||||
|
||||
// This happens because react native has conflict with @babel/plugin-transform-private-methods plugin
|
||||
// https://github.com/ethers-io/ethers.js/discussions/4309#discussioncomment-6694524
|
||||
plugins: [ 'optional-require',
|
||||
[ '@babel/plugin-transform-private-methods', {
|
||||
'loose': true
|
||||
} ]
|
||||
plugins: [
|
||||
'optional-require',
|
||||
[
|
||||
'@babel/plugin-transform-private-methods', {
|
||||
'loose': true
|
||||
}
|
||||
],
|
||||
'react-native-worklets-core/plugin'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -134,6 +134,7 @@ import {
|
||||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute
|
||||
} from './react/features/base/tracks/functions';
|
||||
import { getLocalJitsiAudioTrackSettings } from './react/features/base/tracks/functions.web';
|
||||
import { downloadJSON } from './react/features/base/util/downloadJSON';
|
||||
import { getJitsiMeetGlobalNSConnectionTimes } from './react/features/base/util/helpers';
|
||||
import { openLeaveReasonDialog } from './react/features/conference/actions.web';
|
||||
@@ -158,6 +159,7 @@ import { disableReceiver, stopReceiver } from './react/features/remote-control/a
|
||||
import { setScreenAudioShareState } from './react/features/screen-share/actions.web';
|
||||
import { isScreenAudioShared } from './react/features/screen-share/functions';
|
||||
import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture/actions';
|
||||
import { setAudioSettings } from './react/features/settings/actions.web';
|
||||
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
|
||||
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
|
||||
@@ -566,7 +568,15 @@ export default {
|
||||
if (browser.isWebKitBased()) {
|
||||
this.muteAudio(true, true);
|
||||
} else {
|
||||
localTracks = localTracks.filter(track => track.getType() !== MEDIA_TYPE.AUDIO);
|
||||
localTracks = localTracks.filter(track => {
|
||||
if (track.getType() === MEDIA_TYPE.AUDIO) {
|
||||
track.stopStream();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1763,7 +1773,11 @@ export default {
|
||||
return this.useAudioStream(stream);
|
||||
})
|
||||
.then(() => {
|
||||
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
|
||||
const state = APP.store.getState();
|
||||
const localAudio = getLocalJitsiAudioTrack(state);
|
||||
const settings = getLocalJitsiAudioTrackSettings(state);
|
||||
|
||||
APP.store.dispatch(setAudioSettings(settings));
|
||||
|
||||
if (localAudio && isDefaultMicSelected) {
|
||||
// workaround for the default device to be shown as selected in the
|
||||
|
||||
@@ -363,6 +363,7 @@ var config = {
|
||||
// Desktop sharing
|
||||
|
||||
// Optional desktop sharing frame rate options. Default value: min:5, max:5.
|
||||
// Setting higher min/max values will affect the resolution, it makes it worse.
|
||||
// desktopSharingFrameRate: {
|
||||
// min: 5,
|
||||
// max: 5,
|
||||
@@ -791,7 +792,6 @@ var config = {
|
||||
// Configs for prejoin page.
|
||||
// prejoinConfig: {
|
||||
// // When 'true', it shows an intermediate page before joining, where the user can configure their devices.
|
||||
// // This replaces `prejoinPageEnabled`. Defaults to true.
|
||||
// enabled: true,
|
||||
// // Hides the participant name editing field in the prejoin screen.
|
||||
// // If requireDisplayName is also set as true, a name should still be provided through
|
||||
|
||||
@@ -1497,6 +1497,27 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- react-native-worklets-core (1.6.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- React-nativeconfig (0.77.2)
|
||||
- React-NativeModulesApple (0.77.2):
|
||||
- glog
|
||||
@@ -1932,6 +1953,7 @@ DEPENDENCIES:
|
||||
- react-native-video (from `../node_modules/react-native-video`)
|
||||
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
|
||||
- react-native-webview (from `../node_modules/react-native-webview`)
|
||||
- react-native-worklets-core (from `../node_modules/react-native-worklets-core`)
|
||||
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
|
||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||
@@ -2101,6 +2123,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-webrtc"
|
||||
react-native-webview:
|
||||
:path: "../node_modules/react-native-webview"
|
||||
react-native-worklets-core:
|
||||
:path: "../node_modules/react-native-worklets-core"
|
||||
React-nativeconfig:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
React-NativeModulesApple:
|
||||
@@ -2257,6 +2281,7 @@ SPEC CHECKSUMS:
|
||||
react-native-video: eb861d67a71dfef1bbf6086a811af5f338b13781
|
||||
react-native-webrtc: 2261a482150195092246fe70b3aff976f2e11ec5
|
||||
react-native-webview: 079eca50edf657503318b66687dadfb903731aa8
|
||||
react-native-worklets-core: b59cf88762c8fb6132d8796babd4cec15217d6f0
|
||||
React-nativeconfig: ecf4dc92c40b97e2b3f0c619938f78bfd6507b08
|
||||
React-NativeModulesApple: f457bbfb30fb3bc41979b1a87b99d292d7340d39
|
||||
React-perflogger: 1111b5feb064c4cc83df88fb403efda54b387951
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */; };
|
||||
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C8AFD2802462C613000293D2 /* InfoPlistUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */; };
|
||||
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */ = {isa = PBXBuildFile; fileRef = DE438CD82350934700DD541D /* JavaScriptSandbox.m */; };
|
||||
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DE65AAC92317FFCD00290BEC /* LogUtils.h */; };
|
||||
DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */; };
|
||||
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
@@ -98,7 +97,6 @@
|
||||
DE9A015C289A9A9A00E41CBB /* JitsiMeetLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = DE81A2D32316AC4D00AE1940 /* JitsiMeetLogger.m */; };
|
||||
DE9A015E289A9A9A00E41CBB /* JitsiMeetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */; };
|
||||
DE9A015F289A9A9A00E41CBB /* JitsiMeet.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535321FB1BF800011A3A /* JitsiMeet.m */; };
|
||||
DE9A0160289A9A9A00E41CBB /* JavaScriptSandbox.m in Sources */ = {isa = PBXBuildFile; fileRef = DE438CD82350934700DD541D /* JavaScriptSandbox.m */; };
|
||||
DE9A0162289A9A9A00E41CBB /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD781F5EC6D7001C08DB /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
DE9A0163289A9A9A00E41CBB /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD761F5EC6CE001C08DB /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
DE9A0166289A9A9A00E41CBB /* CallKitIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */; };
|
||||
@@ -161,7 +159,6 @@
|
||||
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExternalAPI.h; sourceTree = "<group>"; };
|
||||
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoPlistUtil.h; sourceTree = "<group>"; };
|
||||
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InfoPlistUtil.m; sourceTree = "<group>"; };
|
||||
DE438CD82350934700DD541D /* JavaScriptSandbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JavaScriptSandbox.m; sourceTree = "<group>"; };
|
||||
DE65AAC92317FFCD00290BEC /* LogUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LogUtils.h; sourceTree = "<group>"; };
|
||||
DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetBaseLogHandler+Private.h"; sourceTree = "<group>"; };
|
||||
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetUserInfo.h; sourceTree = "<group>"; };
|
||||
@@ -250,7 +247,6 @@
|
||||
C69EFA02209A0EFD0027712B /* callkit */,
|
||||
A4A934E7212F3AB8001E9388 /* dropbox */,
|
||||
0BD906E91EC0C00300C8C18E /* Info.plist */,
|
||||
DE438CD82350934700DD541D /* JavaScriptSandbox.m */,
|
||||
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
|
||||
DEFE535821FB311F00011A3A /* JitsiMeet+Private.h */,
|
||||
DEA9F283258A5D9900D4CD74 /* JitsiMeetSDK.h */,
|
||||
@@ -681,7 +677,6 @@
|
||||
DE81A2D52316AC4D00AE1940 /* JitsiMeetLogger.m in Sources */,
|
||||
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
|
||||
DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */,
|
||||
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -714,7 +709,6 @@
|
||||
4E0EF63328CA2FB3005D1B03 /* JMCallKitEmitter.m in Sources */,
|
||||
DE9A015E289A9A9A00E41CBB /* JitsiMeetView.m in Sources */,
|
||||
DE9A015F289A9A9A00E41CBB /* JitsiMeet.m in Sources */,
|
||||
DE9A0160289A9A9A00E41CBB /* JavaScriptSandbox.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@import JavaScriptCore;
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
|
||||
@interface JavaScriptSandbox : NSObject<RCTBridgeModule>
|
||||
@end
|
||||
|
||||
@implementation JavaScriptSandbox
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Exported methods
|
||||
|
||||
RCT_EXPORT_METHOD(evaluate:(NSString *)code
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
__block BOOL hasError = NO;
|
||||
JSContext *ctx = [[JSContext alloc] init];
|
||||
ctx.exceptionHandler = ^(JSContext *context, JSValue *exception) {
|
||||
hasError = YES;
|
||||
reject(@"evaluate", [exception toString], nil);
|
||||
};
|
||||
JSValue *ret = [ctx evaluateScript:code];
|
||||
if (!hasError) {
|
||||
NSString *result = [ret toString];
|
||||
if (result == nil) {
|
||||
reject(@"evaluate", @"Error in string coercion", nil);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -112,7 +112,9 @@
|
||||
"disabled": "L'invio di messaggi in chat è disabilitato.",
|
||||
"enter": "Entra nella conversazione",
|
||||
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
|
||||
"everyone": "Tutti",
|
||||
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
|
||||
"guestsChatIndicator": "(ospite)",
|
||||
"lobbyChatMessageTo": "Messaggio a {{recipient}} in sala d'attesa",
|
||||
"message": "Messaggio",
|
||||
"messageAccessibleTitle": "{{user}} dice:",
|
||||
@@ -154,7 +156,7 @@
|
||||
"installExtensionText": "Installa un'estensione per integrare Google Calendar e Office 365"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "Il contenuto dei sottotitoli sarà disponibile una volta che l'organizzatore lo attiva",
|
||||
"emptyState": "Il contenuto dei sottotitoli sarà disponibile una volta che l'organizzatore lo attiverà",
|
||||
"startClosedCaptionsButton": "Attiva sottotitoli"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
@@ -300,6 +302,12 @@
|
||||
"alreadySharedVideoTitle": "È permessa una sola condivisione video alla volta",
|
||||
"applicationWindow": "Finestra dell'applicazione",
|
||||
"authenticationRequired": "Richiesta autenticazione",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "Scatta ed invia una foto usando la fotocamera del telefono",
|
||||
"ok": "Apri la fotocamera",
|
||||
"reject": "Non adesso",
|
||||
"title": "Scatta una foto"
|
||||
},
|
||||
"cameraConstraintFailedError": "La tua videocamera non soddisfa alcuni dei requisiti.",
|
||||
"cameraNotFoundError": "Videocamera non trovata.",
|
||||
"cameraNotSendingData": "Impossibile accedere alla videocamera. Controlla che non sia in uso in un'altra applicazione, seleziona un altro dispositivo dalle impostazioni o prova a ricaricare l'applicazione.",
|
||||
@@ -323,7 +331,7 @@
|
||||
"contactSupport": "Contatta il supporto",
|
||||
"copied": "Copiato",
|
||||
"copy": "Copia",
|
||||
"demoteParticipantDialog": "Sei sicuro di voler far diventare questo partecipante uno spettatore?",
|
||||
"demoteParticipantDialog": "Vuoi far diventare questo partecipante uno spettatore?",
|
||||
"demoteParticipantTitle": "Fai diventare spettatore",
|
||||
"dismiss": "Scarta",
|
||||
"displayNameRequired": "Ciao, qual è il tuo nome?",
|
||||
@@ -338,8 +346,8 @@
|
||||
"error": "Errore",
|
||||
"errorRoomCreationRestriction": "Hai provato ad accedere alla riunione troppo presto, torna tra un po'.",
|
||||
"gracefulShutdown": "Il nostro servizio è al momento inattivo per manutenzione. Si prega di riprovare più tardi.",
|
||||
"grantModeratorDialog": "Sei sicuro di voler rendere organizzatore questo partecipante?",
|
||||
"grantModeratorTitle": "Fai diventare organizzatore",
|
||||
"grantModeratorDialog": "Vuoi rendere relatore questo partecipante?",
|
||||
"grantModeratorTitle": "Fai diventare relatore",
|
||||
"hide": "Nascondi",
|
||||
"hideShareAudioHelper": "Non mostrare più questa finestra",
|
||||
"incorrectPassword": "Nome utente o password errati",
|
||||
@@ -348,7 +356,7 @@
|
||||
"internalErrorTitle": "Errore interno",
|
||||
"kickMessage": "Puoi contattare {{participantDisplayName}} per maggiori dettagli.",
|
||||
"kickParticipantButton": "Espelli",
|
||||
"kickParticipantDialog": "Sei sicuro di voler espellere questo partecipante?",
|
||||
"kickParticipantDialog": "Vuoi espellere questo partecipante?",
|
||||
"kickParticipantTitle": "Espellere questo partecipante?",
|
||||
"kickSystemTitle": "Oh! Sei stato espulso dalla riunione",
|
||||
"kickTitle": "Oh! {{participantDisplayName}} ti ha espulso dalla riunione.",
|
||||
@@ -362,8 +370,8 @@
|
||||
"lockRoom": "Aggiungi una $t(lockRoomPassword) alla riunione",
|
||||
"lockTitle": "Blocco fallito",
|
||||
"login": "Accesso",
|
||||
"loginQuestion": "Sei sicuro di voler fare l'accesso e abbandonare la riunione?",
|
||||
"logoutQuestion": "Sei sicuro di volerti disconnettere e abbandonare la riunione?",
|
||||
"loginQuestion": "Vuoi fare l'accesso e abbandonare la riunione?",
|
||||
"logoutQuestion": "Vuoi disconnetterti e abbandonare la riunione?",
|
||||
"logoutTitle": "Disconnessione",
|
||||
"maxUsersLimitReached": "È stato raggiunto il numero massimo di partecipanti. La riunione è al completo. Contatta l'organizzatore o riprova più tardi!",
|
||||
"maxUsersLimitReachedTitle": "Raggiunto limite massimo partecipanti",
|
||||
@@ -375,27 +383,39 @@
|
||||
"micTimeoutError": "Impossibile avviare la sorgente audio. Tempo di attesa scaduto.",
|
||||
"micUnknownError": "Impossibile usare il microfono per un motivo sconosciuto.",
|
||||
"moderationAudioLabel": "Consenti ai partecipanti di attivare il microfono",
|
||||
"moderationDesktopLabel": "Consenti ai partecipanti di condividere lo schermo",
|
||||
"moderationVideoLabel": "Consenti ai partecipanti di attivare la videocamera",
|
||||
"muteEveryoneDialog": "I partecipanti possono attivare il microfono in qualsiasi momento.",
|
||||
"muteEveryoneDialogModerationOn": "I partecipanti possono chiedere di parlare in qualsiasi momento.",
|
||||
"muteEveryoneElseDialog": "Una volta spenti i microfoni non potrai riattivarli, ma ogni partecipante potrà farlo da sé in qualsiasi momento.",
|
||||
"muteEveryoneElseTitle": "Spegnere il microfono a tutti, tranne che a {{whom}}?",
|
||||
"muteEveryoneElsesDesktopDialog": "Una volta interrotta la condivisione dello schermo non potrai riattivarla, ma ogni partecipante potrà farlo da sé in qualsiasi momento.",
|
||||
"muteEveryoneElsesDesktopTitle": "Interrompere la condivisione dello schermo a tutti, tranne che a {{whom}}?",
|
||||
"muteEveryoneElsesVideoDialog": "Una volta spente le videocamere non potrai riaccenderle, ma ogni partecipante potrà farlo da sé in qualsiasi momento.",
|
||||
"muteEveryoneElsesVideoTitle": "Spegnere la videocamera a tutti, tranne che a {{whom}}?",
|
||||
"muteEveryoneSelf": "tu",
|
||||
"muteEveryoneStartMuted": "Tutti iniziano a microfono spento da adesso in avanti",
|
||||
"muteEveryoneTitle": "Spegnere il microfono a tutti?",
|
||||
"muteEveryonesDesktopDialog": "I partecipanti potranno condividere lo schermo in qualsiasi momento.",
|
||||
"muteEveryonesDesktopDialogModerationOn": "I partecipanti possono inviare una richiesta per condividere lo schermo in ogni momento.",
|
||||
"muteEveryonesDesktopTitle": "Interrompere la condivisione dello schermo a tutti?",
|
||||
"muteEveryonesVideoDialog": "Ogni partecipante potrà riavviare il video da sé in qualsiasi momento.",
|
||||
"muteEveryonesVideoDialogModerationOn": "I partecipanti possono chiedere di attivare il video in qualsiasi momento.",
|
||||
"muteEveryonesVideoDialogOk": "Spegni",
|
||||
"muteEveryonesVideoTitle": "Spegnere la videocamera a tutti?",
|
||||
"muteParticipantBody": "Non potrai riattivare il loro microfono, ma loro potranno farlo in qualsiasi momento.",
|
||||
"muteParticipantButton": "Silenzia",
|
||||
"muteParticipantsDesktopBody": "Non potrai riavviare la loro condivisione dello schermo, ma loro potranno farlo in qualsiasi momento.",
|
||||
"muteParticipantsDesktopBodyModerationOn": "Non potrai riavviare la loro condivisione dello schermo e nemmeno loro potranno.",
|
||||
"muteParticipantsDesktopButton": "Interrompi la condivisione dello schermo",
|
||||
"muteParticipantsDesktopDialog": "Vuoi interrompere la condivisione dello schermo di questo partecipante? Non potrai riattivarla, ma lui potrà farlo in qualsiasi momento.",
|
||||
"muteParticipantsDesktopDialogModerationOn": "Vuoi interrompere la condivisione dello schermo di questo partecipante? Non potrai riattivarla e nemmeno loro potranno.",
|
||||
"muteParticipantsDesktopTitle": "Disattivare la condivisione dello schermo di questo partecipante?",
|
||||
"muteParticipantsVideoBody": "Non potrai riattivare le videocamere, ma loro potranno farlo in qualsiasi momento.",
|
||||
"muteParticipantsVideoBodyModerationOn": "Non potrai riattivare le videocamere e nemmeno loro potranno.",
|
||||
"muteParticipantsVideoButton": "Spegni videocamere",
|
||||
"muteParticipantsVideoDialog": "Sei sicuro di voler spegnere la videocamera di questo partecipante? Non potrai riattivarla, ma lui potrà farlo in qualsiasi momento.",
|
||||
"muteParticipantsVideoDialogModerationOn": "Sei sicuro di voler spegnere la videocamera di questo partecipante? Non potrai riattivarla e nemmeno lui potrà farlo.",
|
||||
"muteParticipantsVideoDialog": "Vuoi spegnere la videocamera di questo partecipante? Non potrai riattivarla, ma lui potrà farlo in qualsiasi momento.",
|
||||
"muteParticipantsVideoDialogModerationOn": "Vuoi spegnere la videocamera di questo partecipante? Non potrai riattivarla e nemmeno lui potrà farlo.",
|
||||
"muteParticipantsVideoTitle": "Spegnere la videocamera di questo partecipante?",
|
||||
"noDropboxToken": "Token Dropbox non valido",
|
||||
"password": "Password",
|
||||
@@ -424,7 +444,7 @@
|
||||
"remoteControlTitle": "Connessione desktop remoto",
|
||||
"remoteUserControls": "Controlli dell'utente remoto {{username}}",
|
||||
"removePassword": "Rimuovi la $t(lockRoomPassword)",
|
||||
"removeSharedVideoMsg": "Sei sicuro di voler rimuovere il tuo video condiviso?",
|
||||
"removeSharedVideoMsg": "Vuoi rimuovere il tuo video condiviso?",
|
||||
"removeSharedVideoTitle": "Rimuovi video condiviso",
|
||||
"renameBreakoutRoomLabel": "Nome della stanza",
|
||||
"renameBreakoutRoomTitle": "Rinomina stanza",
|
||||
@@ -477,8 +497,8 @@
|
||||
"startRemoteControlErrorMessage": "Si è verificato un errore nel tentativo di avviare la sessione di controllo remoto!",
|
||||
"stopLiveStreaming": "Ferma la diretta",
|
||||
"stopRecording": "Ferma registrazione",
|
||||
"stopRecordingWarning": "Sei sicuro di voler interrompere la registrazione?",
|
||||
"stopStreamingWarning": "Sei sicuro di voler interrompere la diretta?",
|
||||
"stopRecordingWarning": "Vuoi interrompere la registrazione?",
|
||||
"stopStreamingWarning": "Vuoi interrompere la diretta?",
|
||||
"streamKey": "Chiave della diretta",
|
||||
"thankYou": "Grazie per aver usato {{appName}}!",
|
||||
"token": "token",
|
||||
@@ -547,11 +567,15 @@
|
||||
"downloadFailedDescription": "Si prega di riprovare.",
|
||||
"downloadFailedTitle": "Download non riuscito",
|
||||
"downloadFile": "Download",
|
||||
"downloadStarted": "Download del file iniziato",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
|
||||
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Il file selezionato è troppo grande",
|
||||
"fileUploadProgress": "Caricamento del file in corso",
|
||||
"fileUploadedSuccessfully": "Il file è stato caricato con successo",
|
||||
"removeFile": "Rimuovi",
|
||||
"removeFileSuccess": "File rimosso con successo",
|
||||
"uploadFailedDescription": "Si prega di riprovare.",
|
||||
"uploadFailedTitle": "Caricamento non riuscito",
|
||||
"uploadFile": "Condividi file"
|
||||
@@ -683,7 +707,7 @@
|
||||
"signInCTA": "Accedi o inserisci la tua chiave della diretta su YouTube.",
|
||||
"signOut": "Disconnetti",
|
||||
"signedInAs": "Hai effettuato l'accesso come:",
|
||||
"start": "Inizia una diretta",
|
||||
"start": "Avvia una diretta",
|
||||
"streamIdHelp": "Cos'è questo?",
|
||||
"title": "Diretta",
|
||||
"unavailableTitle": "La diretta non è disponibile",
|
||||
@@ -744,9 +768,9 @@
|
||||
"engaged": "Registrazione avviata.",
|
||||
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione all'organizzatore.",
|
||||
"finishedModerator": "La registrazione della sessione {{token}} è terminata. La registrazione della traccia è stata salvata. Chiedi ai partecipanti di inviare le loro registrazioni.",
|
||||
"notModerator": "Non sei un organizzatore. Non puoi avviare o interrompere la registrazione."
|
||||
"notModerator": "Non sei un relatore. Non puoi avviare o interrompere la registrazione."
|
||||
},
|
||||
"moderator": "Organizzatore",
|
||||
"moderator": "Relatore",
|
||||
"no": "No",
|
||||
"participant": "Partecipante",
|
||||
"participantStats": "Statistiche partecipante",
|
||||
@@ -767,8 +791,9 @@
|
||||
"me": "io",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "Falla di sicurezza!",
|
||||
"allowAll": "Consenti tutto",
|
||||
"allowAudio": "Consenti l'audio",
|
||||
"allowBoth": "Entrambi",
|
||||
"allowDesktop": "Consenti la condivisione dello schermo",
|
||||
"allowVideo": "Consenti il video",
|
||||
"allowedUnmute": "Puoi accendere il microfono, avviare la videocamera o condividere il tuo schermo.",
|
||||
"audioUnmuteBlockedDescription": "L'accensione dei microfoni è stata temporaneamente bloccata per i limiti del sistema.",
|
||||
@@ -782,6 +807,7 @@
|
||||
"dataChannelClosedDescription": "Il canale bridge è inattivo, quindi la qualità video potrebbe essere limitata all'impostazione più bassa.",
|
||||
"dataChannelClosedDescriptionWithAudio": "Il canale bridge è inattivo, quindi potrebbero esserci interruzioni di audio e video",
|
||||
"dataChannelClosedWithAudio": "La qualità audio e video potrebbe essere scarsa",
|
||||
"desktopMutedRemotelyTitle": "La tua condivisione dello schermo è stata interrotta da {{participantDisplayName}}",
|
||||
"disabledIframe": "L'incorporamento serve solo come dimostrazione, quindi questa chiamata si disconnetterà tra {{timeout}} minuti.",
|
||||
"disabledIframeSecondaryNative": "L'incorporamento {{domain}} serve solo come dimostrazione, quindi questa chiamata si disconnetterà tra {{timeout}} minuti.",
|
||||
"disabledIframeSecondaryWeb": "L'incorporamento {{domain}} serve solo come dimostrazione, quindi questa chiamata si disconnetterà tra {{timeout}} minuti. Si prega di usare <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> per la produzione di riunioni incorporate!",
|
||||
@@ -792,7 +818,7 @@
|
||||
"focusFail": "{{component}} non disponibile - nuovo tentativo tra {{ms}} sec",
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Notifiche",
|
||||
"hostAskedUnmute": "L'organizzatore ti chiede di intervenire.",
|
||||
"hostAskedUnmute": "Il relatore ti chiede di intervenire.",
|
||||
"invalidTenant": "Nome non valido",
|
||||
"invalidTenantHyphenDescription": "Il nome che hai scelto non è valido (inizia o finisce con '-').",
|
||||
"invalidTenantLengthDescription": "Il nome che hai scelto è troppo lungo.",
|
||||
@@ -814,17 +840,17 @@
|
||||
"localRecordingStopped": "{{name}} ha smesso di registrare.",
|
||||
"me": "Io",
|
||||
"moderationInEffectCSDescription": "Alza la mano, se vuoi condividere lo schermo.",
|
||||
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dall'organizzatore",
|
||||
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dal relatore",
|
||||
"moderationInEffectDescription": "Alza la mano, se vuoi prendere la parola.",
|
||||
"moderationInEffectTitle": "Il tuo microfono è stato spento dall'organizzatore",
|
||||
"moderationInEffectTitle": "Il tuo microfono è stato spento dal relatore",
|
||||
"moderationInEffectVideoDescription": "Alza la mano, se vuoi avviare la tua videocamera.",
|
||||
"moderationInEffectVideoTitle": "La tua videocamera è stata spenta dall'organizzatore",
|
||||
"moderationRequestFromModerator": "L'organizzatore vorrebbe che tu accendessi il microfono",
|
||||
"moderationInEffectVideoTitle": "La tua videocamera è stata spenta dal relatore",
|
||||
"moderationRequestFromModerator": "Il relatore vorrebbe che tu accendessi il microfono",
|
||||
"moderationRequestFromParticipant": "Vuole parlare",
|
||||
"moderationStartedTitle": "Moderazione in corso",
|
||||
"moderationStoppedTitle": "Moderazione interrotta",
|
||||
"moderationToggleDescription": "da {{participantDisplayName}}",
|
||||
"moderator": "Ora sei un organizzatore!",
|
||||
"moderator": "Ora sei un relatore!",
|
||||
"muted": "Hai iniziato la conversazione con il microfono disattivato.",
|
||||
"mutedRemotelyDescription": "Puoi sempre attivare il microfono quando vuoi parlare. Spegni il microfono quando hai finito, per evitare rumori di fondo nella riunione.",
|
||||
"mutedRemotelyTitle": "{{participantDisplayName}} ti ha spento il microfono",
|
||||
@@ -839,6 +865,7 @@
|
||||
"oldElectronClientDescription1": "Sembra che tu stia usando una versione obsoleta del client Jitsi Meet, che ha vulnerabilità note. Assicurati di aggiornarlo alla nostra ",
|
||||
"oldElectronClientDescription2": "ultima versione",
|
||||
"oldElectronClientDescription3": " ora!",
|
||||
"openChat": "Apri chat",
|
||||
"participantWantsToJoin": "Vuole unirsi alla riunione",
|
||||
"participantsWantToJoin": "Vogliono unirsi alla riunione",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) è stata rimossa da un altro partecipante",
|
||||
@@ -862,6 +889,7 @@
|
||||
"suggestRecordingDescription": "Vuoi avviare la registrazione?",
|
||||
"suggestRecordingTitle": "Registra questa riunione",
|
||||
"unmute": "Accendi il microfono",
|
||||
"unmuteScreen": "Iniziata la condivisione dello schermo",
|
||||
"unmuteVideo": "Accendi la videocamera",
|
||||
"videoMutedRemotelyDescription": "Puoi riaccenderla in qualsiasi momento.",
|
||||
"videoMutedRemotelyTitle": "{{participantDisplayName}} ti ha spento la videocamera",
|
||||
@@ -881,11 +909,14 @@
|
||||
"admit": "Ammetti",
|
||||
"admitAll": "Ammetti tutti",
|
||||
"allow": "Permetti ai partecipanti di:",
|
||||
"allowDesktop": "Consenti la condivisione dello schermo",
|
||||
"allowVideo": "Permetti videocamere",
|
||||
"askDesktop": "Chiedi di condividere lo schermo",
|
||||
"askUnmute": "Chiedi di accendere il microfono",
|
||||
"audioModeration": "Riattivare il microfono",
|
||||
"blockEveryoneMicCamera": "Blocca microfono e videocamera a tutti",
|
||||
"breakoutRooms": "Stanze",
|
||||
"desktopModeration": "Avvia la condivisione dello schermo",
|
||||
"goLive": "Vai alla diretta",
|
||||
"invite": "Invita partecipanti",
|
||||
"lowerAllHands": "Abbassa tutte le mani",
|
||||
@@ -897,6 +928,8 @@
|
||||
"muteAll": "Silenzia tutti",
|
||||
"muteEveryoneElse": "Silenzia tutti gli altri",
|
||||
"reject": "Respingi",
|
||||
"stopDesktop": "Interrompi la condivisione dello schermo",
|
||||
"stopEveryonesDesktop": "Interrompi la condivisione dello schermo agli altri",
|
||||
"stopEveryonesVideo": "Ferma il video di tutti",
|
||||
"stopVideo": "Ferma il video",
|
||||
"unblockEveryoneMicCamera": "Sblocca microfono e videocamera a tutti",
|
||||
@@ -906,6 +939,7 @@
|
||||
"headings": {
|
||||
"lobby": "Sala d'attesa ({{count}})",
|
||||
"participantsList": "Partecipanti alla riunione ({{count}})",
|
||||
"viewerRequests": "Richieste spettatori ({{count}})",
|
||||
"visitorInQueue": " ({{count}} in attesa)",
|
||||
"visitorRequests": " ({{count}} richiesta/e)",
|
||||
"visitors": "Spettatori {{count}}",
|
||||
@@ -1073,7 +1107,7 @@
|
||||
"fileSharingdescription": "Condividi la registrazione con i partecipanti alla riunione",
|
||||
"highlight": "Evidenzia",
|
||||
"highlightMoment": "Evidenzia momento",
|
||||
"highlightMomentDisabled": "Puoi evidenziare dei momenti quando inizia la registrazione",
|
||||
"highlightMomentDisabled": "Puoi evidenziare i momenti in cui inizia la registrazione",
|
||||
"highlightMomentSuccess": "Momento evidenziato",
|
||||
"highlightMomentSucessDescription": "Il tuo momento evidenziato sarà aggiunto al riepilogo della riunione.",
|
||||
"inProgress": "Registrazione o diretta in corso",
|
||||
@@ -1084,7 +1118,7 @@
|
||||
"localRecordingNoVideo": "Il video non sta venendo registrato",
|
||||
"localRecordingStartWarning": "Assicurati di interrompere la registrazione prima di uscire dalla riunione, altrimenti la registrazione non verrà salvata.",
|
||||
"localRecordingStartWarningTitle": "Interrompi la registrazione per salvarla",
|
||||
"localRecordingVideoStop": "Interrompere il video fermerà anche la registrazione. Sei sicuro di voler continuare?",
|
||||
"localRecordingVideoStop": "Interrompere il video fermerà anche la registrazione. Vuoi continuare?",
|
||||
"localRecordingVideoWarning": "Per registrare il video, deve essere già avviato prima dell'inizio della registrazione",
|
||||
"localRecordingWarning": "Assicurati di aver selezionato la scheda corrente, per registrare gli audio e video corretti.",
|
||||
"loggedIn": "Accesso effettuato come {{userName}}",
|
||||
@@ -1153,8 +1187,8 @@
|
||||
"loggedIn": "Connesso come {{name}}",
|
||||
"maxStageParticipants": "Numero massimo di partecipanti che possono essere messi in evidenza nella schermata principale",
|
||||
"microphones": "Microfoni",
|
||||
"moderator": "Organizzatore",
|
||||
"moderatorOptions": "Opzioni organizzatore",
|
||||
"moderator": "Relatore",
|
||||
"moderatorOptions": "Opzioni relatore",
|
||||
"more": "Generali",
|
||||
"name": "Nome",
|
||||
"noDevice": "Nessuno",
|
||||
@@ -1190,7 +1224,7 @@
|
||||
"conferenceSection": "Riunione",
|
||||
"disableCallIntegration": "Disattiva l'integrazione nativa delle chiamate",
|
||||
"disableCrashReporting": "Disattiva la diagnostica dei crash",
|
||||
"disableCrashReportingWarning": "Sei sicuro di voler disattivare la diagnostica dei crash? Quest'impostazione verrà applicata al prossimo avvio dell'app.",
|
||||
"disableCrashReportingWarning": "Vuoi disattivare la diagnostica dei crash? Quest'impostazione verrà applicata al prossimo avvio dell'app.",
|
||||
"disableP2P": "Disattiva la modalità Peer-to-Peer",
|
||||
"displayName": "Nome visualizzato",
|
||||
"displayNamePlaceholderText": "Es: Mario Rossi",
|
||||
@@ -1207,8 +1241,8 @@
|
||||
"serverURL": "URL del server",
|
||||
"showAdvanced": "Mostra impostazioni avanzate",
|
||||
"startCarModeInLowBandwidthMode": "Avvia modalità auto in modalità larghezza di banda limitata",
|
||||
"startWithAudioMuted": "Inizia con audio disattivato",
|
||||
"startWithVideoMuted": "Inizia con video disattivato",
|
||||
"startWithAudioMuted": "Avvia con audio disattivato",
|
||||
"startWithVideoMuted": "Avvia con video disattivato",
|
||||
"terms": "Termini",
|
||||
"version": "Versione"
|
||||
},
|
||||
@@ -1283,7 +1317,7 @@
|
||||
"feedback": "Lascia un feedback",
|
||||
"fullScreen": "Attiva modalità a schermo intero",
|
||||
"giphy": "Mostra menu GIPHY",
|
||||
"grantModerator": "Concedi permessi di organizzatore",
|
||||
"grantModerator": "Concedi permessi da relatore",
|
||||
"hangup": "Lascia la riunione",
|
||||
"heading": "Barra degli strumenti",
|
||||
"help": "Aiuto",
|
||||
@@ -1346,6 +1380,20 @@
|
||||
"videounmute": "Accendi videocamera"
|
||||
},
|
||||
"addPeople": "Aggiungi partecipanti alla chiamata",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "Cancellazione dell'eco"
|
||||
},
|
||||
"agc": {
|
||||
"label": "Controllo del guadagno automatico"
|
||||
},
|
||||
"ns": {
|
||||
"label": "Cancellazione del rumore"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "Stereo"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "Disabilita modalità larghezza di banda limitata",
|
||||
"audioOnlyOn": "Abilita modalità larghezza di banda limitata",
|
||||
"audioRoute": "Scegli il dispositivo audio",
|
||||
@@ -1412,11 +1460,12 @@
|
||||
"profile": "Modifica profilo",
|
||||
"raiseHand": "Alza la mano",
|
||||
"raiseYourHand": "Alza la mano",
|
||||
"reactionBoo": "Invia buu",
|
||||
"reactionClap": "Invia applauso",
|
||||
"reactionHeart": "Invia cuore",
|
||||
"reactionLaugh": "Invia risata",
|
||||
"reactionLike": "Invia mi piace",
|
||||
"reactionBoo": "Invia Buu",
|
||||
"reactionClap": "Invia Applauso",
|
||||
"reactionHeart": "Invia Cuore",
|
||||
"reactionLaugh": "Invia Risata",
|
||||
"reactionLike": "Invia Mi piace",
|
||||
"reactionLove": "Invia Love",
|
||||
"reactionSilence": "Invia senza parole",
|
||||
"reactionSurprised": "Invia a bocca aperta",
|
||||
"reactions": "Reazioni",
|
||||
@@ -1502,15 +1551,17 @@
|
||||
"connectionInfo": "Informazioni connessione",
|
||||
"demote": "Fai diventare spettatore",
|
||||
"domute": "Disattiva microfono",
|
||||
"domuteDesktop": "Interrompi la condivisione dello schermo",
|
||||
"domuteDesktopOfOthers": "Interrompi la condivisione dello schermo agli altri",
|
||||
"domuteOthers": "Disattiva microfono a tutti gli altri",
|
||||
"domuteVideo": "Disattiva videocamera",
|
||||
"domuteVideoOfOthers": "Disattiva videocamera a tutti gli altri",
|
||||
"flip": "Specchia",
|
||||
"grantModerator": "Concedi permessi da organizzatore",
|
||||
"grantModerator": "Concedi permessi da relatore",
|
||||
"hideSelfView": "Nascondi la tua immagine",
|
||||
"kick": "Espelli",
|
||||
"mirrorVideo": "Specchia il tuo video",
|
||||
"moderator": "Organizzatore",
|
||||
"moderator": "Relatore",
|
||||
"mute": "Il partecipante ha il microfono spento",
|
||||
"muted": "Microfono spento",
|
||||
"pinToStage": "Metti in primo piano",
|
||||
@@ -1557,7 +1608,7 @@
|
||||
"description": "Adesso sei uno spettatore in questa riunione.",
|
||||
"raiseHand": "Alza la mano",
|
||||
"title": "Ingresso nella riunione in corso",
|
||||
"wishToSpeak": "Se vuoi parlare, si prega di alzare la mano sotto e aspettare l'autorizzazione dell'organizzatore."
|
||||
"wishToSpeak": "Se vuoi parlare, si prega di alzare la mano sotto e aspettare l'autorizzazione del relatore."
|
||||
},
|
||||
"labelTooltip": "Numero di spettatori: {{count}}",
|
||||
"notification": {
|
||||
@@ -1618,7 +1669,7 @@
|
||||
"roomnameHint": "Inserisci il nome o l'URL della riunione a cui vuoi accedere. Puoi anche inventarti un nome, assicurati solo che le persone con cui vuoi collegarti lo conoscano, così che possano inserire lo stesso nome.",
|
||||
"sendFeedback": "Invia feedback",
|
||||
"settings": "Impostazioni",
|
||||
"startMeeting": "Inizia riunione",
|
||||
"startMeeting": "Avvia riunione",
|
||||
"terms": "Termini di utilizzo",
|
||||
"title": "Il sistema di videoconferenza sicuro, funzionale e completamente gratuito.",
|
||||
"upcomingMeetings": "Prossime riunioni"
|
||||
|
||||
@@ -112,7 +112,9 @@
|
||||
"disabled": "Tērzēšanas ziņojumu sūtīšana ir atspējota.",
|
||||
"enter": "Ienākt istabā",
|
||||
"error": "Kļūda: Jūsu ziņa netika nosūtīta. Cēlonis: {{error}}",
|
||||
"everyone": "Visi",
|
||||
"fieldPlaceHolder": "Rakstiet ziņu šeit",
|
||||
"guestsChatIndicator": "(viesis)",
|
||||
"lobbyChatMessageTo": "Vestibila tērzēšanas ziņa adresātam {{recipient}}",
|
||||
"message": "Ziņa",
|
||||
"messageAccessibleTitle": "{{user}} saka:",
|
||||
@@ -565,11 +567,15 @@
|
||||
"downloadFailedDescription": "Lūdzu, mēģiniet vēlreiz.",
|
||||
"downloadFailedTitle": "Lejuplādes kļūda",
|
||||
"downloadFile": "Lejuplādēt",
|
||||
"downloadStarted": "Sākta faila lejuplāde",
|
||||
"dragAndDrop": "Velciet un palaidiet failus šeit, vai jebkurā ekrāna vietā",
|
||||
"fileAlreadyUploaded": "Fails jau ir augšupielādēts šajā sanāksmē.",
|
||||
"fileAlreadyUploaded": "Fails jau ir augšuplādēts šajā sanāksmē.",
|
||||
"fileTooLargeDescription": "Lūdzu, pārliecinieties, vai faila lielums nepārsniedz {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Izvēlētais fails ir pārāk liels",
|
||||
"fileUploadProgress": "Faila augšuplādes gaita",
|
||||
"fileUploadedSuccessfully": "Fails veiksmīgi augšuplādēts",
|
||||
"removeFile": "Noņemt",
|
||||
"removeFileSuccess": "Fails veiksmīgi noņemts",
|
||||
"uploadFailedDescription": "Lūdzu, mēģiniet vēlreiz.",
|
||||
"uploadFailedTitle": "Augšuplādes kļūda",
|
||||
"uploadFile": "Kopīgot failu"
|
||||
|
||||
@@ -109,8 +109,10 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "O envio de mensagens de chat está desativado.",
|
||||
"enter": "Entrar na sala",
|
||||
"error": "Erro: a sua mensagem não foi enviada. Motivo: {{error}}",
|
||||
"everyone": "Todos",
|
||||
"fieldPlaceHolder": "Aa",
|
||||
"lobbyChatMessageTo": "Mensagem de chat na sala de espera para {{recipient}}",
|
||||
"message": "Mensagem",
|
||||
@@ -122,7 +124,10 @@
|
||||
"nickname": {
|
||||
"popover": "Escolha um apelido",
|
||||
"title": "Introduza um apelido para usar o chat",
|
||||
"titleWithPolls": "Introduza um apelido para usar o chat e as sondagens"
|
||||
"titleWithCC": "Insira um apelido para usar o chat e as legendas ocultas",
|
||||
"titleWithPolls": "Digite um apelido para usar o chat e as sondagens",
|
||||
"titleWithPollsAndCC": "Insira um apelido para utilizar o chat, as sondagens e as legendas ocultas",
|
||||
"titleWithPollsAndCCAndFileSharing": "Insira um apelido para utilizar o chat, as sondagens, as legendas e os ficheiros"
|
||||
},
|
||||
"noMessagesMessage": "Ainda não há mensagens na reunião. Comece aqui uma conversa!",
|
||||
"privateNotice": "Mensagem privada para {{recipient}}",
|
||||
@@ -131,10 +136,15 @@
|
||||
"systemDisplayName": "Sistema",
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"closedCaptions": "LO",
|
||||
"fileSharing": "Ficheiros",
|
||||
"polls": "Sondagens"
|
||||
},
|
||||
"title": "Chat",
|
||||
"titleWithPolls": "Chat e Sondagens",
|
||||
"titleWithCC": "LO",
|
||||
"titleWithFeatures": "Chat e",
|
||||
"titleWithFileSharing": "Ficheiros",
|
||||
"titleWithPolls": "Sondagens",
|
||||
"you": "você"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
@@ -144,6 +154,10 @@
|
||||
"dontShowAgain": "Não me mostre isto outra vez",
|
||||
"installExtensionText": "Instalar a extensão para a integração Google Calendar e Office 365"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "O conteúdo das legendas ocultas estará disponível assim que um moderador iniciar a sessão.",
|
||||
"startClosedCaptionsButton": "Iniciar legendas ocultas"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "A ligá-lo à reunião…"
|
||||
},
|
||||
@@ -263,6 +277,8 @@
|
||||
"Remove": "Remover",
|
||||
"Share": "Partilhar",
|
||||
"Submit": "Submeter",
|
||||
"Understand": "Entendo, mantenha-me em silêncio por enquanto.",
|
||||
"UnderstandAndUnmute": "Entendo, por favor, desative o silêncio.",
|
||||
"WaitForHostMsg": "A conferência ainda não começou porque ainda não chegaram moderadores. Se quiser ser um moderador, inicie a sessão. Caso contrário, aguarde.",
|
||||
"WaitForHostNoAuthMsg": "A conferência ainda não começou porque ainda não chegaram os moderadores. Por favor, aguarde.",
|
||||
"WaitingForHostButton": "Esperar pelo moderador",
|
||||
@@ -285,6 +301,12 @@
|
||||
"alreadySharedVideoTitle": "Só é permitido um vídeo partilhado de cada vez",
|
||||
"applicationWindow": "Janela de aplicação",
|
||||
"authenticationRequired": "Autenticação necessária",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "Tire e envie uma foto usando a câmara do seu telemóvel",
|
||||
"ok": "Ligar a câmara",
|
||||
"reject": "Agora não",
|
||||
"title": "Tire uma foto"
|
||||
},
|
||||
"cameraConstraintFailedError": "A sua câmara não satisfaz algumas das restrições exigidas.",
|
||||
"cameraNotFoundError": "A câmara não foi encontrada.",
|
||||
"cameraNotSendingData": "Não podemos aceder à sua câmara. Verifique se outra aplicação está a utilizar este dispositivo, seleccione outro dispositivo do menu de definições ou tente recarregar a aplicação.",
|
||||
@@ -299,6 +321,7 @@
|
||||
"conferenceReloadMsg": "Estamos a tentar resolver isto. Reconexão em {{seconds}} seg…",
|
||||
"conferenceReloadTitle": "Infelizmente, algo correu mal.",
|
||||
"confirm": "Confirme",
|
||||
"confirmBack": "Voltar",
|
||||
"confirmNo": "Não",
|
||||
"confirmYes": "Sim",
|
||||
"connectError": "Oops! Algo correu mal e não conseguimos estabelecer uma ligação com a conferência.",
|
||||
@@ -307,8 +330,8 @@
|
||||
"contactSupport": "Contacte o suporte",
|
||||
"copied": "Copiado",
|
||||
"copy": "Cópia",
|
||||
"demoteParticipantDialog": "Tem a certeza de que pretende passar este participante para visitante?",
|
||||
"demoteParticipantTitle": "Passar a visitante",
|
||||
"demoteParticipantDialog": "Tem a certeza de que deseja mover este participante para espectador?",
|
||||
"demoteParticipantTitle": "Mover para espectador",
|
||||
"dismiss": "Dispensar",
|
||||
"displayNameRequired": "Olá! Qual é o seu nome?",
|
||||
"done": "Feito",
|
||||
@@ -334,7 +357,9 @@
|
||||
"kickParticipantButton": "Expulsar",
|
||||
"kickParticipantDialog": "Tem a certeza que quer expulsar este participante?",
|
||||
"kickParticipantTitle": "Expulsar este participante?",
|
||||
"kickSystemTitle": "Ai! Foste expulso da reunião.",
|
||||
"kickTitle": "Ai! {{participantDisplayName}} expulsou-o da reunião",
|
||||
"learnMore": "saiba mais",
|
||||
"linkMeeting": "Link da reunião",
|
||||
"linkMeetingTitle": "Link da reunião à Força de Vendas",
|
||||
"liveStreaming": "Transmissão em direto",
|
||||
@@ -356,23 +381,35 @@
|
||||
"micPermissionDeniedError": "Não concedeu autorização para utilizar o seu microfone. Ainda pode participar na conferência, mas outros não o ouvirão. Use o botão da câmara na barra de endereço para corrigir isto.",
|
||||
"micTimeoutError": "Não foi possível iniciar a fonte de áudio. Tempo limite expirado!",
|
||||
"micUnknownError": "Não pode usar microfone por uma razão desconhecida.",
|
||||
"moderationAudioLabel": "Permitir aos participantes ligar o som",
|
||||
"moderationVideoLabel": "Permitir aos participantes ligar a câmara",
|
||||
"moderationAudioLabel": "Permitir aos não moderadores ligar o som",
|
||||
"moderationDesktopLabel": "Permitir que não moderadores partilhem o seu ecrã",
|
||||
"moderationVideoLabel": "Permitir que não moderadores iniciem os seus vídeos",
|
||||
"muteEveryoneDialog": "Os participantes podem ligar o som a qualquer momento.",
|
||||
"muteEveryoneDialogModerationOn": "Os participantes podem enviar um pedido para falar a qualquer momento.",
|
||||
"muteEveryoneElseDialog": "Uma vez silenciados, não poderá reativá-los, mas eles podem ligar o microfone a qualquer momento.",
|
||||
"muteEveryoneElseTitle": "Silenciar todos excepto {{whom}}?",
|
||||
"muteEveryoneElsesDesktopDialog": "Depois que o compartilhamento for interrompido, não será possível reiniciá-lo, mas eles poderão fazê-lo a qualquer momento.",
|
||||
"muteEveryoneElsesDesktopTitle": "SInterromper a partilha de ecrã de todos, exceto {{whom}}?",
|
||||
"muteEveryoneElsesVideoDialog": "Quando a câmara for desligada, não poderá voltar a ligá-la, mas eles podem voltar a ligá-la em qualquer momento.",
|
||||
"muteEveryoneElsesVideoTitle": "Parar o vídeo de todos excepto {{whom}}?",
|
||||
"muteEveryoneSelf": "você mesmo",
|
||||
"muteEveryoneStartMuted": "A partir de agora, toda a gente começa a ficar calada",
|
||||
"muteEveryoneTitle": "Silenciar toda a gente?",
|
||||
"muteEveryonesDesktopDialog": "Os participantes podem partilhar o seu ecrã a qualquer momento.",
|
||||
"muteEveryonesDesktopDialogModerationOn": "Os participantes podem enviar um pedido para partilhar o seu ecrã a qualquer momento.",
|
||||
"muteEveryonesDesktopTitle": "Interromper a partilha de ecrã de todos?",
|
||||
"muteEveryonesVideoDialog": "Os participantes podem ligar a sua câmara a qualquer momento.",
|
||||
"muteEveryonesVideoDialogModerationOn": "Os participantes podem enviar um pedido para ligar a sua câmara a qualquer momento.",
|
||||
"muteEveryonesVideoDialogOk": "Desativar",
|
||||
"muteEveryonesVideoTitle": "Desligar a câmara de todos?",
|
||||
"muteParticipantBody": "Não poderá reativá-los, mas eles podem reativar-se a qualquer momento.",
|
||||
"muteParticipantButton": "Silenciar",
|
||||
"muteParticipantsDesktopBody": "Não poderá iniciar a partilha de ecrã deles, mas eles podem fazê-lo a qualquer momento.",
|
||||
"muteParticipantsDesktopBodyModerationOn": "Não será possível iniciar a partilha de ecrã nem para si nem para eles.",
|
||||
"muteParticipantsDesktopButton": "Parar a partilha de ecrã",
|
||||
"muteParticipantsDesktopDialog": "Tem a certeza de que deseja desativar a partilha de ecrã deste participante? Não será possível reiniciá-la, mas ele poderá fazê-lo a qualquer momento.",
|
||||
"muteParticipantsDesktopDialogModerationOn": "Tem a certeza de que deseja desativar a partilha de ecrã deste participante? Não será possível reativar o ecrã, nem para si nem para ele.",
|
||||
"muteParticipantsDesktopTitle": "Desativar a partilha de ecrã deste participante?",
|
||||
"muteParticipantsVideoBody": "Não poderá voltar a ligar a câmara, mas eles podem voltar a ligá-la a qualquer momento.",
|
||||
"muteParticipantsVideoBodyModerationOn": "Não será capaz de voltar a ligar a câmara e eles também não.",
|
||||
"muteParticipantsVideoButton": "Parar vídeo",
|
||||
@@ -392,6 +429,10 @@
|
||||
"recentlyUsedObjects": "Os seus objetos recentemente utilizados",
|
||||
"recording": "A gravar",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Não possível enquanto a transmissão em direto estiver activa",
|
||||
"recordingInProgressDescription": "Esta reunião está a ser gravada e analisada pela IA{{learnMore}}. O seu áudio e vídeo foram silenciados. Se optar por ativar o som, concorda em ser gravado.",
|
||||
"recordingInProgressDescriptionFirstHalf": "Esta reunião está a ser gravada e analisada por IA.",
|
||||
"recordingInProgressDescriptionSecondHalf": ". Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
|
||||
"recordingInProgressTitle": "Gravação em andamento",
|
||||
"rejoinNow": "Reingressar agora",
|
||||
"remoteControlAllowedMessage": "{{user}} aceitou o seu pedido de controlo remoto!",
|
||||
"remoteControlDeniedMessage": "{{user}} rejeitou o seu pedido de controlo remoto!",
|
||||
@@ -439,7 +480,10 @@
|
||||
"shareScreenWarningD2": "é necessário parar a partilha de áudio, iniciar a partilha de ecrã e verificar a opção \"partilhar áudio\".",
|
||||
"shareScreenWarningH1": "Se quiser partilhar apenas o seu ecrã:",
|
||||
"shareScreenWarningTitle": "Tem de parar a partilha de áudio antes de partilhar o seu ecrã",
|
||||
"shareVideoLinkError": "Por favor, forneça um link correcto do vídeo.",
|
||||
"shareVideoConfirmPlay": "Está prestes a abrir um site externo. Deseja continuar?",
|
||||
"shareVideoConfirmPlayTitle": "{{name}} partilhou um vídeo consigo.",
|
||||
"shareVideoLinkError": "Oops, este vídeo não pode ser reproduzido.",
|
||||
"shareVideoLinkStopped": "O vídeo de {{name}} foi interrompido.",
|
||||
"shareVideoTitle": "Partilhar vídeo",
|
||||
"shareYourScreen": "Partilhe o seu ecrã",
|
||||
"shareYourScreenDisabled": "Partilha de ecrã desactivada.",
|
||||
@@ -518,6 +562,21 @@
|
||||
"veryBad": "Muito má",
|
||||
"veryGood": "Muito boa"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "Por favor, tente novamente.",
|
||||
"downloadFailedTitle": "Falha no descarregar",
|
||||
"downloadFile": "Descarregar",
|
||||
"dragAndDrop": "Arraste e solte os ficheiros aqui ou em qualquer lugar do ecrã",
|
||||
"fileAlreadyUploaded": "O ficheiro já foi carregado para esta reunião.",
|
||||
"fileTooLargeDescription": "Certifique-se de que o ficheiro não exceda {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "O ficheiro selecionado é muito grande",
|
||||
"fileUploadProgress": "Progresso do envio do ficheiro",
|
||||
"fileUploadedSuccessfully": "Ficheiro carregado com sucesso",
|
||||
"removeFile": "Remover",
|
||||
"uploadFailedDescription": "Por favor, tente novamente.",
|
||||
"uploadFailedTitle": "Falha ao carregar",
|
||||
"uploadFile": "Partilhar ficheiro"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Miniaturas de vídeo"
|
||||
@@ -638,6 +697,7 @@
|
||||
"on": "Iniciada a transmissão em direto",
|
||||
"onBy": "{{name}} iniciou a transmissão em direto",
|
||||
"pending": "Início da transmissão em direto…",
|
||||
"policyError": "Tentou iniciar uma transmissão ao vivo muito rapidamente. Por favor, tente novamente mais tarde!",
|
||||
"serviceName": "Serviço de Transmissão em Direto",
|
||||
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
|
||||
"signIn": "Iniciar sessão com o Google",
|
||||
@@ -728,7 +788,10 @@
|
||||
"me": "eu",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "Vulnerabilidade de segurança!",
|
||||
"allowAction": "Permitir",
|
||||
"allowAll": "Permitir tudo",
|
||||
"allowAudio": "Permitir áudio",
|
||||
"allowDesktop": "Permitir partilha de ecrã",
|
||||
"allowVideo": "Permitir vídeo",
|
||||
"allowedUnmute": "Pode ligar o seu microfone, ligar a sua câmara ou partilhar o seu ecrã.",
|
||||
"audioUnmuteBlockedDescription": "A operação de ligar o microfone foi temporariamente bloqueada devido aos limites do sistema.",
|
||||
"audioUnmuteBlockedTitle": "Ligar microfone bloqueado!",
|
||||
@@ -736,12 +799,15 @@
|
||||
"connectedOneMember": "{{name}} entrou na reunião",
|
||||
"connectedThreePlusMembers": "{{name}} e muitos outros entraram na reunião",
|
||||
"connectedTwoMembers": "{{first}} e {{second}} entraram na reunião",
|
||||
"connectionFailed": "Falha na ligação. Por favor, tente novamente mais tarde!",
|
||||
"dataChannelClosed": "A qualidade do vídeo pode ser afetada",
|
||||
"dataChannelClosedDescription": "O canal de ponte está em baixo e, por isso, a qualidade de vídeo pode estar limitada à sua definição mais baixa.",
|
||||
"dataChannelClosedDescriptionWithAudio": "O canal de ponte está em baixo, pelo que podem ocorrer interrupções no áudio e no vídeo.",
|
||||
"dataChannelClosedWithAudio": "A qualidade do áudio e do vídeo pode ser afetada",
|
||||
"desktopMutedRemotelyTitle": "A partilha do seu ecrã foi interrompida por {{participantDisplayName}}",
|
||||
"disabledIframe": "A incorporação destina-se apenas a fins de demonstração, pelo que esta chamada será desligada em {{timeout}} minutos.",
|
||||
"disabledIframeSecondary": "A incorporação de {{domain}} destina-se apenas a fins de demonstração, pelo que esta chamada será desligada em {{timeout}} minutos. Por favor, use <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> para incorporação em produção!",
|
||||
"disabledIframeSecondaryNative": "A incorporação de {{domain}} destina-se apenas a fins de demonstração, pelo que esta chamada será desligada em {{timeout}} minutos.",
|
||||
"disabledIframeSecondaryWeb": "A incorporação de {{domain}} destina-se apenas a fins de demonstração, pelo que esta chamada será desligada em {{timeout}} minutos. Utilize <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> para incorporação em produção!",
|
||||
"disconnected": "desconectado",
|
||||
"displayNotifications": "Mostrar notificações para",
|
||||
"dontRemindMe": "Não me lembre",
|
||||
@@ -749,7 +815,10 @@
|
||||
"focusFail": "{{component}} não disponĩvel - tente em {{ms}} seg.",
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Notificações",
|
||||
"hostAskedUnmute": "O moderador gostaria que você falasse",
|
||||
"hostAskedUnmute": "O moderador gostaria que participasse.",
|
||||
"invalidTenant": "Tenant inválido",
|
||||
"invalidTenantHyphenDescription": "O tenant que está a utilizar é inválido (começa ou termina com '-').",
|
||||
"invalidTenantLengthDescription": "O tenant que está a utilizar é demasiado longo.",
|
||||
"invitedOneMember": "{{displayName}} foi convidado",
|
||||
"invitedThreePlusMembers": "{{name}} e {{count}} outros foram convidados",
|
||||
"invitedTwoMembers": "{{first}} e {{second}} foram convidados",
|
||||
@@ -787,9 +856,9 @@
|
||||
"newDeviceAudioTitle": "Novo dispositivo de áudio detetado",
|
||||
"newDeviceCameraTitle": "Nova câmara detetada",
|
||||
"nextToSpeak": "É o próximo na fila para falar",
|
||||
"noiseSuppressionDesktopAudioDescription": "A supressão de ruído não pode ser ativada enquanto se partilha o áudio do ambiente de trabalho, por favor desative-o e tente novamente.",
|
||||
"noiseSuppressionFailedTitle": "Falha ao iniciar a supressão de ruído",
|
||||
"noiseSuppressionStereoDescription": "A supressão do ruído de áudio estéreo não é atualmente suportada.",
|
||||
"noiseSuppressionDesktopAudioDescription": "A supressão de ruído extra não pode ser ativada enquanto estiver a partilhar o áudio do ambiente de trabalho. Desative-a e tente novamente.",
|
||||
"noiseSuppressionFailedTitle": "Falha ao iniciar a supressão de ruído extra",
|
||||
"noiseSuppressionStereoDescription": "Atualmente, a supressão extra de ruído não é suportada com áudio estéreo.",
|
||||
"oldElectronClientDescription1": "Parece estar a utilizar uma versão antiga do cliente Jitsi Meet que tem vulnerabilidades de segurança conhecidas. Por favor, certifique-se de que actualiza a nossa ",
|
||||
"oldElectronClientDescription2": "compilação mais recente",
|
||||
"oldElectronClientDescription3": " agora!",
|
||||
@@ -798,7 +867,7 @@
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) definido por outro participante",
|
||||
"raiseHandAction": "Levantar a mão",
|
||||
"raisedHand": "Gostaria de falar.",
|
||||
"raisedHand": "Gostaria de participar.",
|
||||
"raisedHands": "{{participantName}} e mais {{raisedHands}} pessoas",
|
||||
"reactionSounds": "Desactivar sons",
|
||||
"reactionSoundsForAll": "Desativar sons para todos",
|
||||
@@ -816,15 +885,17 @@
|
||||
"suggestRecordingDescription": "Gostaria de iniciar uma gravação?",
|
||||
"suggestRecordingTitle": "Gravar esta reunião",
|
||||
"unmute": "Ligar microfone",
|
||||
"unmuteScreen": "Iniciar partilha de ecrã",
|
||||
"unmuteVideo": "Ligar câmara",
|
||||
"videoMutedRemotelyDescription": "Pode sempre ligá-la novamente.",
|
||||
"videoMutedRemotelyTitle": "A sua câmara foi desligada pelo {{participantDisplayName}}.",
|
||||
"videoUnmuteBlockedDescription": "A operação de ligar a câmara e partilhar o ambiente de trabalho foi temporariamente bloqueada devido aos limites do sistema.",
|
||||
"videoUnmuteBlockedTitle": "Está bloqueado ligar a câmara e partilhar o ambiente de trabalho!",
|
||||
"viewLobby": "Ver sala de espera",
|
||||
"viewParticipants": "Ver participantes",
|
||||
"viewVisitors": "Ver visitantes",
|
||||
"viewVisitors": "Visualizar espectadores",
|
||||
"waitingParticipants": "{{waitingParticipants}} pessoas",
|
||||
"waitingVisitors": "Visitantes em fila de espera: {{waitingVisitors}}",
|
||||
"waitingVisitors": "Espectadores em fila de espera: {{waitingVisitors}}",
|
||||
"waitingVisitorsTitle": "A reunião ainda não está em direto!",
|
||||
"whiteboardLimitDescription": "Guarde o seu progresso, pois o limite de utilizadores será atingido em breve e o quadro branco será encerrado.",
|
||||
"whiteboardLimitTitle": "Utilização do quadro branco"
|
||||
@@ -833,14 +904,17 @@
|
||||
"actions": {
|
||||
"admit": "Aceitar",
|
||||
"admitAll": "Aceitar todos",
|
||||
"allow": "Permitir aos participantes:",
|
||||
"allow": "Permitir que os não moderadores:",
|
||||
"allowDesktop": "Permitir partilha de ecrã",
|
||||
"allowVideo": "Permitir vídeo",
|
||||
"askDesktop": "Pedir para partilhar o ecrã",
|
||||
"askUnmute": "Pedir para ligar o som",
|
||||
"audioModeration": "Ligar o microfone deles",
|
||||
"blockEveryoneMicCamera": "Bloquear o microfone e a câmara de todos",
|
||||
"breakoutRooms": "Salas simultâneas",
|
||||
"desktopModeration": "Iniciar partilha de ecrã",
|
||||
"goLive": "Aceder ao vivo",
|
||||
"invite": "Convidar alguém",
|
||||
"invite": "Convide alguém",
|
||||
"lowerAllHands": "Baixar todas as mãos",
|
||||
"lowerHand": "Baixar a mão",
|
||||
"moreModerationActions": "Mais opções de moderação",
|
||||
@@ -850,6 +924,8 @@
|
||||
"muteAll": "Silenciar todos",
|
||||
"muteEveryoneElse": "Silenciar todos os outros",
|
||||
"reject": "Rejeitar",
|
||||
"stopDesktop": "Parar a partilha de ecrã",
|
||||
"stopEveryonesDesktop": "Interromper a partilha de ecrã de todos",
|
||||
"stopEveryonesVideo": "Desligar a câmara de todos",
|
||||
"stopVideo": "Desligar a câmara",
|
||||
"unblockEveryoneMicCamera": "Desbloquear o microfone e a câmara de todos",
|
||||
@@ -859,12 +935,15 @@
|
||||
"headings": {
|
||||
"lobby": "Sala de espera ({{count}})",
|
||||
"participantsList": "Participantes da reunião ({{count}})",
|
||||
"viewerRequests": "Pedidos dos espectadores {{count}}",
|
||||
"visitorInQueue": " (à espera {{count}})",
|
||||
"visitorRequests": " (pedidos {{count}})",
|
||||
"visitors": "Visitantes ({{count}})",
|
||||
"visitors": "Espectadores ({{count}})",
|
||||
"visitorsList": "Espectadores ({{count}})",
|
||||
"waitingLobby": "Aguardam na sala de espera ({{count}})"
|
||||
},
|
||||
"search": "Pesquisar participantes",
|
||||
"searchDescription": "Comece a digitar para filtrar os participantes",
|
||||
"title": "Participantes"
|
||||
},
|
||||
"passwordDigitsOnly": "Até {{number}} dígitos",
|
||||
@@ -901,7 +980,7 @@
|
||||
},
|
||||
"results": {
|
||||
"changeVote": "Mudar o voto",
|
||||
"empty": "Ainda não há sondagens na reunião. Comece aqui uma sondagem!",
|
||||
"empty": "Ainda não há sondagens na reunião.",
|
||||
"hideDetailedResults": "Ocultar detalhes",
|
||||
"showDetailedResults": "Mostrar detalhes",
|
||||
"vote": "Voto"
|
||||
@@ -919,9 +998,11 @@
|
||||
"configuringDevices": "A configurar os dispositivos…",
|
||||
"connectedWithAudioQ": "Está ligado com áudio?",
|
||||
"connection": {
|
||||
"failed": "Falha no teste de ligação!",
|
||||
"good": "A sua ligação à Internet parece boa!",
|
||||
"nonOptimal": "A sua ligação à Internet não é óptima",
|
||||
"poor": "Tem uma má ligação à Internet"
|
||||
"poor": "Tem uma ligação à Internet fraca",
|
||||
"running": "A realizar teste de ligação..."
|
||||
},
|
||||
"connectionDetails": {
|
||||
"audioClipping": "Prevemos que o seu áudio tenha cortes.",
|
||||
@@ -930,6 +1011,7 @@
|
||||
"goodQuality": "Fantástico! A qualidade dos seus meios de comunicação vai ser óptima.",
|
||||
"noMediaConnectivity": "Não foi possível encontrar uma forma de estabelecer a conectividade dos meios de comunicação para este teste. Isto é tipicamente causado por uma firewall ou NAT.",
|
||||
"noVideo": "Prevemos que o seu vídeo seja terrível.",
|
||||
"testFailed": "O teste de ligação encontrou problemas inesperados, mas isso pode não afetar a sua experiência.",
|
||||
"undetectable": "Se mesmo assim não conseguir fazer chamadas no browser, recomendamos que se certifique de que os seus altifalantes, microfone e câmara estão devidamente configurados, que concedeu ao seu browser direitos de utilização do seu microfone e câmara, e que a versão do seu browser está actualizada. Se mesmo assim tiver problemas em telefonar, deverá contactar o criador da aplicação web.",
|
||||
"veryPoorConnection": "Prevemos que a qualidade da sua chamada seja realmente terrível.",
|
||||
"videoFreezing": "Prevemos que o seu vídeo congele, fique preto, e seja pixelizado.",
|
||||
@@ -958,7 +1040,7 @@
|
||||
"joinWithoutAudio": "Entrar sem áudio",
|
||||
"keyboardShortcuts": "Ativar os atalhos de teclado",
|
||||
"linkCopied": "Link copiado para a área de transferência",
|
||||
"lookGood": "Tudo está a funcionar corretamente",
|
||||
"lookGood": "Os seus dispositivos estão a funcionar corretamente",
|
||||
"or": "ou",
|
||||
"premeeting": "Pré-reunião",
|
||||
"proceedAnyway": "Continuar na mesma",
|
||||
@@ -1044,6 +1126,7 @@
|
||||
"onBy": "{{name}} iniciou a gravação",
|
||||
"onlyRecordSelf": "Gravar apenas as minhas transmissões áudio e vídeo",
|
||||
"pending": "Preparando para gravar a reunião…",
|
||||
"policyError": "Tentou iniciar uma gravação muito rapidamente. Por favor, tente novamente mais tarde!",
|
||||
"recordAudioAndVideo": "Gravar áudio e vídeo",
|
||||
"recordTranscription": "Gravar transcrições",
|
||||
"saveLocalRecording": "Guardar ficheiro de gravação localmente (Beta)",
|
||||
@@ -1087,11 +1170,13 @@
|
||||
"signedIn": "Atualmente a aceder a eventos de calendário por {{email}}. Clique no botão Desconectar abaixo para parar de aceder a eventos de calendário.",
|
||||
"title": "Calendário"
|
||||
},
|
||||
"chatWithPermissions": "O chat requer permissão",
|
||||
"desktopShareFramerate": "Taxa de fotogramas para partilha do ambiente de trabalho",
|
||||
"desktopShareHighFpsWarning": "Uma taxa de fotogramas mais elevada para a partilha do ambiente de trabalho pode afectar a sua largura de banda. É necessário reiniciar a partilha de ecrã para que as novas definições entrem em vigor.",
|
||||
"desktopShareWarning": "É necessário reiniciar a partilha do ecrã para que as novas definições entrem em vigor.",
|
||||
"devices": "Dispositivos",
|
||||
"followMe": "Todos me seguem",
|
||||
"followMeRecorder": "O gravador segue-me",
|
||||
"framesPerSecond": "fotogramas-por-segundo",
|
||||
"incomingMessage": "Receber uma mensagem",
|
||||
"language": "Idioma",
|
||||
@@ -1115,6 +1200,7 @@
|
||||
"selectMic": "Microfone",
|
||||
"selfView": "Autovisualização",
|
||||
"shortcuts": "Atalhos",
|
||||
"showSubtitlesOnStage": "Mostrar legendas",
|
||||
"speakers": "Altifalantes",
|
||||
"startAudioMuted": "Todos começam com microfone desligado",
|
||||
"startReactionsMuted": "Todos começam com os sons de reação desativados",
|
||||
@@ -1168,11 +1254,13 @@
|
||||
"fearful": "Temeroso",
|
||||
"happy": "Feliz",
|
||||
"hours": "{{count}}h",
|
||||
"labelTooltip": "Número de participantes: {{count}}",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Nome",
|
||||
"neutral": "Neutro",
|
||||
"sad": "Triste",
|
||||
"search": "Pesquisar",
|
||||
"searchDescription": "Comece a digitar para filtrar os participantes",
|
||||
"searchHint": "Pesquisar participantes",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Estatísticas dos Participantes",
|
||||
@@ -1209,6 +1297,7 @@
|
||||
"closeChat": "Fechar chat",
|
||||
"closeMoreActions": "Fechar menu de mais ações",
|
||||
"closeParticipantsPane": "Fechar painel de participantes",
|
||||
"closedCaptions": "Legendas ocultas",
|
||||
"collapse": "Colapsar",
|
||||
"document": "Mudar para documento partilhado",
|
||||
"documentClose": "Fechar documento partilhado",
|
||||
@@ -1238,6 +1327,7 @@
|
||||
"lobbyButton": "Ativar/desativar sala de espera",
|
||||
"localRecording": "Mudar os controlos locais de gravação",
|
||||
"lockRoom": "Mudar palavra-chave de reunião",
|
||||
"love": "Coração",
|
||||
"lowerHand": "Baixar a mão",
|
||||
"moreActions": "Mais ações",
|
||||
"moreActionsMenu": "Menu de mais ações",
|
||||
@@ -1248,13 +1338,14 @@
|
||||
"muteEveryoneElsesVideo": "Parar o vídeo de todos os outros",
|
||||
"muteEveryonesVideo": "Parar o vídeo de todos",
|
||||
"muteGUMPending": "A ligar o seu microfone",
|
||||
"noiseSuppression": "Supressão de ruído",
|
||||
"noiseSuppression": "Supressão extra de ruído (BETA)",
|
||||
"openChat": "Abrir chat",
|
||||
"participants": "Abrir painel de participantes",
|
||||
"participants": "Abrir painel de participantes. {{participantsCount}} participantes",
|
||||
"pip": "Mudar para o modo Picture-in-Picture",
|
||||
"privateMessage": "Enviar mensagem privada",
|
||||
"profile": "Editar o seu perfil",
|
||||
"raiseHand": "Levantar a mão",
|
||||
"react": "Reações às mensagens",
|
||||
"reactions": "Reações",
|
||||
"reactionsMenu": "Menu de reações",
|
||||
"recording": "Mudar gravação",
|
||||
@@ -1297,14 +1388,15 @@
|
||||
"closeChat": "Fechar chat",
|
||||
"closeParticipantsPane": "Fechar painel de participantes",
|
||||
"closeReactionsMenu": "Fechar menu de reações",
|
||||
"disableNoiseSuppression": "Desativar a supressão de ruído",
|
||||
"closedCaptions": "Legendas ocultas",
|
||||
"disableNoiseSuppression": "Desativar supressão de ruído extra (BETA)",
|
||||
"disableReactionSounds": "Pode desactivar os sons de reacção para esta reunião",
|
||||
"documentClose": "Fechar documento partilhado",
|
||||
"documentOpen": "Abrir documento partilhado",
|
||||
"download": "Descarregar as nossas aplicações",
|
||||
"e2ee": "Criptografia ponta a ponta",
|
||||
"embedMeeting": "Incorporar reunião",
|
||||
"enableNoiseSuppression": "Ativar a supressão de ruído",
|
||||
"enableNoiseSuppression": "Ativar supressão extra de ruído (BETA)",
|
||||
"endConference": "Terminar reunião para todos",
|
||||
"enterFullScreen": "Ver em ecrã completo",
|
||||
"enterTileView": "Ver em quadrícula",
|
||||
@@ -1326,6 +1418,7 @@
|
||||
"lobbyButtonEnable": "Ativar sala de espera",
|
||||
"login": "Iniciar sessão",
|
||||
"logout": "Terminar sessão",
|
||||
"love": "Coração",
|
||||
"lowerYourHand": "Baixar a mão",
|
||||
"moreActions": "Mais ações",
|
||||
"moreOptions": "Mais opções",
|
||||
@@ -1338,7 +1431,7 @@
|
||||
"noAudioSignalDialInDesc": "Também pode marcar usando:",
|
||||
"noAudioSignalDialInLinkDesc": "Números de marcação",
|
||||
"noAudioSignalTitle": "Não há nenhuma entrada vinda do seu microfone!",
|
||||
"noiseSuppression": "Supressão de ruído",
|
||||
"noiseSuppression": "Supressão extra de ruído (BETA)",
|
||||
"noisyAudioInputDesc": "Parece que o seu microfone está a fazer barulho, por favor considere silenciar ou mudar de dispositivo.",
|
||||
"noisyAudioInputTitle": "Seu microfone parece estar barulhento!",
|
||||
"openChat": "Abrir chat",
|
||||
@@ -1351,6 +1444,7 @@
|
||||
"raiseYourHand": "Levantar a mão",
|
||||
"reactionBoo": "Enviar reação de vaia",
|
||||
"reactionClap": "Enviar reação de aplausos",
|
||||
"reactionHeart": "Enviar reação com coração",
|
||||
"reactionLaugh": "Enviar reação de risos",
|
||||
"reactionLike": "Enviar reação de aprovado",
|
||||
"reactionSilence": "Enviar reação de silêncio",
|
||||
@@ -1384,15 +1478,19 @@
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Iniciar/parar legendas",
|
||||
"expandedLabel": "Transcrição ativada",
|
||||
"failedToStart": "Transcrição falhou ao iniciar",
|
||||
"labelToolTip": "A reunião esta sendo transcrita",
|
||||
"failed": "Falha na transcrição",
|
||||
"labelTooltip": "Esta reunião está a ser transcrita.",
|
||||
"labelTooltipExtra": "Além disso, uma transcrição estará disponível posteriormente.",
|
||||
"openClosedCaptions": "Abrir legendas ocultas",
|
||||
"original": "Original",
|
||||
"sourceLanguageDesc": "Atualmente a língua da reunião está definida para <b>{{sourceLanguage}}</b>. <br/> Pode alterá-la a partir ",
|
||||
"sourceLanguageHere": "daqui",
|
||||
"start": "Exibir legendas",
|
||||
"stop": "Não exibir legendas",
|
||||
"subtitles": "Legendas",
|
||||
"subtitlesOff": "Desligado",
|
||||
"tr": "TR"
|
||||
"tr": "TR",
|
||||
"translateTo": "Traduzir para"
|
||||
},
|
||||
"unpinParticipant": "{{participantName}} - Desafixar",
|
||||
"userMedia": {
|
||||
@@ -1424,7 +1522,7 @@
|
||||
"ldTooltip": "Ver vídeo em baixa definição",
|
||||
"lowDefinition": "Baixa definição (LD)",
|
||||
"performanceSettings": "Definições de desempenho",
|
||||
"recording": "Gravação em curso",
|
||||
"recording": "Esta reunião está a ser gravada.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Ver vídeo em definição padrão",
|
||||
"standardDefinition": "Definição padrão",
|
||||
@@ -1432,8 +1530,10 @@
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Informações sobre a ligação",
|
||||
"demote": "Passar a visitante",
|
||||
"demote": "Passar a espectador",
|
||||
"domute": "Sem som",
|
||||
"domuteDesktop": "Parar a partilha de ecrã",
|
||||
"domuteDesktopOfOthers": "Interromper a partilha de ecrã para todos os outros",
|
||||
"domuteOthers": "Silenciar todos os outros",
|
||||
"domuteVideo": "Desativar a câmara",
|
||||
"domuteVideoOfOthers": "Desactivar a câmara de todos os outros",
|
||||
@@ -1484,21 +1584,21 @@
|
||||
"webAssemblyWarningDescription": "WebAssembly desactivado ou não suportado por este navegador"
|
||||
},
|
||||
"visitors": {
|
||||
"chatIndicator": "(visitante)",
|
||||
"chatIndicator": "(espectador)",
|
||||
"joinMeeting": {
|
||||
"description": "Atualmente, é um observador nesta conferência.",
|
||||
"description": "Atualmente, é um espectador nesta conferência.",
|
||||
"raiseHand": "Levantar a mão",
|
||||
"title": "Participar na reunião",
|
||||
"wishToSpeak": "Se deseja intervir, levante a mão e aguarde a aprovação do moderador."
|
||||
},
|
||||
"labelTooltip": "Número de visitantes: {{count}}",
|
||||
"labelTooltip": "Número de espectadores: {{count}}",
|
||||
"notification": {
|
||||
"demoteDescription": "Enviado aqui pelo {{actor}}, levante a mão para participar",
|
||||
"noMainParticipantsDescription": "Um participante precisa de iniciar a reunião. Tente novamente daqui a pouco.",
|
||||
"noMainParticipantsTitle": "Esta reunião ainda não começou.",
|
||||
"noVisitorLobby": "Não é possível aderir enquanto houver uma sala de espera activada para a reunião.",
|
||||
"notAllowedPromotion": "É necessário que um participante autorize primeiro o seu pedido.",
|
||||
"title": "É um visitante na reunião"
|
||||
"title": "É um espectador na reunião"
|
||||
},
|
||||
"waitingMessage": "Participará na reunião assim que esta estiver em direto!"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -331,11 +331,11 @@
|
||||
"internalError": "Ett fel uppstod. Fel: {{error}}",
|
||||
"internalErrorTitle": "Internt fel",
|
||||
"kickMessage": "Du kan kontakta {{participantDisplayName}} för mer information.",
|
||||
"kickParticipantButton": "Sparka ut",
|
||||
"kickParticipantDialog": "Vill du sparka ut den här deltagaren?",
|
||||
"kickParticipantButton": "Ta bort från mötet",
|
||||
"kickParticipantDialog": "Vill du ta bort denna deltagaren från mötet?",
|
||||
"kickParticipantTitle": "Tysta deltagaren?",
|
||||
"kickSystemTitle": "Aj! du blev utsparkad ur mötet",
|
||||
"kickTitle": "Aj! {{participantDisplayName}} sparkade ut dig ur mötet",
|
||||
"kickSystemTitle": "Du har blivit borttagen från mötet",
|
||||
"kickTitle": "{{participantDisplayName}} tog bort dig från mötet",
|
||||
"linkMeeting": "Länka möte",
|
||||
"linkMeetingTitle": "Länka möte till Salesforce",
|
||||
"liveStreaming": "Streama",
|
||||
@@ -763,7 +763,7 @@
|
||||
"invitedThreePlusMembers": "{{name}} och {{count}} andra har bjudits in",
|
||||
"invitedTwoMembers": "{{first}} och {{second}} har bjudits in",
|
||||
"joinMeeting": "Delta",
|
||||
"kickParticipant": "{{kicked}} sparkades ut av {{kicker}}",
|
||||
"kickParticipant": "{{kicked}} togs bort av {{kicker}}",
|
||||
"leftOneMember": "{{name}} lämnade mötet",
|
||||
"leftThreePlusMembers": "{{name}} och många andra lämnade mötet",
|
||||
"leftTwoMembers": "{{first}} och {{second}} lämnade mötet",
|
||||
@@ -1245,7 +1245,7 @@
|
||||
"help": "Hjälp",
|
||||
"hideWhiteboard": "Dölj whiteboard",
|
||||
"invite": "Bjud in personer",
|
||||
"kick": "Sparka ut deltagare",
|
||||
"kick": "Ta bort deltagare",
|
||||
"laugh": "Skratta",
|
||||
"leaveConference": "Lämna mötet",
|
||||
"like": "Tummen upp",
|
||||
@@ -1459,7 +1459,7 @@
|
||||
"flip": "Vänd",
|
||||
"grantModerator": "Godkänn moderator",
|
||||
"hideSelfView": "Dölj självvyn",
|
||||
"kick": "Sparka ut",
|
||||
"kick": "Ta bort",
|
||||
"mirrorVideo": "Spegelvänd video",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Deltagaren har avstängd mikrofon",
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
"error": "Error: your message was not sent. Reason: {{error}}",
|
||||
"everyone": "Everyone",
|
||||
"fieldPlaceHolder": "Aa",
|
||||
"guestsChatIndicator": "(guest)",
|
||||
"lobbyChatMessageTo": "Lobby chat message to {{recipient}}",
|
||||
"message": "Message",
|
||||
"messageAccessibleTitle": "{{user}} says:",
|
||||
@@ -566,11 +567,15 @@
|
||||
"downloadFailedDescription": "Please try again.",
|
||||
"downloadFailedTitle": "Download failed",
|
||||
"downloadFile": "Download",
|
||||
"downloadStarted": "File download started",
|
||||
"dragAndDrop": "Drag and drop files here or anywhere on screen",
|
||||
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
|
||||
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "The selected file is too large",
|
||||
"fileUploadProgress": "File upload progress",
|
||||
"fileUploadedSuccessfully": "File uploaded successfully",
|
||||
"removeFile": "Remove",
|
||||
"removeFileSuccess": "File removed successfully",
|
||||
"uploadFailedDescription": "Please try again.",
|
||||
"uploadFailedTitle": "Upload failed",
|
||||
"uploadFile": "Share file"
|
||||
@@ -860,6 +865,7 @@
|
||||
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
|
||||
"oldElectronClientDescription2": "latest build",
|
||||
"oldElectronClientDescription3": " now!",
|
||||
"openChat": "Open chat",
|
||||
"participantWantsToJoin": "Wants to join the meeting",
|
||||
"participantsWantToJoin": "Want to join the meeting",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
|
||||
@@ -1374,6 +1380,20 @@
|
||||
"videounmute": "Start camera"
|
||||
},
|
||||
"addPeople": "Add people to your call",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "Acoustic echo cancellation"
|
||||
},
|
||||
"agc": {
|
||||
"label": "Automatic gain control"
|
||||
},
|
||||
"ns": {
|
||||
"label": "Noise suppression"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "Stereo"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "Disable low bandwidth mode",
|
||||
"audioOnlyOn": "Enable low bandwidth mode",
|
||||
"audioRoute": "Select the sound device",
|
||||
@@ -1445,6 +1465,7 @@
|
||||
"reactionHeart": "Send heart reaction",
|
||||
"reactionLaugh": "Send laugh reaction",
|
||||
"reactionLike": "Send thumbs up reaction",
|
||||
"reactionLove": "Send love reaction",
|
||||
"reactionSilence": "Send silence reaction",
|
||||
"reactionSurprised": "Send surprised reaction",
|
||||
"reactions": "Reactions",
|
||||
@@ -1596,6 +1617,8 @@
|
||||
"noMainParticipantsTitle": "This meeting hasn't started yet.",
|
||||
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
|
||||
"notAllowedPromotion": "A participant needs to allow your request first.",
|
||||
"requestToJoin": "Hand Raised",
|
||||
"requestToJoinDescription": "Your request was sent to the moderators. Hang tight!",
|
||||
"title": "You are a viewer in the meeting"
|
||||
},
|
||||
"waitingMessage": "You'll join the meeting as soon as it is live!"
|
||||
|
||||
154
package-lock.json
generated
154
package-lock.json
generated
@@ -51,6 +51,7 @@
|
||||
"clipboard-copy": "4.0.1",
|
||||
"clsx": "1.1.1",
|
||||
"dayjs": "1.11.13",
|
||||
"dompurify": "3.2.6",
|
||||
"dropbox": "10.7.0",
|
||||
"focus-visible": "5.1.0",
|
||||
"glob": "11.0.3",
|
||||
@@ -65,7 +66,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2044.0.0+437abe32/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2084.0.0+819cdfbb/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -103,6 +104,7 @@
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "124.0.4",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-worklets-core": "https://github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
@@ -124,6 +126,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.9",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-transform-private-methods": "7.25.9",
|
||||
"@babel/preset-env": "7.25.9",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
@@ -1000,6 +1004,43 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-nullish-coalescing-operator": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
|
||||
"integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
|
||||
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6",
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-optional-chaining": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
|
||||
"integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
|
||||
"deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
|
||||
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||
@@ -1839,13 +1880,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-optional-chaining": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz",
|
||||
"integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
|
||||
"integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -7957,6 +7998,13 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/unorm": {
|
||||
"version": "1.3.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/unorm/-/unorm-1.3.28.tgz",
|
||||
@@ -12437,6 +12485,15 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
@@ -18203,8 +18260,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2044.0.0+437abe32/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-LdYhBImyB3q7HV3KlJdww1Qw+Ke2vJOir32+8dYxJl9QdrQ5HedRXoF/+FF9+LdDIy5BG1o8M+To4/poYKLYTw==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2084.0.0+819cdfbb/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-O+T1/0z4+a3ERbJk2pipErcdThyCFOiAPRd+hOqq+PP4kB7NcP7k4coyeqbXoG+oPh3N60aPLFIg9u9Qlsh0AA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
@@ -22474,6 +22531,19 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-worklets-core": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"integrity": "sha512-SW47DvuNLjhoj8PJK8haq0B/69//ChG+/3WyK9NkhwUSM1kqZLle1O7SqGeLFp2f37qyBXH5X+cXPDaabJ8CHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"string-hash-64": "^1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-youtube-iframe": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.3.0.tgz",
|
||||
@@ -24457,6 +24527,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/string-hash-64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-hash-64/-/string-hash-64-1.0.3.tgz",
|
||||
"integrity": "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-replace-to-array": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
|
||||
@@ -27652,6 +27728,27 @@
|
||||
"@babel/helper-plugin-utils": "^7.25.9"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
|
||||
"integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6",
|
||||
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-optional-chaining": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
|
||||
"integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
|
||||
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||
@@ -28136,12 +28233,12 @@
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-optional-chaining": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz",
|
||||
"integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
|
||||
"integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.25.9",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9"
|
||||
"@babel/helper-plugin-utils": "^7.27.1",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-parameters": {
|
||||
@@ -32389,6 +32486,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
|
||||
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="
|
||||
},
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"optional": true
|
||||
},
|
||||
"@types/unorm": {
|
||||
"version": "1.3.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/unorm/-/unorm-1.3.28.tgz",
|
||||
@@ -35531,6 +35634,14 @@
|
||||
"domelementtype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||
"requires": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
@@ -39600,8 +39711,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2044.0.0+437abe32/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-LdYhBImyB3q7HV3KlJdww1Qw+Ke2vJOir32+8dYxJl9QdrQ5HedRXoF/+FF9+LdDIy5BG1o8M+To4/poYKLYTw==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2084.0.0+819cdfbb/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-O+T1/0z4+a3ERbJk2pipErcdThyCFOiAPRd+hOqq+PP4kB7NcP7k4coyeqbXoG+oPh3N60aPLFIg9u9Qlsh0AA==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
@@ -42634,6 +42745,14 @@
|
||||
"invariant": "2.2.4"
|
||||
}
|
||||
},
|
||||
"react-native-worklets-core": {
|
||||
"version": "git+ssh://git@github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"integrity": "sha512-SW47DvuNLjhoj8PJK8haq0B/69//ChG+/3WyK9NkhwUSM1kqZLle1O7SqGeLFp2f37qyBXH5X+cXPDaabJ8CHA==",
|
||||
"from": "react-native-worklets-core@https://github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"requires": {
|
||||
"string-hash-64": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"react-native-youtube-iframe": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.3.0.tgz",
|
||||
@@ -43991,6 +44110,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"string-hash-64": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-hash-64/-/string-hash-64-1.0.3.tgz",
|
||||
"integrity": "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw=="
|
||||
},
|
||||
"string-replace-to-array": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"clipboard-copy": "4.0.1",
|
||||
"clsx": "1.1.1",
|
||||
"dayjs": "1.11.13",
|
||||
"dompurify": "3.2.6",
|
||||
"dropbox": "10.7.0",
|
||||
"focus-visible": "5.1.0",
|
||||
"glob": "11.0.3",
|
||||
@@ -71,7 +72,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2044.0.0+437abe32/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2084.0.0+819cdfbb/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -109,6 +110,7 @@
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "124.0.4",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-worklets-core": "https://github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
@@ -130,6 +132,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.9",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.21.0",
|
||||
"@babel/plugin-transform-private-methods": "7.25.9",
|
||||
"@babel/preset-env": "7.25.9",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
|
||||
@@ -124,12 +124,11 @@ repositories {
|
||||
|
||||
|
||||
dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
// noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+"
|
||||
implementation 'com.squareup.duktape:duktape-android:1.3.0'
|
||||
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
// From node_modules
|
||||
// From node_modules
|
||||
}
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
|
||||
@@ -22,7 +22,6 @@ public class JitsiMeetReactNativePackage implements ReactPackage {
|
||||
new AppInfoModule(reactContext),
|
||||
new AudioModeModule(reactContext),
|
||||
new JMOngoingConferenceModule(reactContext),
|
||||
new JavaScriptSandboxModule(reactContext),
|
||||
new LocaleDetector(reactContext),
|
||||
new LogBridgeModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
|
||||
@@ -85,7 +85,12 @@
|
||||
"react-native-video": "0.0.0",
|
||||
"react-native-watch-connectivity": "0.0.0",
|
||||
"react-native-webrtc": "0.0.0",
|
||||
"react-native-webview": "0.0.0"
|
||||
"react-native-webview": "0.0.0",
|
||||
"react-native-worklets-core": "0.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "0.0.0",
|
||||
"@babel/plugin-proposal-optional-chaining": "0.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node sdk_instructions.js",
|
||||
|
||||
8
react-native-sdk/prepare_sdk.js
vendored
8
react-native-sdk/prepare_sdk.js
vendored
@@ -100,10 +100,6 @@ fs.copyFileSync(
|
||||
`${iosSrcPath}/InfoPlistUtil.h`,
|
||||
`${iosDestPath}/InfoPlistUtil.h`
|
||||
);
|
||||
fs.copyFileSync(
|
||||
`${iosSrcPath}/JavaScriptSandbox.m`,
|
||||
`${iosDestPath}/JavaScriptSandbox.m`
|
||||
);
|
||||
fs.copyFileSync(
|
||||
`${iosSrcPath}/JitsiAudioSession.m`,
|
||||
`${iosDestPath}/JitsiAudioSession.m`
|
||||
@@ -184,10 +180,6 @@ fs.copyFileSync(
|
||||
`${androidSourcePath}/ConnectionService.java`,
|
||||
`${androidTargetPath}/ConnectionService.java`
|
||||
);
|
||||
fs.copyFileSync(
|
||||
`${androidSourcePath}/JavaScriptSandboxModule.java`,
|
||||
`${androidTargetPath}/JavaScriptSandboxModule.java`
|
||||
);
|
||||
fs.copyFileSync(
|
||||
`${androidSourcePath}/LocaleDetector.java`,
|
||||
`${androidTargetPath}/LocaleDetector.java`
|
||||
|
||||
18
react-native-sdk/update_dependencies.js
vendored
18
react-native-sdk/update_dependencies.js
vendored
@@ -60,12 +60,12 @@ This is now set on your end.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
packageJSON.devDependencies = packageJSON.devDependencies || {};
|
||||
|
||||
packageJSON.overrides = packageJSON.overrides || {};
|
||||
|
||||
for (const key in RNSDKpackageJSON.overrides) {
|
||||
if (!packageJSON.overrides.hasOwnProperty(key)) {
|
||||
packageJSON.overrides[key] = RNSDKpackageJSON.overrides[key];
|
||||
for (const key in RNSDKpackageJSON.devDependencies) {
|
||||
if (!packageJSON.devDependencies.hasOwnProperty(key)) {
|
||||
packageJSON.devDependencies[key] = RNSDKpackageJSON.devDependencies[key];
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,14 @@ This is now set on your end.`
|
||||
|
||||
return item;
|
||||
}, {});
|
||||
|
||||
packageJSON.devDependencies = Object.keys(packageJSON.devDependencies)
|
||||
.sort()
|
||||
.reduce((item, itemKey) => {
|
||||
item[itemKey] = packageJSON.devDependencies[itemKey];
|
||||
|
||||
return item;
|
||||
}, {});
|
||||
|
||||
fs.writeFileSync(pathToPackageJSON, JSON.stringify(packageJSON, null, 2));
|
||||
|
||||
|
||||
7
react-native-sdk/update_sdk_dependencies.js
vendored
7
react-native-sdk/update_sdk_dependencies.js
vendored
@@ -26,6 +26,13 @@ function mergeDependencyVersions() {
|
||||
SDKPackageJSON.peerDependencies[key] = packageJSON.dependencies[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Updates SDK dev dependencies(used by react-native-worklets-core lib. babel plugin)
|
||||
for (const key in packageJSON.devDependencies) {
|
||||
if (SDKPackageJSON.devDependencies.hasOwnProperty(key)) {
|
||||
SDKPackageJSON.devDependencies[key] = packageJSON.devDependencies[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Set RN peer dependency.
|
||||
const rnVersion = semver.parse(packageJSON.dependencies['react-native']);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ToolbarButton } from '../../toolbox/types';
|
||||
import { ILoggingConfig } from '../logging/types';
|
||||
import { IAudioSettings } from '../settings/reducer';
|
||||
import { DesktopSharingSourceType } from '../tracks/types';
|
||||
|
||||
type ButtonsWithNotifyClick = 'camera' |
|
||||
@@ -191,6 +192,7 @@ export interface IConfig {
|
||||
appId?: string;
|
||||
audioLevelsInterval?: number;
|
||||
audioQuality?: {
|
||||
enableAdvancedAudioSettings?: boolean;
|
||||
opusMaxAverageBitrate?: number | null;
|
||||
stereo?: boolean;
|
||||
};
|
||||
@@ -237,6 +239,7 @@ export interface IConfig {
|
||||
inactiveDisabled?: boolean;
|
||||
};
|
||||
constraints?: {
|
||||
audio?: IAudioSettings;
|
||||
video?: {
|
||||
height?: {
|
||||
ideal?: number;
|
||||
@@ -521,7 +524,6 @@ export interface IConfig {
|
||||
preCallTestEnabled?: boolean;
|
||||
preCallTestICEUrl?: string;
|
||||
};
|
||||
prejoinPageEnabled?: boolean;
|
||||
raisedHands?: {
|
||||
disableLowerHandByModerator?: boolean;
|
||||
disableLowerHandNotification?: boolean;
|
||||
|
||||
@@ -202,7 +202,6 @@ export default [
|
||||
'prejoinConfig.enabled',
|
||||
'prejoinConfig.hideDisplayName',
|
||||
'prejoinConfig.hideExtraJoinButtons',
|
||||
'prejoinPageEnabled',
|
||||
'raisedHands',
|
||||
'recordingService',
|
||||
'requireDisplayName',
|
||||
|
||||
@@ -6,7 +6,9 @@ import { safeJsonParse } from '@jitsi/js-utils/json';
|
||||
import { isEmpty, mergeWith, pick } from 'lodash-es';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { browser } from '../lib-jitsi-meet';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import { isEmbedded } from '../util/embedUtils';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import { IConfig } from './configType';
|
||||
@@ -255,6 +257,17 @@ export function isDisplayNameVisible(state: IReduxState): boolean {
|
||||
return !state['features/base/config'].hideDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the advanced audio settings are enabled.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAdvancedAudioSettingsEnabled(state: IReduxState): boolean {
|
||||
|
||||
return !browser.isWebKitBased() && Boolean(state['features/base/config']?.audioQuality?.enableAdvancedAudioSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a Jitsi Meet config.js from {@code localStorage} if it was
|
||||
* previously downloaded from a specific {@code baseURL} and stored with
|
||||
@@ -335,7 +348,7 @@ export function setConfigFromURLParams(
|
||||
|
||||
overrideConfigJSON(config, interfaceConfig, json);
|
||||
|
||||
// Print warning about depricated URL params
|
||||
// Print warning about deprecated URL params
|
||||
if ('interfaceConfig.SUPPORT_URL' in params) {
|
||||
logger.warn('Using SUPPORT_URL interfaceConfig URL overwrite is deprecated.'
|
||||
+ ' Please use supportUrl from advanced branding!');
|
||||
@@ -371,6 +384,13 @@ export function setConfigFromURLParams(
|
||||
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
|
||||
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
|
||||
}
|
||||
|
||||
// When not in an iframe, start without media if the pre-join page is not enabled.
|
||||
if (!isEmbedded()
|
||||
&& 'config.prejoinConfig.enabled' in params && config.prejoinConfig?.enabled === false) {
|
||||
logger.warn('Using prejoinConfig.enabled config URL overwrite implies starting without media.');
|
||||
config.disableInitialGUM = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
@@ -405,13 +405,6 @@ function _translateLegacyConfig(oldValue: IConfig) {
|
||||
newValue.welcomePage.disabled = !oldValue.enableWelcomePage;
|
||||
}
|
||||
|
||||
newValue.prejoinConfig = oldValue.prejoinConfig || {};
|
||||
if (oldValue.hasOwnProperty('prejoinPageEnabled')
|
||||
&& !newValue.prejoinConfig.hasOwnProperty('enabled')
|
||||
) {
|
||||
newValue.prejoinConfig.enabled = oldValue.prejoinPageEnabled;
|
||||
}
|
||||
|
||||
newValue.disabledSounds = newValue.disabledSounds || [];
|
||||
|
||||
if (oldValue.disableJoinLeaveSounds) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-ignore
|
||||
import { safeJsonParse } from '@jitsi/js-utils/json';
|
||||
import { NativeModules } from 'react-native';
|
||||
// @ts-ignore
|
||||
import { Worklets } from 'react-native-worklets-core';
|
||||
|
||||
import { loadScript } from '../util/loadScript.native';
|
||||
|
||||
@@ -8,7 +9,12 @@ import logger from './logger';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
const { JavaScriptSandbox } = NativeModules;
|
||||
|
||||
/**
|
||||
* Worklet context usefull for running small tasks off the JS thread.
|
||||
*/
|
||||
export const workletContext = Worklets.createContext('ConfigParser');
|
||||
|
||||
|
||||
/**
|
||||
* Loads config.js from a specific remote server.
|
||||
@@ -18,14 +24,46 @@ const { JavaScriptSandbox } = NativeModules;
|
||||
*/
|
||||
export async function loadConfig(url: string): Promise<Object> {
|
||||
try {
|
||||
const configTxt = await loadScript(url, 10 * 1000 /* Timeout in ms */, true /* skipeval */);
|
||||
const configJson = await JavaScriptSandbox.evaluate(`${configTxt}\nJSON.stringify(config);`);
|
||||
const config = safeJsonParse(configJson);
|
||||
const configTxt = await loadScript(url, 10 * 1000, true);
|
||||
|
||||
if (typeof config !== 'object') {
|
||||
throw new Error('config is not an object');
|
||||
const parseConfigAsync = workletContext.createRunAsync(function parseConfig(configText: string): string {
|
||||
'worklet';
|
||||
try {
|
||||
// Used IIFE wrapper to capture config object from config.js
|
||||
const configObj = eval(
|
||||
'(function(){\n'
|
||||
+ configText
|
||||
+ '\n; return (typeof config !== "undefined" ? config : globalThis.config); })()'
|
||||
);
|
||||
|
||||
if (configObj == void 0) {
|
||||
return 'Worklet_Error: config is undefined after eval()';
|
||||
}
|
||||
|
||||
if (typeof configObj !== 'object') {
|
||||
return 'Worklet_Error: config is not an object';
|
||||
}
|
||||
|
||||
return JSON.stringify(configObj);
|
||||
} catch (err) {
|
||||
return 'Worklet_Error:' + ((err as Error)?.message ?? String(err));
|
||||
}
|
||||
});
|
||||
|
||||
const workletConfig = await parseConfigAsync(configTxt);
|
||||
|
||||
if (typeof workletConfig !== 'string') {
|
||||
throw new Error('Worklet error: workletConfig is not a string');
|
||||
}
|
||||
|
||||
if (workletConfig.startsWith('Worklet_Error:')) {
|
||||
const msg = workletConfig.slice('Worklet_Error:'.length);
|
||||
|
||||
throw new Error(`Worklet error: ${msg}`);
|
||||
}
|
||||
|
||||
const config = safeJsonParse(workletConfig);
|
||||
|
||||
logger.info(`Config loaded from ${url}`);
|
||||
|
||||
return config;
|
||||
|
||||
@@ -24,12 +24,14 @@ import { hideNotification, showNotification } from '../../notifications/actions'
|
||||
import {
|
||||
LOCAL_RECORDING_NOTIFICATION_ID,
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
RAISE_HAND_NOTIFICATION_ID
|
||||
RAISE_HAND_NOTIFICATION_ID,
|
||||
VISITOR_ASKED_TO_JOIN_NOTIFICATION_ID
|
||||
} from '../../notifications/constants';
|
||||
import { open as openParticipantsPane } from '../../participants-pane/actions';
|
||||
import { CALLING, INVITED } from '../../presence-status/constants';
|
||||
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
|
||||
import { CONFERENCE_JOINED, CONFERENCE_WILL_JOIN } from '../conference/actionTypes';
|
||||
import { forEachConference, getCurrentConference } from '../conference/functions';
|
||||
@@ -167,7 +169,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case LOCAL_PARTICIPANT_RAISE_HAND: {
|
||||
const { raisedHandTimestamp } = action;
|
||||
const localId = getLocalParticipant(store.getState())?.id;
|
||||
const localParticipant = getLocalParticipant(store.getState());
|
||||
const localId = localParticipant?.id;
|
||||
const _iAmVisitor = iAmVisitor(store.getState());
|
||||
const isHandRaised = hasRaisedHand(localParticipant);
|
||||
|
||||
store.dispatch(participantUpdated({
|
||||
// XXX Only the local participant is allowed to update without
|
||||
@@ -186,6 +191,18 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
raisedHandTimestamp
|
||||
}));
|
||||
|
||||
if (_iAmVisitor) {
|
||||
const notifyAction = isHandRaised
|
||||
? hideNotification(VISITOR_ASKED_TO_JOIN_NOTIFICATION_ID)
|
||||
: showNotification({
|
||||
titleKey: 'visitors.notification.requestToJoin',
|
||||
descriptionKey: 'visitors.notification.requestToJoinDescription',
|
||||
uid: VISITOR_ASKED_TO_JOIN_NOTIFICATION_ID
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY);
|
||||
|
||||
store.dispatch(notifyAction);
|
||||
}
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRaiseHandUpdated(localId, raisedHandTimestamp);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,15 @@ const DEFAULT_STATE: ISettingsState = {
|
||||
userSelectedMicDeviceLabel: undefined
|
||||
};
|
||||
|
||||
export interface IAudioSettings {
|
||||
autoGainControl?: boolean;
|
||||
channelCount?: 1 | 2;
|
||||
echoCancellation?: boolean;
|
||||
noiseSuppression?: boolean;
|
||||
}
|
||||
export interface ISettingsState {
|
||||
audioOutputDeviceId?: string;
|
||||
audioSettings?: IAudioSettings;
|
||||
audioSettingsVisible?: boolean;
|
||||
avatarURL?: string;
|
||||
cameraDeviceId?: string | boolean;
|
||||
@@ -66,6 +73,7 @@ export interface ISettingsState {
|
||||
localFlipX?: boolean;
|
||||
maxStageParticipants?: number;
|
||||
micDeviceId?: string | boolean;
|
||||
previewAudioTrack?: any | null;
|
||||
serverURL?: string;
|
||||
showSubtitlesOnStage?: boolean;
|
||||
soundsIncomingMessage?: boolean;
|
||||
|
||||
@@ -164,6 +164,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
|
||||
= createLocalTracksF(
|
||||
{
|
||||
cameraDeviceId: options.cameraDeviceId,
|
||||
constraints: options?.constraints,
|
||||
devices: [ device ],
|
||||
facingMode:
|
||||
options.facingMode || getCameraFacingMode(state),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen
|
||||
import { isAudioOnlySharing, isScreenVideoShared } from '../../screen-share/functions';
|
||||
import { toggleScreenshotCaptureSummary } from '../../screenshot-capture/actions';
|
||||
import { isScreenshotCaptureEnabled } from '../../screenshot-capture/functions';
|
||||
import { setAudioSettings } from '../../settings/actions.web';
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { notifyCameraError, notifyMicError } from '../devices/actions.web';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
} from '../media/constants';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
import { IAudioSettings } from '../settings/reducer';
|
||||
|
||||
import { addLocalTrack, replaceLocalTrack } from './actions.any';
|
||||
import AllowToggleCameraDialog from './components/web/AllowToggleCameraDialog';
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
getLocalVideoTrack,
|
||||
isToggleCameraEnabled
|
||||
} from './functions';
|
||||
import { applyAudioConstraints, getLocalJitsiAudioTrackSettings } from './functions.web';
|
||||
import logger from './logger';
|
||||
import { ICreateInitialTracksOptions, IInitialTracksErrors, IShareOptions, IToggleScreenSharingOptions } from './types';
|
||||
|
||||
@@ -329,7 +332,6 @@ export function setGUMPendingStateOnFailedTracks(tracks: Array<any>, dispatch: I
|
||||
export function createAndAddInitialAVTracks(devices: Array<MediaType>) {
|
||||
return async (dispatch: IStore['dispatch']) => {
|
||||
dispatch(gumPending(devices, IGUMPendingState.PENDING_UNMUTE));
|
||||
|
||||
const { tracks, errors } = await dispatch(createInitialAVTracks({ devices }));
|
||||
|
||||
setGUMPendingStateOnFailedTracks(tracks, dispatch);
|
||||
@@ -541,3 +543,21 @@ export function toggleCamera() {
|
||||
await dispatch(replaceLocalTrack(null, newVideoTrack));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the audio settings.
|
||||
*
|
||||
* @param {IAudioSettings} settings - The settings to apply.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleUpdateAudioSettings(settings: IAudioSettings) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
await applyAudioConstraints(state, settings);
|
||||
|
||||
const updatedSettings = getLocalJitsiAudioTrackSettings(state) as IAudioSettings;
|
||||
|
||||
dispatch(setAudioSettings(updatedSettings));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -201,6 +201,45 @@ export function getLocalJitsiAudioTrack(state: IReduxState) {
|
||||
return track?.jitsiTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audio settings from the local Jitsi audio track.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {IAudioSettings} The extracted audio settings.
|
||||
*/
|
||||
export function getLocalJitsiAudioTrackSettings(state: IReduxState) {
|
||||
const jitsiTrack = getLocalJitsiAudioTrack(state);
|
||||
|
||||
if (!jitsiTrack) {
|
||||
const config = state['features/base/config'];
|
||||
const disableAP = Boolean(config?.disableAP);
|
||||
const disableAGC = Boolean(config?.disableAGC);
|
||||
const disableAEC = Boolean(config?.disableAEC);
|
||||
const disableNS = Boolean(config?.disableNS);
|
||||
const stereo = Boolean(config?.audioQuality?.stereo);
|
||||
|
||||
return {
|
||||
autoGainControl: !disableAP && !disableAGC,
|
||||
channelCount: stereo ? 2 : 1,
|
||||
echoCancellation: !disableAP && !disableAEC,
|
||||
noiseSuppression: !disableAP && !disableNS
|
||||
};
|
||||
}
|
||||
|
||||
const hasAudioMixerEffect = Boolean(typeof jitsiTrack._streamEffect?.setMuted === 'function' && jitsiTrack._streamEffect?._originalTrack);
|
||||
|
||||
const track = hasAudioMixerEffect ? jitsiTrack._streamEffect._originalTrack : jitsiTrack.getTrack();
|
||||
|
||||
const { autoGainControl, channelCount, echoCancellation, noiseSuppression } = track.getSettings();
|
||||
|
||||
return {
|
||||
autoGainControl,
|
||||
channelCount,
|
||||
echoCancellation,
|
||||
noiseSuppression
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns track of specified media type for specified participant.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { IStateful } from '../app/types';
|
||||
import { isAdvancedAudioSettingsEnabled } from '../config/functions.any';
|
||||
import { isMobileBrowser } from '../environment/utils';
|
||||
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||
import { gumPending, setAudioMuted } from '../media/actions';
|
||||
@@ -11,9 +12,10 @@ import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId
|
||||
} from '../settings/functions.web';
|
||||
import { IAudioSettings } from '../settings/reducer';
|
||||
import { getJitsiMeetGlobalNSConnectionTimes } from '../util/helpers';
|
||||
|
||||
import { getCameraFacingMode } from './functions.any';
|
||||
import { getCameraFacingMode, getLocalJitsiAudioTrack, getLocalJitsiAudioTrackSettings } from './functions.any';
|
||||
import loadEffects from './loadEffects';
|
||||
import logger from './logger';
|
||||
import { ITrackOptions } from './types';
|
||||
@@ -62,7 +64,13 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore,
|
||||
desktopSharingFrameRate,
|
||||
resolution
|
||||
} = state['features/base/config'];
|
||||
const constraints = options.constraints ?? state['features/base/config'].constraints;
|
||||
|
||||
const constraints = options.constraints ?? state['features/base/config'].constraints ?? {};
|
||||
|
||||
if (isAdvancedAudioSettingsEnabled(state) && typeof APP !== 'undefined') {
|
||||
constraints.audio = state['features/settings'].audioSettings ?? getLocalJitsiAudioTrackSettings(state);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
loadEffects(store).then((effectsArray: Object[]) => {
|
||||
@@ -214,3 +222,32 @@ export function isToggleCameraEnabled(stateful: IStateful) {
|
||||
|
||||
return isMobileBrowser() && Number(videoInput?.length) > 1;
|
||||
}
|
||||
/**
|
||||
* Applies audio constraints to the local Jitsi audio track.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState} function.
|
||||
* @param {IAudioSettings} settings - The audio settings to apply.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function applyAudioConstraints(stateful: IStateful, settings: IAudioSettings) {
|
||||
const state = toState(stateful);
|
||||
const track = getLocalJitsiAudioTrack(state);
|
||||
|
||||
if (!track) {
|
||||
logger.debug('No local audio track found');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAdvancedAudioSettingsEnabled(state)) {
|
||||
logger.debug('Advanced audio settings disabled');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await track.applyConstraints(settings);
|
||||
} catch (error) {
|
||||
logger.error('Failed to apply audio constraints ', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { IStore } from '../../app/types';
|
||||
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import {
|
||||
SET_AUDIO_MUTED,
|
||||
SET_CAMERA_FACING_MODE,
|
||||
SET_SCREENSHARE_MUTED,
|
||||
SET_VIDEO_MUTED,
|
||||
@@ -46,15 +45,6 @@ import './subscriber';
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_AUDIO_MUTED:
|
||||
if (!action.muted
|
||||
&& isUserInteractionRequiredForUnmute(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_setMuted(store, action, MEDIA_TYPE.AUDIO);
|
||||
break;
|
||||
|
||||
case SET_CAMERA_FACING_MODE: {
|
||||
// XXX The camera facing mode of a MediaStreamTrack can be specified
|
||||
// only at initialization time and then it can only be toggled. So in
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_AUDIO_MUTED } from '../media/actionTypes';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
VIDEO_TYPE
|
||||
@@ -8,8 +10,11 @@ import {
|
||||
TRACK_UPDATED
|
||||
} from './actionTypes';
|
||||
import {
|
||||
toggleScreensharing
|
||||
createLocalTracksA,
|
||||
toggleScreensharing,
|
||||
trackMuteUnmuteFailed
|
||||
} from './actions.native';
|
||||
import { getLocalTrack, setTrackMuted } from './functions.any';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
@@ -23,11 +28,15 @@ import './middleware.any';
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_AUDIO_MUTED: {
|
||||
_setMuted(store, action);
|
||||
break;
|
||||
}
|
||||
case TRACK_UPDATED: {
|
||||
const { jitsiTrack, local } = action.track;
|
||||
|
||||
if (local && jitsiTrack.isMuted()
|
||||
&& jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
|
||||
&& jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) {
|
||||
store.dispatch(toggleScreensharing(false));
|
||||
}
|
||||
break;
|
||||
@@ -36,3 +45,32 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutes or unmutes a local track with a specific media type.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is dispatched.
|
||||
* @param {Action} action - The redux action dispatched in the specified store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setMuted(store: IStore, { ensureTrack, muted }: {
|
||||
ensureTrack: boolean; muted: boolean; }) {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const localTrack = getLocalTrack(state['features/base/tracks'], MEDIA_TYPE.AUDIO, /* includePending */ true);
|
||||
|
||||
if (localTrack) {
|
||||
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
|
||||
// completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
|
||||
// created.
|
||||
const { jitsiTrack } = localTrack;
|
||||
|
||||
if (jitsiTrack) {
|
||||
setTrackMuted(jitsiTrack, muted, state, dispatch)
|
||||
.catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
|
||||
}
|
||||
} else if (!muted && ensureTrack) {
|
||||
dispatch(createLocalTracksA({ devices: [ MEDIA_TYPE.AUDIO ] }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ import { AnyAction } from 'redux';
|
||||
import { IStore } from '../../app/types';
|
||||
import { hideNotification } from '../../notifications/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { setAudioSettings } from '../../settings/actions.web';
|
||||
import { getAvailableDevices } from '../devices/actions.web';
|
||||
import { setScreenshareMuted } from '../media/actions';
|
||||
import { SET_AUDIO_MUTED } from '../media/actionTypes';
|
||||
import { gumPending, setScreenshareMuted } from '../media/actions';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
VIDEO_TYPE
|
||||
} from '../media/constants';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import {
|
||||
@@ -20,14 +23,19 @@ import {
|
||||
TRACK_UPDATED
|
||||
} from './actionTypes';
|
||||
import {
|
||||
createLocalTracksA,
|
||||
showNoDataFromSourceVideoError,
|
||||
toggleScreensharing,
|
||||
trackMuteUnmuteFailed,
|
||||
trackNoDataFromSourceNotificationInfoChanged
|
||||
} from './actions.web';
|
||||
import {
|
||||
getTrackByJitsiTrack, logTracksForParticipant
|
||||
getLocalJitsiAudioTrackSettings,
|
||||
getLocalTrack,
|
||||
getTrackByJitsiTrack, isUserInteractionRequiredForUnmute, logTracksForParticipant,
|
||||
setTrackMuted
|
||||
} from './functions.web';
|
||||
import { ITrack } from './types';
|
||||
import { ITrack, ITrackOptions } from './types';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
@@ -138,7 +146,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return result;
|
||||
}
|
||||
case SET_AUDIO_MUTED: {
|
||||
if (!action.muted
|
||||
&& isUserInteractionRequiredForUnmute(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_setMuted(store, action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
@@ -207,3 +223,47 @@ function _removeNoDataFromSourceNotification({ getState, dispatch }: IStore, tra
|
||||
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or unmutes a local track with a specific media type.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified action is
|
||||
* dispatched.
|
||||
* @param {Action} action - The redux action dispatched in the specified store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setMuted(store: IStore, { ensureTrack, muted }: {
|
||||
ensureTrack: boolean; muted: boolean; }) {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const localTrack = getLocalTrack(state['features/base/tracks'], MEDIA_TYPE.AUDIO, /* includePending */ true);
|
||||
|
||||
if (localTrack) {
|
||||
// The `jitsiTrack` property will have a value only for a localTrack for which `getUserMedia` has already
|
||||
// completed. If there's no `jitsiTrack`, then the `muted` state will be applied once the `jitsiTrack` is
|
||||
// created.
|
||||
const { jitsiTrack } = localTrack;
|
||||
|
||||
if (jitsiTrack) {
|
||||
setTrackMuted(jitsiTrack, muted, state, dispatch)
|
||||
.catch(() => {
|
||||
dispatch(trackMuteUnmuteFailed(localTrack, muted));
|
||||
});
|
||||
}
|
||||
} else if (!muted && ensureTrack) {
|
||||
// TODO(saghul): reconcile these 2 types.
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.PENDING_UNMUTE));
|
||||
|
||||
const createTrackOptions: ITrackOptions = {
|
||||
devices: [ MEDIA_TYPE.AUDIO ],
|
||||
};
|
||||
|
||||
dispatch(createLocalTracksA(createTrackOptions)).then(() => {
|
||||
dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.NONE));
|
||||
const updatedSettings = getLocalJitsiAudioTrackSettings(getState());
|
||||
|
||||
dispatch(setAudioSettings(updatedSettings));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MediaType } from '../media/constants';
|
||||
import { IAudioSettings } from '../settings/reducer';
|
||||
|
||||
export interface ITrackOptions {
|
||||
cameraDeviceId?: string | null;
|
||||
constraints?: {
|
||||
audio?: IAudioSettings;
|
||||
video?: {
|
||||
height?: {
|
||||
ideal?: number;
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ITabProps {
|
||||
icon?: Function;
|
||||
id: string;
|
||||
label?: string;
|
||||
title?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -126,26 +127,34 @@ const Tabs = ({
|
||||
aria-label = { accessibilityLabel }
|
||||
className = { cx(classes.container, className) }
|
||||
role = 'tablist'>
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
aria-controls = { tab.controlsId }
|
||||
aria-label = { tab.accessibilityLabel }
|
||||
aria-selected = { selected === tab.id }
|
||||
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
|
||||
disabled = { tab.disabled }
|
||||
id = { tab.id }
|
||||
key = { tab.id }
|
||||
onClick = { onClick(tab.id) }
|
||||
onKeyDown = { onKeyDown(index) }
|
||||
role = 'tab'
|
||||
tabIndex = { selected === tab.id ? undefined : -1 }>
|
||||
{tab.icon && <Icon
|
||||
className = { classes.icon }
|
||||
src = { tab.icon } />}
|
||||
{tab.label}
|
||||
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
|
||||
</button>
|
||||
))}
|
||||
{
|
||||
tabs.map((tab, index) => (
|
||||
<button
|
||||
aria-controls = { tab.controlsId }
|
||||
aria-label = { tab.accessibilityLabel }
|
||||
aria-selected = { selected === tab.id }
|
||||
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
|
||||
disabled = { tab.disabled }
|
||||
id = { tab.id }
|
||||
key = { tab.id }
|
||||
onClick = { onClick(tab.id) }
|
||||
onKeyDown = { onKeyDown(index) }
|
||||
role = 'tab'
|
||||
tabIndex = { selected === tab.id ? undefined : -1 }
|
||||
title = { tab.title }>
|
||||
{
|
||||
tab.icon && <Icon
|
||||
className = { classes.icon }
|
||||
src = { tab.icon } />
|
||||
}
|
||||
{ tab.label }
|
||||
{
|
||||
tab.countBadge && <span className = { classes.badge }>
|
||||
{ tab.countBadge }
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -170,7 +170,7 @@ export const microsoftCalendarApi = {
|
||||
|
||||
popupAuthWindow = window.open(
|
||||
authUrl,
|
||||
'Auth M$',
|
||||
`Auth M$-${Date.now()}`,
|
||||
`width=${w}, height=${h}, top=${
|
||||
(screen.height / 2) - (h / 2)}, left=${
|
||||
(screen.width / 2) - (w / 2)}`);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { navigate }
|
||||
from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { navigate } from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
@@ -18,7 +17,7 @@ export * from './actions.any';
|
||||
* type: OPEN_CHAT
|
||||
* }}
|
||||
*/
|
||||
export function openChat(participant: IParticipant | undefined | Object, disablePolls?: boolean) {
|
||||
export function openChat(participant?: IParticipant | undefined | Object, disablePolls?: boolean) {
|
||||
if (disablePolls) {
|
||||
navigate(screen.conference.chat);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants/functions';
|
||||
import { getVisitorDisplayName } from '../../visitors/functions';
|
||||
import { getDisplayName } from '../../visitors/functions';
|
||||
import { setLobbyChatActiveState, setPrivateMessageRecipient } from '../actions.any';
|
||||
import { isVisitorChatParticipant } from '../functions';
|
||||
|
||||
@@ -84,7 +84,7 @@ export function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
|
||||
if (privateMessageRecipient) {
|
||||
_privateMessageRecipient = _isVisitor
|
||||
? getVisitorDisplayName(state, privateMessageRecipient.name)
|
||||
? getDisplayName(state, privateMessageRecipient.name)
|
||||
: getParticipantDisplayName(state, privateMessageRecipient.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { isGifEnabled, isGifMessage } from '../../../gifs/functions.native';
|
||||
import { CHAR_LIMIT, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
|
||||
import {
|
||||
getCanReplyToMessage,
|
||||
getDisplayNameSuffix,
|
||||
getFormattedTimestamp,
|
||||
getMessageText,
|
||||
getPrivateNoticeMessage,
|
||||
@@ -122,17 +123,17 @@ class ChatMessage extends Component<IChatMessageProps> {
|
||||
* @returns {React.ReactElement<*> | null}
|
||||
*/
|
||||
_renderDisplayName() {
|
||||
const { message, showDisplayName, t } = this.props;
|
||||
const { message, showDisplayName } = this.props;
|
||||
|
||||
if (!showDisplayName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { displayName, isFromVisitor } = message;
|
||||
const { displayName } = message;
|
||||
|
||||
return (
|
||||
<Text style = { styles.senderDisplayName }>
|
||||
{ `${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}` }
|
||||
{ `${displayName}${getDisplayNameSuffix(message)}` }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -512,7 +512,8 @@ const Chat = ({
|
||||
_focusedTab !== ChatTabs.CHAT && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
|
||||
id: ChatTabs.CHAT,
|
||||
controlsId: `${ChatTabs.CHAT}-panel`,
|
||||
icon: IconMessage
|
||||
icon: IconMessage,
|
||||
title: t('chat.tabs.chat')
|
||||
}
|
||||
];
|
||||
|
||||
@@ -522,7 +523,8 @@ const Chat = ({
|
||||
countBadge: _focusedTab !== ChatTabs.POLLS && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
|
||||
id: ChatTabs.POLLS,
|
||||
controlsId: `${ChatTabs.POLLS}-panel`,
|
||||
icon: IconInfo
|
||||
icon: IconInfo,
|
||||
title: t('chat.tabs.polls')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -532,7 +534,8 @@ const Chat = ({
|
||||
countBadge: undefined,
|
||||
id: ChatTabs.CLOSED_CAPTIONS,
|
||||
controlsId: `${ChatTabs.CLOSED_CAPTIONS}-panel`,
|
||||
icon: IconSubtitles
|
||||
icon: IconSubtitles,
|
||||
title: t('chat.tabs.closedCaptions')
|
||||
});
|
||||
}
|
||||
|
||||
@@ -542,7 +545,8 @@ const Chat = ({
|
||||
countBadge: undefined,
|
||||
id: ChatTabs.FILE_SHARING,
|
||||
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
|
||||
icon: IconShareDoc
|
||||
icon: IconShareDoc,
|
||||
title: t('chat.tabs.fileSharing')
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } f
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import Message from '../../../base/react/components/web/Message';
|
||||
import { MESSAGE_TYPE_LOCAL } from '../../constants';
|
||||
import { getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
|
||||
import { getDisplayNameSuffix, getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
|
||||
import { IChatMessageProps } from '../../types';
|
||||
|
||||
import MessageMenu from './MessageMenu';
|
||||
@@ -226,13 +226,13 @@ const ChatMessage = ({
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
function _renderDisplayName() {
|
||||
const { displayName, isFromVisitor = false } = message;
|
||||
const { displayName } = message;
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden = { true }
|
||||
className = { cx('display-name', classes.displayName) }>
|
||||
{`${displayName}${isFromVisitor ? ` ${t('visitors.chatIndicator')}` : ''}`}
|
||||
{`${displayName}${getDisplayNameSuffix(message)}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,3 +265,21 @@ export function isVisitorChatParticipant(
|
||||
): participant is IVisitorChatParticipant {
|
||||
return Boolean(participant && 'isVisitor' in participant && participant.isVisitor === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a suffix to be appended to the display name based on the message origin.
|
||||
*
|
||||
* @param {IMessage} message - The message.
|
||||
* @returns {string} The suffix, if any.
|
||||
*/
|
||||
export function getDisplayNameSuffix(message: IMessage): string {
|
||||
let suffix = '';
|
||||
|
||||
if (message.isFromVisitor) {
|
||||
suffix = ` ${i18next.t('visitors.chatIndicator')}`;
|
||||
} else if (message.isFromGuest) {
|
||||
suffix = ` ${i18next.t('chat.guestsChatIndicator')}`;
|
||||
}
|
||||
|
||||
return suffix;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { pushReactions } from '../reactions/actions.any';
|
||||
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
|
||||
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
import { getVisitorDisplayName } from '../visitors/functions';
|
||||
import { getDisplayName } from '../visitors/functions';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
@@ -51,8 +51,9 @@ import {
|
||||
clearMessages,
|
||||
closeChat,
|
||||
notifyPrivateRecipientsChanged,
|
||||
openChat,
|
||||
setPrivateMessageRecipient
|
||||
} from './actions.any';
|
||||
} from './actions';
|
||||
import { ChatPrivacyDialog } from './components';
|
||||
import {
|
||||
ChatTabs,
|
||||
@@ -63,7 +64,12 @@ import {
|
||||
MESSAGE_TYPE_REMOTE,
|
||||
MESSAGE_TYPE_SYSTEM
|
||||
} from './constants';
|
||||
import { getUnreadCount, isSendGroupChatDisabled, isVisitorChatParticipant } from './functions';
|
||||
import {
|
||||
getDisplayNameSuffix,
|
||||
getUnreadCount,
|
||||
isSendGroupChatDisabled,
|
||||
isVisitorChatParticipant
|
||||
} from './functions';
|
||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||
import './subscriber';
|
||||
|
||||
@@ -366,7 +372,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
/* eslint-disable max-params */
|
||||
(participantId: string, message: string, timestamp: number,
|
||||
displayName: string, isFromVisitor: boolean, messageId: string) => {
|
||||
displayName: string, isFromVisitor: boolean, messageId: string, source: string) => {
|
||||
/* eslint-enable max-params */
|
||||
_onConferenceMessageReceived(store, {
|
||||
// in case of messages coming from visitors we can have unknown id
|
||||
@@ -376,7 +382,9 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
displayName,
|
||||
isFromVisitor,
|
||||
messageId,
|
||||
privateMessage: false });
|
||||
source,
|
||||
privateMessage: false
|
||||
});
|
||||
|
||||
if (isSendGroupChatDisabled(store.getState()) && participantId) {
|
||||
const participant = getParticipantById(store, participantId);
|
||||
@@ -426,9 +434,9 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onConferenceMessageReceived(store: IStore,
|
||||
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp, source }: {
|
||||
displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string;
|
||||
participantId: string; privateMessage: boolean; timestamp: number; }
|
||||
participantId: string; privateMessage: boolean; source?: string; timestamp: number; }
|
||||
) {
|
||||
|
||||
const isGif = isGifEnabled(store.getState()) && isGifMessage(message);
|
||||
@@ -447,7 +455,8 @@ function _onConferenceMessageReceived(store: IStore,
|
||||
privateMessage,
|
||||
lobbyChat: false,
|
||||
timestamp,
|
||||
messageId
|
||||
messageId,
|
||||
source
|
||||
}, true, isGif);
|
||||
}
|
||||
|
||||
@@ -556,9 +565,9 @@ function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, timestamp }: {
|
||||
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, source, timestamp }: {
|
||||
displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string;
|
||||
messageId?: string; participantId: string; privateMessage: boolean; timestamp: number; },
|
||||
messageId?: string; participantId: string; privateMessage: boolean; source?: string; timestamp: number; },
|
||||
shouldPlaySound = true,
|
||||
isReaction = false
|
||||
) {
|
||||
@@ -571,35 +580,33 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
}
|
||||
|
||||
// Provide a default for the case when a message is being
|
||||
// backfilled for a participant that has left the conference.
|
||||
const participant = getParticipantById(state, participantId) || { local: undefined };
|
||||
const participant = getParticipantById(state, participantId);
|
||||
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
let _displayName, displayNameToShow;
|
||||
let _displayName;
|
||||
|
||||
if (lobbyChat) {
|
||||
displayNameToShow = _displayName = getLobbyChatDisplayName(state, participantId);
|
||||
_displayName = getLobbyChatDisplayName(state, participantId);
|
||||
} else if (isFromVisitor) {
|
||||
_displayName = getVisitorDisplayName(state, displayName);
|
||||
displayNameToShow = `${_displayName} ${i18next.t('visitors.chatIndicator')}`;
|
||||
_displayName = getDisplayName(state, displayName);
|
||||
} else if (!participant) {
|
||||
_displayName = getDisplayName(state, displayName);
|
||||
} else {
|
||||
displayNameToShow = _displayName = getParticipantDisplayName(state, participantId);
|
||||
_displayName = getParticipantDisplayName(state, participantId);
|
||||
}
|
||||
|
||||
const hasRead = participant.local || isChatOpen;
|
||||
const hasRead = participant?.local || isChatOpen;
|
||||
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
|
||||
const millisecondsTimestamp = timestampToDate.getTime();
|
||||
|
||||
// skip message notifications on join (the messages having timestamp - coming from the history)
|
||||
const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
|
||||
&& !hasRead && !isReaction && (!timestamp || lobbyChat);
|
||||
|
||||
dispatch(addMessage({
|
||||
const newMessage = {
|
||||
displayName: _displayName,
|
||||
hasRead,
|
||||
participantId,
|
||||
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
|
||||
messageType: participant?.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
|
||||
message,
|
||||
privateMessage,
|
||||
lobbyChat,
|
||||
@@ -607,13 +614,29 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
timestamp: millisecondsTimestamp,
|
||||
messageId,
|
||||
isReaction,
|
||||
isFromVisitor
|
||||
}));
|
||||
isFromVisitor,
|
||||
isFromGuest: source === 'guest'
|
||||
};
|
||||
|
||||
dispatch(addMessage(newMessage));
|
||||
|
||||
let notificationDisplayName = _displayName;
|
||||
|
||||
// source can be 'token' or 'guest'. When it is 'guest', we append a guest indicator
|
||||
// to the display name to notify users that this message can be anything, while the token one is coming
|
||||
// from a trusted source (the jwt token, the name can be locked from the backend if the
|
||||
// token has the feature 'name-readonly')
|
||||
if (isFromVisitor || (!participant && source === 'guest')) {
|
||||
// @ts-ignore
|
||||
notificationDisplayName = `${_displayName} ${getDisplayNameSuffix(newMessage)}`;
|
||||
}
|
||||
|
||||
if (shouldShowNotification) {
|
||||
dispatch(showMessageNotification({
|
||||
title: displayNameToShow,
|
||||
description: message
|
||||
title: notificationDisplayName,
|
||||
description: message,
|
||||
customActionNameKey: [ 'notify.openChat' ],
|
||||
customActionHandler: [ () => dispatch(openChat()) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
|
||||
@@ -623,7 +646,7 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
APP.API.notifyReceivedChatMessage({
|
||||
body: message,
|
||||
from: participantId,
|
||||
nick: displayNameToShow,
|
||||
nick: notificationDisplayName,
|
||||
privateMessage,
|
||||
ts: timestamp
|
||||
});
|
||||
@@ -665,7 +688,7 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: I
|
||||
|
||||
const recipientName
|
||||
= recipient.isVisitor
|
||||
? getVisitorDisplayName(state, recipient.name)
|
||||
? getDisplayName(state, recipient.name)
|
||||
: (isLobbyPrivateMessage
|
||||
? lobbyMessageRecipient?.name
|
||||
: getParticipantDisplayName(getState, recipient?.id));
|
||||
|
||||
@@ -68,6 +68,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
const newMessage: IMessage = {
|
||||
displayName: action.displayName,
|
||||
error: action.error,
|
||||
isFromGuest: Boolean(action.isFromGuest),
|
||||
isFromVisitor: Boolean(action.isFromVisitor),
|
||||
participantId: action.participantId,
|
||||
isReaction: action.isReaction,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
|
||||
export interface IMessage {
|
||||
displayName: string;
|
||||
error?: Object;
|
||||
isFromGuest?: boolean;
|
||||
isFromVisitor?: boolean;
|
||||
isReaction: boolean;
|
||||
lobbyChat: boolean;
|
||||
|
||||
@@ -579,7 +579,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { startCarMode } = state['features/base/settings'];
|
||||
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
|
||||
const brandingStyles = backgroundColor ? {
|
||||
backgroundColor
|
||||
background: backgroundColor
|
||||
} : undefined;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { createDeviceChangedEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
@@ -7,8 +9,12 @@ import {
|
||||
} from '../base/devices/actions';
|
||||
import { getDeviceLabelById, setAudioOutputDeviceId } from '../base/devices/functions';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { toggleUpdateAudioSettings } from '../base/tracks/actions.web';
|
||||
import { getLocalJitsiAudioTrack } from '../base/tracks/functions.any';
|
||||
import { toggleNoiseSuppression } from '../noise-suppression/actions';
|
||||
import { setScreenshareFramerate } from '../screen-share/actions';
|
||||
import { setScreenshareFramerate } from '../screen-share/actions.web';
|
||||
import { setAudioSettings } from '../settings/actions.web';
|
||||
import { disposePreviewAudioTrack } from '../settings/functions.web';
|
||||
|
||||
import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from './functions';
|
||||
import logger from './logger';
|
||||
@@ -22,16 +28,21 @@ import logger from './logger';
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getAudioDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
|
||||
const isSelectedAudioInputIdChanged = newState.selectedAudioInputId
|
||||
&& newState.selectedAudioInputId !== currentState.selectedAudioInputId;
|
||||
|
||||
if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
|
||||
if (isSelectedAudioInputIdChanged) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: newState.selectedAudioInputId,
|
||||
userSelectedMicDeviceLabel:
|
||||
getDeviceLabelById(getState(), newState.selectedAudioInputId, 'audioInput')
|
||||
}));
|
||||
|
||||
await disposePreviewAudioTrack(getState());
|
||||
|
||||
dispatch(setAudioSettings(newState.audioSettings));
|
||||
dispatch(setAudioInputDevice(newState.selectedAudioInputId));
|
||||
}
|
||||
|
||||
@@ -58,6 +69,21 @@ export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcom
|
||||
if (newState.noiseSuppressionEnabled !== currentState.noiseSuppressionEnabled) {
|
||||
dispatch(toggleNoiseSuppression());
|
||||
}
|
||||
|
||||
if (!isEqual(newState.audioSettings, currentState.audioSettings) && !isSelectedAudioInputIdChanged && !newState.noiseSuppressionEnabled) {
|
||||
const state = getState();
|
||||
const jitsiTrack = getLocalJitsiAudioTrack(state);
|
||||
|
||||
if (!jitsiTrack) {
|
||||
logger.debug('No local audio track found');
|
||||
dispatch(setAudioSettings(newState.audioSettings));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await disposePreviewAudioTrack(getState());
|
||||
dispatch(toggleUpdateAudioSettings(newState.audioSettings));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isAdvancedAudioSettingsEnabled } from '../../base/config/functions.any';
|
||||
import { getAvailableDevices } from '../../base/devices/actions.web';
|
||||
import AbstractDialogTab, {
|
||||
type IProps as AbstractDialogTabProps
|
||||
} from '../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
|
||||
import { IAudioSettings } from '../../base/settings/reducer';
|
||||
import Checkbox from '../../base/ui/components/web/Checkbox';
|
||||
import { setPreviewAudioTrack } from '../../settings/actions.web';
|
||||
import { disposeTrack } from '../../settings/functions.web';
|
||||
import { iAmVisitor as iAmVisitorCheck } from '../../visitors/functions';
|
||||
import logger from '../logger';
|
||||
|
||||
import AudioInputPreview from './AudioInputPreview';
|
||||
import AudioOutputPreview from './AudioOutputPreview';
|
||||
import AudioInputPreview from './AudioInputPreview.web';
|
||||
import AudioOutputPreview from './AudioOutputPreview.web';
|
||||
import DeviceHidContainer from './DeviceHidContainer.web';
|
||||
import DeviceSelector from './DeviceSelector.web';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioDevicesSelection}.
|
||||
*/
|
||||
interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* The audio local track settings.
|
||||
*/
|
||||
audioSettings?: IAudioSettings;
|
||||
|
||||
/**
|
||||
* All known audio and video devices split by type. This prop comes from
|
||||
* the app state.
|
||||
@@ -98,11 +109,21 @@ interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
*/
|
||||
iAmVisitor: boolean;
|
||||
|
||||
/**
|
||||
* Whether the advanced audio settings are enabled from config.
|
||||
*/
|
||||
isAdvancedAudioSettingsConfigEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether noise suppression is on or not.
|
||||
*/
|
||||
noiseSuppressionEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The audio track that is used for previewing the audio input.
|
||||
*/
|
||||
previewAudioTrack: any | null;
|
||||
|
||||
/**
|
||||
* The id of the audio input device to preview.
|
||||
*/
|
||||
@@ -114,19 +135,20 @@ interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
selectedAudioOutputId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AudioDevicesSelection}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The JitsiTrack to use for previewing audio input.
|
||||
*/
|
||||
previewAudioTrack?: any | null;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
checkbox: {
|
||||
width: 'max-content'
|
||||
},
|
||||
|
||||
checkboxGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto auto',
|
||||
gap: theme.spacing(3),
|
||||
margin: `${theme.spacing(3)} 0`
|
||||
},
|
||||
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
@@ -134,32 +156,33 @@ const styles = (theme: Theme) => {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
hidden: {
|
||||
display: 'none'
|
||||
},
|
||||
|
||||
inputContainer: {
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
outputContainer: {
|
||||
margin: `${theme.spacing(5)} 0`,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end'
|
||||
alignItems: 'flex-end',
|
||||
margin: `${theme.spacing(5)} 0`
|
||||
},
|
||||
|
||||
outputButton: {
|
||||
marginLeft: theme.spacing(3)
|
||||
},
|
||||
|
||||
noiseSuppressionContainer: {
|
||||
marginBottom: theme.spacing(5)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* React {@code Component} for previewing audio and video input/output devices.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
|
||||
|
||||
/**
|
||||
* Whether current component is mounted or not.
|
||||
@@ -181,9 +204,6 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewAudioTrack: null
|
||||
};
|
||||
this._unMounted = true;
|
||||
}
|
||||
|
||||
@@ -224,9 +244,29 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
this._unMounted = true;
|
||||
this._disposeAudioInputPreview();
|
||||
disposeTrack(this.props.previewAudioTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the audio settings based on the input change event and updates the state.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleAudioSettings = ({ target: { checked, name } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { audioSettings } = this.props;
|
||||
|
||||
const newValue = name === 'channelCount' ? (checked ? 2 : 1) : checked;
|
||||
|
||||
super._onChange({
|
||||
audioSettings: {
|
||||
...audioSettings,
|
||||
[name]: newValue
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -234,11 +274,13 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
audioSettings,
|
||||
hasAudioPermission,
|
||||
hideAudioInputPreview,
|
||||
hideAudioOutputPreview,
|
||||
hideDeviceHIDContainer,
|
||||
hideNoiseSuppression,
|
||||
isAdvancedAudioSettingsConfigEnabled,
|
||||
iAmVisitor,
|
||||
noiseSuppressionEnabled,
|
||||
selectedAudioOutputId,
|
||||
@@ -248,6 +290,13 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
const isAudioSettingsEnabled = Boolean(audioSettings?.autoGainControl || audioSettings?.channelCount === 2 || audioSettings?.echoCancellation || audioSettings?.noiseSuppression);
|
||||
|
||||
const shouldDisplayNoiseSuppressionCheckbox = !hideNoiseSuppression && !iAmVisitor;
|
||||
const shouldDisplayAdvancedAudioSettingsCheckboxes = !hideNoiseSuppression && !iAmVisitor && isAdvancedAudioSettingsConfigEnabled && Boolean(audioSettings);
|
||||
|
||||
const shouldDisabledNoiseSupressionCheckbox = shouldDisplayAdvancedAudioSettingsCheckboxes && (isAudioSettingsEnabled && !noiseSuppressionEnabled);
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{!iAmVisitor && <div
|
||||
@@ -255,9 +304,60 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
className = { classes.inputContainer }>
|
||||
{this._renderSelector(audioInput)}
|
||||
</div>}
|
||||
|
||||
{!hideAudioInputPreview && hasAudioPermission && !iAmVisitor
|
||||
&& <AudioInputPreview
|
||||
track = { this.state.previewAudioTrack } />}
|
||||
&& <AudioInputPreview
|
||||
track = { this.props.previewAudioTrack } />}
|
||||
|
||||
<fieldset className = { classes.checkboxGrid }>
|
||||
<Checkbox
|
||||
checked = { Boolean(audioSettings?.echoCancellation) }
|
||||
className = { clsx(classes.checkbox, {
|
||||
[classes.hidden]: !shouldDisplayAdvancedAudioSettingsCheckboxes,
|
||||
}) }
|
||||
disabled = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.advancedAudioSettings.aec.label') }
|
||||
name = { 'echoCancellation' }
|
||||
onChange = { this._onToggleAudioSettings } />
|
||||
<Checkbox
|
||||
checked = { Boolean(audioSettings?.channelCount === 2) }
|
||||
className = { clsx(classes.checkbox, {
|
||||
[classes.hidden]: !shouldDisplayAdvancedAudioSettingsCheckboxes,
|
||||
}) }
|
||||
disabled = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.advancedAudioSettings.stereo.label') }
|
||||
name = { 'channelCount' }
|
||||
onChange = { this._onToggleAudioSettings } />
|
||||
<Checkbox
|
||||
checked = { Boolean(audioSettings?.autoGainControl) }
|
||||
className = { clsx(classes.checkbox, {
|
||||
[classes.hidden]: !shouldDisplayAdvancedAudioSettingsCheckboxes,
|
||||
}) }
|
||||
disabled = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.advancedAudioSettings.agc.label') }
|
||||
name = { 'autoGainControl' }
|
||||
onChange = { this._onToggleAudioSettings } />
|
||||
<Checkbox
|
||||
checked = { Boolean(audioSettings?.noiseSuppression) }
|
||||
className = { clsx(classes.checkbox, {
|
||||
[classes.hidden]: !shouldDisplayAdvancedAudioSettingsCheckboxes,
|
||||
}) }
|
||||
disabled = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.advancedAudioSettings.ns.label') }
|
||||
name = { 'noiseSuppression' }
|
||||
onChange = { this._onToggleAudioSettings } />
|
||||
<Checkbox
|
||||
checked = { noiseSuppressionEnabled }
|
||||
className = { clsx(classes.checkbox, {
|
||||
[classes.hidden]: !shouldDisplayNoiseSuppressionCheckbox,
|
||||
}) }
|
||||
disabled = { shouldDisabledNoiseSupressionCheckbox }
|
||||
label = { t('toolbar.enableNoiseSuppression') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { () => super._onChange({
|
||||
noiseSuppressionEnabled: !noiseSuppressionEnabled
|
||||
}) } />
|
||||
</fieldset>
|
||||
<div
|
||||
aria-live = 'polite'
|
||||
className = { classes.outputContainer }>
|
||||
@@ -267,17 +367,7 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
className = { classes.outputButton }
|
||||
deviceId = { selectedAudioOutputId } />}
|
||||
</div>
|
||||
{!hideNoiseSuppression && !iAmVisitor && (
|
||||
<div className = { classes.noiseSuppressionContainer }>
|
||||
<Checkbox
|
||||
checked = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.enableNoiseSuppression') }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = { () => super._onChange({
|
||||
noiseSuppressionEnabled: !noiseSuppressionEnabled
|
||||
}) } />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hideDeviceHIDContainer && !iAmVisitor
|
||||
&& <DeviceHidContainer />}
|
||||
</div>
|
||||
@@ -292,13 +382,13 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_createAudioInputTrack(deviceId: string) {
|
||||
const { hideAudioInputPreview } = this.props;
|
||||
const { hideAudioInputPreview, previewAudioTrack } = this.props;
|
||||
|
||||
if (hideAudioInputPreview) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._disposeAudioInputPreview()
|
||||
return disposeTrack(previewAudioTrack)
|
||||
.then(() => createLocalTrack('audio', deviceId, 5000))
|
||||
.then(jitsiLocalTrack => {
|
||||
if (this._unMounted) {
|
||||
@@ -306,29 +396,14 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
|
||||
return;
|
||||
}
|
||||
this.props.dispatch(setPreviewAudioTrack(jitsiLocalTrack));
|
||||
|
||||
this.setState({
|
||||
previewAudioTrack: jitsiLocalTrack
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
previewAudioTrack: null
|
||||
});
|
||||
this.props.dispatch(setPreviewAudioTrack(null));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for disposing the current audio input preview.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_disposeAudioInputPreview(): Promise<any> {
|
||||
return this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DeviceSelector instance based on the passed in configuration.
|
||||
*
|
||||
@@ -362,8 +437,8 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
id: 'audioInput',
|
||||
label: 'settings.selectMic',
|
||||
onSelect: (selectedAudioInputId: string) => super._onChange({ selectedAudioInputId }),
|
||||
selectedDeviceId: this.state.previewAudioTrack
|
||||
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
||||
selectedDeviceId: this.props.previewAudioTrack
|
||||
? this.props.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
|
||||
};
|
||||
let audioOutput;
|
||||
|
||||
@@ -389,7 +464,9 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, IState> {
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
availableDevices: state['features/base/devices'].availableDevices ?? {},
|
||||
iAmVisitor: iAmVisitorCheck(state)
|
||||
isAdvancedAudioSettingsConfigEnabled: isAdvancedAudioSettingsEnabled(state),
|
||||
iAmVisitor: iAmVisitorCheck(state),
|
||||
previewAudioTrack: state['features/settings'].previewAudioTrack
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getUserSelectedMicDeviceId,
|
||||
getUserSelectedOutputDeviceId
|
||||
} from '../base/settings/functions.web';
|
||||
import { getLocalJitsiAudioTrackSettings } from '../base/tracks/functions.any';
|
||||
import { isNoiseSuppressionEnabled } from '../noise-suppression/functions';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from '../settings/constants';
|
||||
@@ -50,6 +51,7 @@ export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDispla
|
||||
const deviceHidSupported = isDeviceHidSupported() && getWebHIDFeatureConfig(state);
|
||||
const noiseSuppressionEnabled = isNoiseSuppressionEnabled(state);
|
||||
const hideNoiseSuppression = isPrejoinPageVisible(state) || isDisplayedOnWelcomePage;
|
||||
const audioSettings = state['features/settings'].audioSettings ?? getLocalJitsiAudioTrackSettings(state);
|
||||
|
||||
// When the previews are disabled we don't need multiple audio input support in order to change the mic. This is the
|
||||
// case for Safari on iOS.
|
||||
@@ -71,6 +73,7 @@ export function getAudioDeviceSelectionDialogProps(stateful: IStateful, isDispla
|
||||
// we fill the device selection dialog with the devices that are currently
|
||||
// used or if none are currently used with what we have in settings(user selected)
|
||||
return {
|
||||
audioSettings,
|
||||
disableAudioInputChange,
|
||||
disableDeviceChange: !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
hasAudioPermission: permissions.audio,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { cleanSvg } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
@@ -82,7 +83,7 @@ export const fetchCustomIcons = async (customIcons: Record<string, string>) => {
|
||||
if (response.ok) {
|
||||
const svgXml = await response.text();
|
||||
|
||||
localCustomIcons[key] = svgXml;
|
||||
localCustomIcons[key] = cleanSvg(svgXml);
|
||||
} else {
|
||||
logger.error(`Failed to fetch ${url}. Status: ${response.status}`);
|
||||
}
|
||||
|
||||
9
react/features/dynamic-branding/functions.native.ts
Normal file
9
react/features/dynamic-branding/functions.native.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Sanitizes the given SVG by removing dangerous elements.
|
||||
*
|
||||
* @param {string} svg - The SVG string to clean.
|
||||
* @returns {string} The sanitized SVG string.
|
||||
*/
|
||||
export function cleanSvg(svg: string): string {
|
||||
return svg;
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import { adaptV4Theme, createTheme } from '@mui/material/styles';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { breakpoints, colorMap, font, shape, spacing, typography } from '../base/ui/Tokens';
|
||||
import { createColorTokens } from '../base/ui/utils';
|
||||
|
||||
const DEFAULT_FONT_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG by removing dangerous elements.
|
||||
*
|
||||
* @param {string} svg - The SVG string to clean.
|
||||
* @returns {string} The sanitized SVG string.
|
||||
*/
|
||||
export function cleanSvg(svg: string): string {
|
||||
return DOMPurify.sanitize(svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts unitless fontSize and lineHeight values in a typography style object to rem units.
|
||||
* Backward compatibility: This conversion supports custom themes that may still override
|
||||
|
||||
@@ -23,14 +23,13 @@ const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: theme.spacing(2),
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: theme.spacing(3),
|
||||
bottom: 0,
|
||||
left: 0
|
||||
top: 0
|
||||
},
|
||||
|
||||
container: {
|
||||
@@ -80,17 +79,33 @@ const useStyles = makeStyles()(theme => {
|
||||
padding: theme.spacing(3),
|
||||
position: 'relative',
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s'
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
visibility: 'visible'
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
visibility: 'hidden'
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
'&.focused .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&.focused .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
@@ -109,8 +124,11 @@ const useStyles = makeStyles()(theme => {
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
listStyleType: 'none',
|
||||
marginBottom: theme.spacing(8),
|
||||
marginTop: 0,
|
||||
overflowY: 'auto',
|
||||
padding: 0,
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
@@ -195,9 +213,30 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
actionIcon: {
|
||||
background: 'transparent',
|
||||
border: 0,
|
||||
cursor: 'pointer',
|
||||
padding: theme.spacing(1),
|
||||
visibility: 'hidden'
|
||||
visibility: 'hidden',
|
||||
'&:focus': {
|
||||
outline: `2px solid ${theme.palette.action01}`
|
||||
}
|
||||
},
|
||||
|
||||
iconButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
marginLeft: '8px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -205,7 +244,9 @@ const useStyles = makeStyles()(theme => {
|
||||
const FileSharing = () => {
|
||||
const { classes } = useStyles();
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const [ isFocused, setIsFocused ] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const uploadButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore();
|
||||
@@ -234,6 +275,7 @@ const FileSharing = () => {
|
||||
if (e.target.files) {
|
||||
processFiles(e.target.files as FileList, store);
|
||||
e.target.value = ''; // Reset the input value to allow re-uploading the same file
|
||||
uploadButtonRef.current?.focus();
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
@@ -252,11 +294,14 @@ const FileSharing = () => {
|
||||
}, []);
|
||||
|
||||
const handleKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{
|
||||
@@ -269,19 +314,15 @@ const FileSharing = () => {
|
||||
onDragEnter = { handleDragEnter }
|
||||
onDragLeave = { handleDragLeave }
|
||||
onDragOver = { handleDragOver }
|
||||
onDrop = { handleDrop }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<input
|
||||
className = { classes.hiddenInput }
|
||||
multiple = { true }
|
||||
onChange = { handleFileSelect }
|
||||
ref = { fileInputRef }
|
||||
type = 'file' />
|
||||
</div>
|
||||
onDrop = { handleDrop } />
|
||||
{
|
||||
sortedFiles.length === 0 && (
|
||||
<div className = { classes.noFilesContainer }>
|
||||
<div
|
||||
className = { classes.noFilesContainer }
|
||||
onClick = { handleClick }
|
||||
onKeyUp = { handleKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
<Icon
|
||||
className = { classes.uploadIcon }
|
||||
color = { BaseTheme.palette.icon03 }
|
||||
@@ -293,17 +334,28 @@ const FileSharing = () => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<input
|
||||
className = { classes.hiddenInput }
|
||||
multiple = { true }
|
||||
onChange = { handleFileSelect }
|
||||
ref = { fileInputRef }
|
||||
tabIndex = { -1 }
|
||||
type = 'file' />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
sortedFiles.length > 0 && (
|
||||
<div className = { classes.fileList }>
|
||||
<ul className = { classes.fileList }>
|
||||
{
|
||||
sortedFiles.map(file => (
|
||||
<div
|
||||
className = { classes.fileItem }
|
||||
<li
|
||||
className = { `${classes.fileItem} ${isFocused ? 'focused' : ''}` }
|
||||
key = { file.fileId }
|
||||
// Only remove focus when leaving the whole fileItem, not just moving between its buttons
|
||||
onBlur = { e => !e.currentTarget.contains(e.relatedTarget as Node) && setIsFocused(false) }
|
||||
onFocus = { () => setIsFocused(true) }
|
||||
tabIndex = { -1 }
|
||||
title = { file.fileName }>
|
||||
{
|
||||
(file.progress ?? 100) === 100 && (
|
||||
@@ -336,25 +388,30 @@ const FileSharing = () => {
|
||||
{ formatTimestamp(file.timestamp) }
|
||||
</pre>
|
||||
</div>
|
||||
<div className = { classes.buttonContainer }>
|
||||
<Icon
|
||||
className = { `${classes.actionIcon} actionIconVisibility` }
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<div className = { `${classes.buttonContainer} actionIconVisibility` }>
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.downloadFile')} ${file.fileName}` }
|
||||
className = { `${classes.iconButton}` }
|
||||
onClick = { () => dispatch(downloadFile(file.fileId)) }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
</button>
|
||||
|
||||
{
|
||||
isUploadEnabled && (
|
||||
<Icon
|
||||
className = { `${classes.actionIcon} actionIconVisibility` }
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.removeFile')} ${file.fileName}` }
|
||||
className = { `${classes.iconButton}` }
|
||||
onClick = { () => dispatch(removeFile(file.fileId)) }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
@@ -363,17 +420,25 @@ const FileSharing = () => {
|
||||
}
|
||||
{
|
||||
(file.progress ?? 100) < 100 && (
|
||||
<div className = { classes.progressBar }>
|
||||
<>
|
||||
<div
|
||||
className = { classes.progressFill }
|
||||
style = {{ width: `${file.progress}%` }} />
|
||||
</div>
|
||||
aria-label = { t('fileSharing.fileUploadProgress') }
|
||||
aria-valuemax = { 100 }
|
||||
aria-valuemin = { 0 }
|
||||
aria-valuenow = { file.progress }
|
||||
className = { classes.progressBar }
|
||||
role = 'progressbar'>
|
||||
<div
|
||||
className = { classes.progressFill }
|
||||
style = {{ width: `${file.progress}%` }} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
{
|
||||
@@ -384,6 +449,7 @@ const FileSharing = () => {
|
||||
labelKey = { 'fileSharing.uploadFile' }
|
||||
onClick = { handleClick }
|
||||
onKeyPress = { handleKeyPress }
|
||||
ref = { uploadButtonRef }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { showErrorNotification } from '../notifications/actions';
|
||||
import { showErrorNotification, showNotification, showSuccessNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
|
||||
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
|
||||
@@ -101,6 +101,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete file: ${response.statusText}`);
|
||||
}
|
||||
store.dispatch(showSuccessNotification({
|
||||
titleKey: 'fileSharing.removeFileSuccess'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not delete file:', error);
|
||||
@@ -130,6 +133,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
throw new Error('No presigned URL found in the response.');
|
||||
}
|
||||
|
||||
store.dispatch(showNotification({
|
||||
titleKey: 'fileSharing.downloadStarted'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
|
||||
return downloadFile(presignedUrl, fileName);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
@@ -229,6 +236,9 @@ function uploadFile(file: File, store: IStore, token: string): void {
|
||||
const fileSharingHandler = conference?.getFileSharing();
|
||||
|
||||
fileSharingHandler.addFile(fileMetadata);
|
||||
store.dispatch(showSuccessNotification({
|
||||
titleKey: 'fileSharing.fileUploadedSuccessfully'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
} else {
|
||||
handleError();
|
||||
}
|
||||
|
||||
@@ -317,12 +317,12 @@ class LargeVideo extends Component<IProps> {
|
||||
_visibleFilmstrip
|
||||
} = this.props;
|
||||
|
||||
styles.backgroundColor = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
|
||||
styles.background = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
|
||||
|
||||
if (this.props._backgroundAlpha !== undefined) {
|
||||
const alphaColor = setColorAlpha(styles.backgroundColor, this.props._backgroundAlpha);
|
||||
|
||||
styles.backgroundColor = alphaColor;
|
||||
styles.background = alphaColor;
|
||||
}
|
||||
|
||||
if (_customBackgroundImageUrl) {
|
||||
|
||||
@@ -39,8 +39,6 @@ class LobbyChatScreen extends
|
||||
</JitsiScreen>
|
||||
);
|
||||
}
|
||||
|
||||
_onSendMessage: () => void;
|
||||
}
|
||||
|
||||
export default translate(connect(abstractMapStateToProps)(LobbyChatScreen));
|
||||
|
||||
@@ -154,7 +154,7 @@ const Notification = ({
|
||||
<>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.contentTextTitle as TextStyle }>
|
||||
style = { styles.contentTextTitleDescription as TextStyle }>
|
||||
{ titleText }
|
||||
</Text>
|
||||
{
|
||||
|
||||
@@ -45,15 +45,22 @@ export default {
|
||||
|
||||
contentText: {
|
||||
color: BaseTheme.palette.text04,
|
||||
paddingLeft: BaseTheme.spacing[5],
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
contentTextTitleDescription: {
|
||||
color: BaseTheme.palette.text04,
|
||||
fontWeight: 'bold',
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
contentTextTitle: {
|
||||
color: BaseTheme.palette.text04,
|
||||
fontWeight: 'bold',
|
||||
paddingLeft: BaseTheme.spacing[5],
|
||||
paddingTop: BaseTheme.spacing[2]
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -99,7 +106,7 @@ export default {
|
||||
btnContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { isValidElement, useCallback, useContext } from 'react';
|
||||
import React, { isValidElement, useCallback, useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { keyframes } from 'tss-react';
|
||||
@@ -162,6 +162,11 @@ const useStyles = makeStyles()((theme: Theme) => {
|
||||
|
||||
'&.destructive': {
|
||||
color: theme.palette.textError
|
||||
},
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${theme.palette.action01}`,
|
||||
outlineOffset: 2
|
||||
}
|
||||
},
|
||||
|
||||
@@ -192,6 +197,10 @@ const Notification = ({
|
||||
const { t } = useTranslation();
|
||||
const { unmounting } = useContext(NotificationsTransitionContext);
|
||||
const supportUrl = useSelector(getSupportUrl);
|
||||
const isErrorOrWarning = useMemo(
|
||||
() => appearance === NOTIFICATION_TYPE.ERROR || appearance === NOTIFICATION_TYPE.WARNING,
|
||||
[ appearance ]
|
||||
);
|
||||
|
||||
const ICON_COLOR = {
|
||||
error: theme.palette.iconError,
|
||||
@@ -313,11 +322,12 @@ const Notification = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-atomic = 'false'
|
||||
aria-live = 'polite'
|
||||
aria-atomic = { true }
|
||||
aria-live = { isErrorOrWarning ? 'assertive' : 'polite' }
|
||||
className = { cx(classes.container, (unmounting.get(uid ?? '') && 'unmount') as string | undefined) }
|
||||
data-testid = { titleKey || descriptionKey }
|
||||
id = { uid }>
|
||||
id = { uid }
|
||||
role = { isErrorOrWarning ? 'alert' : 'status' }>
|
||||
<div className = { cx(classes.ribbon, appearance) } />
|
||||
<div className = { classes.content }>
|
||||
<div className = { icon }>
|
||||
@@ -332,10 +342,12 @@ const Notification = ({
|
||||
<div className = { classes.actionsContainer }>
|
||||
{mapAppearanceToButtons().map(({ content, onClick, type, testId }) => (
|
||||
<button
|
||||
aria-label = { content }
|
||||
className = { cx(classes.action, type) }
|
||||
data-testid = { testId }
|
||||
key = { content }
|
||||
onClick = { onClick }>
|
||||
onClick = { onClick }
|
||||
type = 'button'>
|
||||
{content}
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -119,6 +119,13 @@ export const VISITORS_PROMOTION_NOTIFICATION_ID = 'VISITORS_PROMOTION_NOTIFICATI
|
||||
*/
|
||||
export const VISITORS_NOT_LIVE_NOTIFICATION_ID = 'VISITORS_NOT_LIVE_NOTIFICATION_ID';
|
||||
|
||||
/**
|
||||
* The identifier of the visitors notification indicating the request for promotion.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const VISITOR_ASKED_TO_JOIN_NOTIFICATION_ID = 'VISITOR_ASKED_TO_JOIN_NOTIFICATION_ID';
|
||||
|
||||
/**
|
||||
* Amount of participants beyond which no join notification will be emitted.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,9 @@ import { translate } from '../../../../base/i18n/functions';
|
||||
import { navigate }
|
||||
from '../../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../../mobile/navigation/routes';
|
||||
import { IProps, _mapStateToProps as abstractMapStateToProps } from '../../LiveStream/AbstractStartLiveStreamDialog';
|
||||
import {
|
||||
IProps, _mapStateToProps as abstractStartLiveStreamDialogMapStateToProps
|
||||
} from '../../LiveStream/AbstractStartLiveStreamDialog';
|
||||
import AbstractRecordButton, {
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps as _abstractMapStateToProps
|
||||
@@ -58,8 +60,8 @@ export function mapStateToProps(state: IReduxState) {
|
||||
|
||||
return {
|
||||
...abstractProps,
|
||||
...abstractMapStateToProps(state),
|
||||
visible: enabled && iosEnabled && abstractProps.visible
|
||||
...abstractStartLiveStreamDialogMapStateToProps(state),
|
||||
visible: Boolean(enabled && iosEnabled && abstractProps.visible)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ export default function RecordingConsentDialog() {
|
||||
const dispatch = useDispatch();
|
||||
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const { consentLearnMoreLink } = recordings ?? {};
|
||||
const learnMore = ` (<a href="${consentLearnMoreLink}" target="_blank" rel="noopener noreferrer">${t('dialog.learnMore')}</a>)`;
|
||||
const learnMore = consentLearnMoreLink
|
||||
? ` (<a href="${consentLearnMoreLink}" target="_blank" rel="noopener noreferrer">${t('dialog.learnMore')}</a>)`
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
APP.API.notifyRecordingConsentDialogOpen(true);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
||||
import { getSoundFileSrc } from '../base/media/functions';
|
||||
import { getLocalParticipant, getRemoteParticipants } from '../base/participants/functions';
|
||||
import { registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { isEmbedded } from '../base/util/embedUtils';
|
||||
import { isSpotTV } from '../base/util/spot';
|
||||
import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
|
||||
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
|
||||
@@ -151,7 +152,7 @@ export function getSessionStatusToShow(state: IReduxState, mode: string): string
|
||||
* @returns {boolean} - Whether local recording is supported or not.
|
||||
*/
|
||||
export function supportsLocalRecording() {
|
||||
return LocalRecordingManager.isSupported();
|
||||
return LocalRecordingManager.isSupported() && !isEmbedded();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* The type of (redux) action which sets the audio settings.
|
||||
*/
|
||||
export const SET_AUDIO_SETTINGS = 'SET_AUDIO_SETTINGS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the preview audio track settings.
|
||||
*/
|
||||
export const SET_PREVIEW_AUDIO_TRACK = 'SET_PREVIEW_AUDIO_TRACK';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the visibility of the audio settings popup.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ import i18next from '../base/i18n/i18next';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
import { getNormalizedDisplayName } from '../base/participants/functions';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { IAudioSettings } from '../base/settings/reducer';
|
||||
import { getLocalVideoTrack } from '../base/tracks/functions.web';
|
||||
import { appendURLHashParam } from '../base/util/uri';
|
||||
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from '../keyboard-shortcuts/actions';
|
||||
@@ -23,7 +24,9 @@ import { toggleBackgroundEffect } from '../virtual-background/actions';
|
||||
import virtualBackgroundLogger from '../virtual-background/logger';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS,
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
SET_PREVIEW_AUDIO_TRACK,
|
||||
SET_VIDEO_SETTINGS_VISIBILITY
|
||||
} from './actionTypes';
|
||||
import LogoutDialog from './components/web/LogoutDialog';
|
||||
@@ -36,7 +39,6 @@ import {
|
||||
getShortcutsTabProps
|
||||
} from './functions.web';
|
||||
|
||||
|
||||
/**
|
||||
* Opens {@code LogoutDialog}.
|
||||
*
|
||||
@@ -340,3 +342,35 @@ export function submitVirtualBackgroundTab(newState: any, isCancel = false) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio preview track.
|
||||
*
|
||||
* @param {any} track - The track to set.
|
||||
* @returns {{
|
||||
* type: SET_PREVIEW_AUDIO_TRACK,
|
||||
* track: any
|
||||
* }}
|
||||
*/
|
||||
export function setPreviewAudioTrack(track: any) {
|
||||
return {
|
||||
type: SET_PREVIEW_AUDIO_TRACK,
|
||||
track
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio settings.
|
||||
*
|
||||
* @param {IAudioSettings} settings - The settings to set.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_SETTINGS,
|
||||
* settings: IAudioSettings
|
||||
* }}
|
||||
*/
|
||||
export function setAudioSettings(settings: IAudioSettings) {
|
||||
return {
|
||||
type: SET_AUDIO_SETTINGS,
|
||||
settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs';
|
||||
import { isCalendarEnabled } from '../../../calendar-sync/functions.web';
|
||||
import { submitAudioDeviceSelectionTab, submitVideoDeviceSelectionTab } from '../../../device-selection/actions.web';
|
||||
import AudioDevicesSelection from '../../../device-selection/components/AudioDevicesSelection';
|
||||
import VideoDeviceSelection from '../../../device-selection/components/VideoDeviceSelection';
|
||||
import AudioDevicesSelection from '../../../device-selection/components/AudioDevicesSelection.web';
|
||||
import VideoDeviceSelection from '../../../device-selection/components/VideoDeviceSelection.web';
|
||||
import {
|
||||
getAudioDeviceSelectionDialogProps,
|
||||
getVideoDeviceSelectionDialogProps
|
||||
@@ -160,6 +160,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
audioSettings: tabState.audioSettings,
|
||||
noiseSuppressionEnabled: tabState.noiseSuppressionEnabled,
|
||||
selectedAudioInputId: tabState.selectedAudioInputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId
|
||||
|
||||
@@ -145,3 +145,31 @@ export function shouldShowModeratorSettings(stateful: IStateful) {
|
||||
|
||||
return hasModeratorRights && !hideModeratorSettingsTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes a track.
|
||||
*
|
||||
* @param {Object} track - The track to dispose.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function disposeTrack(track: any) {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
|
||||
await track.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the audio input preview track from Redux state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function disposePreviewAudioTrack(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const previewTrack = state['features/settings']?.previewAudioTrack;
|
||||
|
||||
await disposeTrack(previewTrack);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { IAudioSettings } from '../base/settings/reducer';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS,
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
SET_PREVIEW_AUDIO_TRACK,
|
||||
SET_VIDEO_SETTINGS_VISIBILITY
|
||||
} from './actionTypes';
|
||||
|
||||
export interface ISettingsState {
|
||||
audioSettings?: IAudioSettings;
|
||||
audioSettingsVisible?: boolean;
|
||||
previewAudioTrack?: any | null;
|
||||
videoSettingsVisible?: boolean;
|
||||
}
|
||||
|
||||
@@ -22,6 +27,16 @@ ReducerRegistry.register('features/settings', (state: ISettingsState = {}, actio
|
||||
...state,
|
||||
videoSettingsVisible: action.value
|
||||
};
|
||||
case SET_PREVIEW_AUDIO_TRACK:
|
||||
return {
|
||||
...state,
|
||||
previewAudioTrack: action.track
|
||||
};
|
||||
case SET_AUDIO_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
audioSettings: action.settings
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -141,19 +141,13 @@ export function shouldDisplayCurrentVisitorsList(stateful: IStateful): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param state
|
||||
* @param displayName
|
||||
* @returns
|
||||
*/
|
||||
/**
|
||||
* Returns visitor's display name, falling back to the default remote display name
|
||||
* Returns display name, falling back to the default remote display name
|
||||
* from config, or 'Fellow Jitster' if neither is available.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @param {string} [displayName] - Optional display name to use if available.
|
||||
* @returns {string} - The display name for a visitor.
|
||||
* @returns {string} - The display name.
|
||||
*/
|
||||
export function getVisitorDisplayName(state: IReduxState, displayName?: string): string {
|
||||
export function getDisplayName(state: IReduxState, displayName?: string): string {
|
||||
return displayName || state['features/base/config'].defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
- breakout_rooms_active - Whether there was a breakout room created in the main room.
|
||||
- breakout_rooms_counter - A counter for breakout rooms created in the main room.
|
||||
- flip_participant_nick - Used in mod_muc_flip, when flipping a participant we store the nick of the second device/participant. Same processing as kicked_participant_nick.
|
||||
- hideDisplayNameForAll - TODO: drop.
|
||||
- hideDisplayNameForGuests - When set to true, the display name of participants is hidden for guests.
|
||||
- jicofo_lock - A boolean value, when set to true the room is locked waiting for Jicofo to join. All attempts to join will be queued until Jicofo joins.
|
||||
- kicked_participant_nick - Used in mod_muc_flip, when flipping a participant we store the nick of the initial device/participant so we can kick it after the second on joins, everything happens in the same pass when the second join presence is processed, so concurrency and overwriting are not a concern.
|
||||
|
||||
@@ -352,6 +352,8 @@ function occupant_joined(event)
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
|
||||
notify_occupants_enable(nil, true, room, occupant.nick, mediaType);
|
||||
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
|
||||
room._data.av_first_moderator_joined = true;
|
||||
|
||||
@@ -53,6 +53,7 @@ local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', '
|
||||
local local_muc_domain = muc_domain_prefix..'.'..local_domain;
|
||||
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
local DISPLAY_NAME_NS = 'http://jitsi.org/protocol/display-name';
|
||||
|
||||
-- in certain cases we consider participants with token as moderators, this is the default behavior which can be turned off
|
||||
local auto_promoted_with_token = module:get_option_boolean('visitors_auto_promoted_with_token', true);
|
||||
@@ -480,6 +481,30 @@ process_host_module(local_domain, function(host_module, host)
|
||||
host_module:hook('message/full', message_handler);
|
||||
end);
|
||||
|
||||
-- add to message stanza display name for the visitor
|
||||
-- remove existing nick to avoid forgery
|
||||
local function process_display_name(stanza, occupant)
|
||||
-- TODO drop nick extension used in messages in favor of new display-name extension
|
||||
-- we want mobile client to update before dropping support for nick from messages
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
stanza:remove_children('display-name', DISPLAY_NAME_NS);
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
stanza:tag('display-name', {
|
||||
xmlns = DISPLAY_NAME_NS,
|
||||
source = 'visitor'
|
||||
}):text(nick_element:get_text()):up();
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS })
|
||||
:text('anonymous'):up();
|
||||
stanza:tag('display-name', {
|
||||
xmlns = DISPLAY_NAME_NS,
|
||||
source = 'visitor'
|
||||
}):text('anonymous'):up();
|
||||
end
|
||||
end
|
||||
|
||||
-- only live chat is supported for visitors
|
||||
module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
@@ -492,16 +517,7 @@ module:hook('muc-occupant-groupchat', function(event)
|
||||
|
||||
-- we manage nick only for visitors
|
||||
if occupant_host ~= main_domain then
|
||||
-- add to message stanza display name for the visitor
|
||||
-- remove existing nick to avoid forgery
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS })
|
||||
:text('anonymous'):up();
|
||||
end
|
||||
process_display_name(stanza, occupant);
|
||||
end
|
||||
|
||||
stanza.attr.from = occupant.nick;
|
||||
@@ -556,15 +572,7 @@ module:hook('muc-private-message', function(event)
|
||||
local original_to = stanza.attr.to;
|
||||
local original_from = stanza.attr.from;
|
||||
|
||||
-- Add nick element for visitor identification
|
||||
-- remove existing nick to avoid forgery
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
local nick_element = sender_occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS }):text('anonymous'):up();
|
||||
end
|
||||
process_display_name(stanza, sender_occupant);
|
||||
|
||||
-- Forward to main prosody, preserving the resource and original from
|
||||
stanza.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain, jid.resource(to));
|
||||
|
||||
@@ -7,6 +7,12 @@ local services_xml = ext_services.services_xml;
|
||||
-- gathers needed information and pushes it with a message to clients
|
||||
-- this way we skip 4 request responses during every client setup
|
||||
|
||||
local main_virtual_host = module:get_option_string('muc_mapper_domain_base');
|
||||
if not main_virtual_host then
|
||||
module:log('warn', 'No muc_mapper_domain_base option set.');
|
||||
return;
|
||||
end
|
||||
|
||||
local shard_name_config = module:get_option_string('shard_name');
|
||||
if shard_name_config then
|
||||
module:add_identity("server", "shard", shard_name_config);
|
||||
@@ -33,7 +39,8 @@ module:hook("resource-bind", function (event)
|
||||
-- disco info data / all identity and features
|
||||
local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" });
|
||||
local done = {};
|
||||
for _,identity in ipairs(module:get_host_items("identity")) do
|
||||
-- to lod this module in different virtual hosts than the main, make sure we query here for main
|
||||
for _,identity in ipairs(module:context(main_virtual_host):get_host_items("identity")) do
|
||||
local identity_s = identity.category.."\0"..identity.type;
|
||||
if not done[identity_s] then
|
||||
query:tag("identity", identity):up();
|
||||
|
||||
@@ -70,7 +70,7 @@ function process_set_affiliation(event)
|
||||
return;
|
||||
end
|
||||
|
||||
if previous_affiliation == 'none' and affiliation == 'owner' then
|
||||
if (previous_affiliation == 'none' or previous_affiliation == 'member') and affiliation == 'owner' then
|
||||
occupant_session.jitsi_meet_context_features = actor_session.jitsi_meet_context_features;
|
||||
if actor_session.jitsi_meet_context_user then
|
||||
occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user['id']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--- This module removes identity information from presence stanzas when the
|
||||
--- hideDisplayNameForAll or hideDisplayNameForGuests options are enabled
|
||||
--- hideDisplayNameForGuests options are enabled
|
||||
--- for a room.
|
||||
|
||||
--- To be enabled under the main muc component
|
||||
@@ -12,6 +12,12 @@ local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_admin = util.is_admin;
|
||||
local ends_with = util.ends_with;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
local DISPLAY_NAME_NS = 'http://jitsi.org/protocol/display-name';
|
||||
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
local ignore_jwt_name = module:get_option_boolean('ignore_jwt_name', false);
|
||||
|
||||
-- we need to get the shared resource for joining moderators, as participants are marked as moderators
|
||||
-- after joining which is after the filter for stanza/out, but we need to know will this participant be a moderator
|
||||
@@ -27,12 +33,11 @@ function filter_stanza_out(stanza, session)
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
local shouldFilter = false;
|
||||
|
||||
if room and (room._data.hideDisplayNameForGuests == true or room._data.hideDisplayNameForAll == true) then
|
||||
if room and room._data.hideDisplayNameForGuests == true then
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
|
||||
-- don't touch self-presence
|
||||
if occupant and stanza.attr.from ~= internal_room_jid_match_rewrite(occupant.nick) then
|
||||
local isModerator = (occupant.role == 'moderator' or joining_moderator_participants[occupant.bare_jid]);
|
||||
shouldFilter = room._data.hideDisplayNameForAll or not isModerator;
|
||||
shouldFilter = occupant.role ~= 'moderator' and not joining_moderator_participants[occupant.bare_jid];
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,8 +48,91 @@ function filter_stanza_out(stanza, session)
|
||||
end
|
||||
end
|
||||
|
||||
-- When a participant has a name-readonly feature enabled, we need to ensure that the
|
||||
-- nick element in the presence stanza is set to the user's name, if it exists.
|
||||
-- If the user does not have a name, we remove the nick element.
|
||||
-- This is to ensure that the name is not changed by the user, as it is read-only.
|
||||
function filter_stanza_in(stanza, session)
|
||||
if not session or not session.jitsi_meet_context_features
|
||||
or session.jitsi_meet_context_features['name-readonly'] ~= true then
|
||||
-- if the name-readonly feature is not set, there is nothing we need to do
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if stanza.name ~= 'presence' or stanza.attr.type == 'error'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- if the user does not have a name in token and name is readonly, remove any nick element
|
||||
if not session.jitsi_meet_context_user or not session.jitsi_meet_context_user.name then
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local nick_element = stanza:get_child('nick', NICK_NS);
|
||||
|
||||
if nick_element:get_text() ~= session.jitsi_meet_context_user.name then
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
stanza:tag('nick', { xmlns = NICK_NS }):text(session.jitsi_meet_context_user.name):up();
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- 'muc-add-history' is called in 'muc-broadcast-message' with priority 0
|
||||
-- we want to clean up message after we have processed it for history, but do it for all messages
|
||||
-- not only those that are added to history (as someone can forge a message and add 'no-store')
|
||||
module:hook('muc-broadcast-message', function(event)
|
||||
-- any message that is about to be delivered, clear up nick and display-name elements
|
||||
event.stanza:remove_children('nick', NICK_NS);
|
||||
event.stanza:remove_children('display-name', DISPLAY_NAME_NS);
|
||||
end, -1);
|
||||
|
||||
module:hook('muc-add-history', function(event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
local name;
|
||||
local source;
|
||||
|
||||
local occupant = room:get_occupant_by_nick(stanza.attr.from);
|
||||
|
||||
if occupant then
|
||||
local session = sessions[occupant.jid];
|
||||
if session and session.jitsi_meet_context_user then
|
||||
if ignore_jwt_name then
|
||||
name = occupant:get_presence():get_child('nick', NICK_NS):get_text();
|
||||
else
|
||||
name = session.jitsi_meet_context_user.name;
|
||||
end
|
||||
|
||||
source = 'token';
|
||||
else
|
||||
name = occupant:get_presence():get_child('nick', NICK_NS):get_text();
|
||||
source = 'guest';
|
||||
end
|
||||
else
|
||||
-- if no occupant found it is a message from visitor let's check display-name source
|
||||
local display_name_element = stanza:get_child('display-name', DISPLAY_NAME_NS);
|
||||
if display_name_element and display_name_element.attr.source == 'visitor' then
|
||||
name = display_name_element:get_text();
|
||||
source = display_name_element.attr.source;
|
||||
end
|
||||
end
|
||||
|
||||
if name then
|
||||
-- clone the stanza, so only history has the display_name extension
|
||||
event.stanza = st.clone(stanza);
|
||||
|
||||
event.stanza:tag('display-name', {
|
||||
xmlns = DISPLAY_NAME_NS,
|
||||
source = source
|
||||
}):text(name):up();
|
||||
end
|
||||
end);
|
||||
|
||||
function filter_session(session)
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza_out, -100);
|
||||
filters.add_filter(session, 'stanzas/in', filter_stanza_in, -100);
|
||||
end
|
||||
|
||||
function module.load()
|
||||
|
||||
@@ -247,6 +247,8 @@ function destroy_lobby_room(room, newjid, message)
|
||||
lobby_room_obj:destroy(newjid, message);
|
||||
end
|
||||
room._data.lobbyroom = nil;
|
||||
room._data.lobby_extra_reason = nil;
|
||||
room._data.lobby_skip_display_name_check = nil;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ function getMetadataJSON(room, metadata)
|
||||
return res;
|
||||
end
|
||||
|
||||
function broadcastMetadata(room)
|
||||
local json_msg = getMetadataJSON(room);
|
||||
|
||||
function broadcastMetadata(room, json_msg)
|
||||
if not json_msg then
|
||||
return;
|
||||
end
|
||||
@@ -99,6 +97,8 @@ function send_metadata(occupant, room, json_msg)
|
||||
metadata_to_send = table_shallow_copy(metadata_to_send);
|
||||
metadata_to_send.participants = participants;
|
||||
metadata_to_send.moderators = moderators;
|
||||
|
||||
module:log('info', 'Sending metadata to jicofo room=%s,meeting_id=%s', room.jid, room._data.meetingId);
|
||||
end
|
||||
|
||||
json_msg = getMetadataJSON(room, metadata_to_send);
|
||||
@@ -193,7 +193,8 @@ function on_message(event)
|
||||
if not table_equals(old_value, jsonData.data) then
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
|
||||
broadcastMetadata(room);
|
||||
module:log('info', 'Мetadata key "%s" updated by %s in room:%s,meeting_id:%s', jsonData.key, from, room.jid, room._data.meetingId);
|
||||
broadcastMetadata(room, getMetadataJSON(room));
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
@@ -218,7 +219,11 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
|
||||
-- The room metadata was updated internally (from another module).
|
||||
host_module:hook("room-metadata-changed", function(event)
|
||||
broadcastMetadata(event.room);
|
||||
local room = event.room;
|
||||
local json_msg = getMetadataJSON(room);
|
||||
|
||||
module:log('info', 'Metadata changed internally in room:%s,meeting_id:%s - broadcasting data:%s', room.jid, room._data.meetingId, json_msg);
|
||||
broadcastMetadata(room, json_msg);
|
||||
end);
|
||||
|
||||
-- TODO: Once clients update to read/write metadata for startMuted policy we can drop this
|
||||
@@ -261,6 +266,17 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end);
|
||||
|
||||
-- The the connection jid for authenticated users (like jicofo) stays the same,
|
||||
-- so leaving and re-joining will result not sending metatadata again.
|
||||
-- Make sure we clear the sent_initial_metadata entry for the occupant on leave.
|
||||
host_module:hook("muc-occupant-left", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if room.sent_initial_metadata then
|
||||
room.sent_initial_metadata[jid.bare(event.occupant.jid)] = nil;
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
@@ -306,8 +322,7 @@ end
|
||||
|
||||
-- Send a message update for metadata before sending the first self presence
|
||||
function filter_stanza(stanza, session)
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence' or stanza.attr.type == 'unavailable' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ local function filter_stanza_nick_if_needed(stanza, room)
|
||||
end
|
||||
|
||||
-- if hideDisplayNameForGuests we want to drop any display name from the presence stanza
|
||||
if room and (room._data.hideDisplayNameForGuests or room._data.hideDisplayNameForAll) then
|
||||
if room and room._data.hideDisplayNameForGuests then
|
||||
return filter_identity_from_presence(stanza);
|
||||
end
|
||||
|
||||
|
||||
@@ -517,6 +517,7 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
or is_sip_jibri_join(stanza)
|
||||
or table_find(room._data.moderators, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id)
|
||||
or (room._data.moderator_id and room._data.moderator_id == (session.jitsi_meet_context_user and session.jitsi_meet_context_user.id))
|
||||
or (room._data.moderator_id and room._data.moderator_id == session.jitsi_meet_context_group)
|
||||
or table_find(room._data.participants, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Auto-allowing visitor %s in room %s', stanza.attr.from, room.jid);
|
||||
|
||||
@@ -26,14 +26,16 @@
|
||||
|
||||
# Configure properties for jaas-specific tests (specs/jaas). Note that some of the iFrame tests can also
|
||||
# be used to test JaaS, they are configured separately via IFRAME_TENANT, JWT_KID, JWT_PRIVATE_KEY_PATH.
|
||||
# Domain for the JaaS environment, e.g. stage.8x8.vc
|
||||
JAAS_DOMAIN=
|
||||
# Make sure this tenant is configured to not allow guests.
|
||||
# The key ID
|
||||
JAAS_KID=
|
||||
#JAAS_KID=
|
||||
# The path to the private key used for generating JWT token (.pk) for jaas-specific tests
|
||||
JAAS_PRIVATE_KEY_PATH=
|
||||
#JAAS_PRIVATE_KEY_PATH=
|
||||
# The JaaS tenant, e.g. vpaas-magic-cookie-abcdabcd1234567890
|
||||
JAAS_TENANT=
|
||||
#JAAS_TENANT=
|
||||
|
||||
# Enable debug logging. Note this includes private information from .env.
|
||||
#JITSI_DEBUG=true
|
||||
|
||||
# An access token to use to create meetings (used for the first participant)
|
||||
#JWT_ACCESS_TOKEN=
|
||||
@@ -67,7 +69,7 @@ JAAS_TENANT=
|
||||
# A shared secret to authenticate the webhook proxy connection
|
||||
#WEBHOOKS_PROXY_SHARED_SECRET=
|
||||
|
||||
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
|
||||
# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service)
|
||||
#WEBHOOKS_PROXY_URL=
|
||||
|
||||
# A stream key abd broadcast ID that can be used by the tests to stream to YouTube
|
||||
|
||||
@@ -13,7 +13,11 @@ import IframeAPI from '../pageobjects/IframeAPI';
|
||||
import InviteDialog from '../pageobjects/InviteDialog';
|
||||
import LargeVideo from '../pageobjects/LargeVideo';
|
||||
import LobbyScreen from '../pageobjects/LobbyScreen';
|
||||
import Notifications, { TOKEN_AUTH_FAILED_TEST_ID, TOKEN_AUTH_FAILED_TITLE_TEST_ID } from '../pageobjects/Notifications';
|
||||
import Notifications, {
|
||||
MAX_USERS_TEST_ID,
|
||||
TOKEN_AUTH_FAILED_TEST_ID,
|
||||
TOKEN_AUTH_FAILED_TITLE_TEST_ID
|
||||
} from '../pageobjects/Notifications';
|
||||
import ParticipantsPane from '../pageobjects/ParticipantsPane';
|
||||
import PasswordDialog from '../pageobjects/PasswordDialog';
|
||||
import PreJoinScreen from '../pageobjects/PreJoinScreen';
|
||||
@@ -32,13 +36,6 @@ export const P2 = 'p2';
|
||||
export const P3 = 'p3';
|
||||
export const P4 = 'p4';
|
||||
|
||||
interface IWaitForSendReceiveDataOptions {
|
||||
checkReceive?: boolean;
|
||||
checkSend?: boolean;
|
||||
msg?: string;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Participant.
|
||||
*/
|
||||
@@ -137,9 +134,10 @@ export class Participant {
|
||||
script: string | ((...innerArgs: InnerArguments) => ReturnValue),
|
||||
...args: InnerArguments): Promise<ReturnValue> {
|
||||
try {
|
||||
// @ts-ignore
|
||||
return await this.driver.execute(script, ...args);
|
||||
} catch (error) {
|
||||
console.error('An error occured while trying to execute a script: ', error);
|
||||
console.error('An error occurred while trying to execute a script: ', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -186,10 +184,10 @@ export class Participant {
|
||||
/**
|
||||
* Joins conference.
|
||||
*
|
||||
* @param {IJoinOptions} options - Options for joining.
|
||||
* @param {IParticipantJoinOptions} options - Options for joining.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async joinConference(options: IParticipantJoinOptions): Promise<void> {
|
||||
async joinConference(options: IParticipantJoinOptions): Promise<Participant> {
|
||||
const config = {
|
||||
room: options.roomName,
|
||||
configOverwrite: {
|
||||
@@ -220,8 +218,8 @@ export class Participant {
|
||||
// @ts-ignore
|
||||
url = `${this.driver.iframePageBase}${url}&domain="${baseUrl.host}"&room="${options.roomName}"`;
|
||||
|
||||
if (process.env.IFRAME_TENANT) {
|
||||
url = `${url}&tenant="${process.env.IFRAME_TENANT}"`;
|
||||
if (options.tenant) {
|
||||
url = `${url}&tenant="${options.tenant}"`;
|
||||
} else if (baseUrl.pathname.length > 1) {
|
||||
// remove leading slash
|
||||
url = `${url}&tenant="${baseUrl.pathname.substring(1)}"`;
|
||||
@@ -231,17 +229,14 @@ export class Participant {
|
||||
url = `${url}&jwt="${this._token.jwt}"`;
|
||||
}
|
||||
|
||||
if (options.baseUrl) {
|
||||
this.driver.options.baseUrl = options.baseUrl;
|
||||
}
|
||||
|
||||
await this.driver.setTimeout({ 'pageLoad': 30000 });
|
||||
|
||||
// drop the leading '/' so we can use the tenant if any
|
||||
url = url.startsWith('/') ? url.substring(1) : url;
|
||||
|
||||
if (options.forceTenant) {
|
||||
url = `/${options.forceTenant}/${url}`;
|
||||
if (options.tenant && !this._iFrameApi) {
|
||||
// For the iFrame API the tenant is passed in a different way.
|
||||
url = `/${options.tenant}/${url}`;
|
||||
}
|
||||
|
||||
await this.driver.url(url);
|
||||
@@ -249,16 +244,16 @@ export class Participant {
|
||||
await this.waitForPageToLoad();
|
||||
|
||||
if (this._iFrameApi) {
|
||||
const mainFrame = this.driver.$('iframe');
|
||||
|
||||
await this.driver.switchFrame(mainFrame);
|
||||
await this.switchToIFrame();
|
||||
}
|
||||
|
||||
if (!options.skipWaitToJoin) {
|
||||
await this.waitToJoinMUC();
|
||||
await this.waitForMucJoinedOrError();
|
||||
}
|
||||
|
||||
await this.postLoadProcess();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,7 +317,7 @@ export class Participant {
|
||||
/**
|
||||
* Waits for the tile view to display.
|
||||
*/
|
||||
async waitForTileViewDisplay(reverse = false) {
|
||||
async waitForTileViewDisplayed(reverse = false) {
|
||||
await this.driver.$('//div[@id="videoconference_page" and contains(@class, "tile-view")]').waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 10_000,
|
||||
@@ -339,16 +334,17 @@ export class Participant {
|
||||
|
||||
/**
|
||||
* Waits until either the MUC is joined, or a password prompt is displayed, or an authentication failure
|
||||
* notification is displayed.
|
||||
* notification is displayed, or max users notification is displayed.
|
||||
*/
|
||||
async waitForMucJoinedOrError(): Promise<void> {
|
||||
await this.driver.waitUntil(async () => {
|
||||
return await this.isInMuc() || await this.getPasswordDialog().isOpen()
|
||||
|| await this.getNotifications().getNotificationText(MAX_USERS_TEST_ID)
|
||||
|| await this.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID)
|
||||
|| await this.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
|
||||
}, {
|
||||
timeout: 10_000,
|
||||
timeoutMsg: 'Timeout waiting for MUC joined or password prompt.'
|
||||
timeoutMsg: 'Timeout waiting for MUC joined or error.'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -422,42 +418,55 @@ export class Participant {
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for send and receive data.
|
||||
* Waits until the conference stats show positive upload and download bitrate (independently).
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {boolean} options.checkSend - If true we will chec
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
waitForSendReceiveData({
|
||||
checkSend = true,
|
||||
checkReceive = true,
|
||||
timeout = 15_000,
|
||||
msg
|
||||
} = {} as IWaitForSendReceiveDataOptions): Promise<boolean> {
|
||||
if (!checkSend && !checkReceive) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
async waitForSendReceiveData(timeout = 15_000, msg?: string): Promise<boolean> {
|
||||
const values = await Promise.all([
|
||||
await this.waitForSendMedia(timeout, msg ? `${msg} (send)` : undefined),
|
||||
await this.waitForReceiveMedia(timeout, msg ? `${msg} (receive)` : undefined)
|
||||
]);
|
||||
|
||||
const lMsg = msg ?? `expected to ${
|
||||
checkSend && checkReceive ? 'receive/send' : checkSend ? 'send' : 'receive'} data in 15s for ${this.name}`;
|
||||
return values[0] && values[1];
|
||||
}
|
||||
|
||||
return this.driver.waitUntil(() => this.execute((pCheckSend: boolean, pCheckReceive: boolean) => {
|
||||
const stats = APP?.conference?.getStats();
|
||||
const bitrateMap = stats?.bitrate || {};
|
||||
const rtpStats = {
|
||||
uploadBitrate: bitrateMap.upload || 0,
|
||||
downloadBitrate: bitrateMap.download || 0
|
||||
};
|
||||
/**
|
||||
* Waits until the conference stats show positive upload bitrate.
|
||||
* @param timeout max time to wait in ms
|
||||
* @param timeoutMsg the message to log if the timeout is reached
|
||||
*/
|
||||
async waitForSendMedia(
|
||||
timeout = 15_000,
|
||||
timeoutMsg = `expected to send media in ${timeout / 1000}s for ${this.name}`): Promise<boolean> {
|
||||
|
||||
return (rtpStats.uploadBitrate > 0 || !pCheckSend) && (rtpStats.downloadBitrate > 0 || !pCheckReceive);
|
||||
}, checkSend, checkReceive), {
|
||||
return this.driver.waitUntil(() => this.execute(() => {
|
||||
return APP?.conference?.getStats()?.bitrate?.upload > 0;
|
||||
}), {
|
||||
timeout,
|
||||
timeoutMsg: lMsg
|
||||
timeoutMsg
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for remote streams.
|
||||
* Waits until the conference stats show positive upload bitrate.
|
||||
* @param timeout max time to wait in ms
|
||||
* @param timeoutMsg the message to log if the timeout is reached
|
||||
*/
|
||||
async waitForReceiveMedia(
|
||||
timeout = 15_000,
|
||||
timeoutMsg = `expected to receive media in ${timeout / 1000}s for ${this.name}`): Promise<boolean> {
|
||||
|
||||
return this.driver.waitUntil(() => this.execute(() => {
|
||||
return APP?.conference?.getStats()?.bitrate?.download > 0;
|
||||
}), {
|
||||
timeout,
|
||||
timeoutMsg
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until there are at least [number] participants that have at least one track.
|
||||
*
|
||||
* @param {number} number - The number of remote streams to wait for.
|
||||
* @returns {Promise<boolean>}
|
||||
@@ -615,19 +624,23 @@ export class Participant {
|
||||
|
||||
|
||||
/**
|
||||
* Switches to the iframe API context
|
||||
* Switches to the main frame context (outside the iFrame; where the Jitsi Meet iFrame API is available).
|
||||
*
|
||||
* If this Participant was initialized with iFrameApi=false this has no effect, as there aren't any other contexts.
|
||||
*/
|
||||
async switchToAPI() {
|
||||
async switchToMainFrame() {
|
||||
await this.driver.switchFrame(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the meeting page context.
|
||||
* Switches to the iFrame context (inside the iFrame; where the Jitsi Meet application runs).
|
||||
*
|
||||
* If this Participant was initialized with iFrameApi=false this will result in an error.
|
||||
*/
|
||||
switchInPage() {
|
||||
const mainFrame = this.driver.$('iframe');
|
||||
async switchToIFrame() {
|
||||
const iframe = this.driver.$('iframe');
|
||||
|
||||
return this.driver.switchFrame(mainFrame);
|
||||
await this.driver.switchFrame(iframe);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -863,26 +876,46 @@ export class Participant {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if video is currently received for the given remote endpoint ID (there is a track, it's not muted,
|
||||
* and it's streaming status according to the connection-indicator is active).
|
||||
*/
|
||||
async isRemoteVideoReceived(endpointId: string): Promise<boolean> {
|
||||
return this.execute(e => JitsiMeetJS.app.testing.isRemoteVideoReceived(e), endpointId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the remove video is displayed for the given remote endpoint ID.
|
||||
* @param endpointId
|
||||
*/
|
||||
async isRemoteVideoDisplayed(endpointId: string): Promise<boolean> {
|
||||
return this.driver.$(
|
||||
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if remote video for a specific remote endpoint is both received and displayed.
|
||||
* @param endpointId
|
||||
*/
|
||||
async isRemoteVideoReceivedAndDisplayed(endpointId: string): Promise<boolean> {
|
||||
return await this.isRemoteVideoReceived(endpointId) && await this.isRemoteVideoDisplayed(endpointId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for remote video state - receiving and displayed.
|
||||
* @param endpointId
|
||||
* @param reverse
|
||||
* @param reverse if true, waits for the remote video to NOT be received AND NOT displayed.
|
||||
*/
|
||||
async waitForRemoteVideo(endpointId: string, reverse = false) {
|
||||
if (reverse) {
|
||||
await this.driver.waitUntil(async () =>
|
||||
!await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
|
||||
endpointId) && !await this.driver.$(
|
||||
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
|
||||
!await this.isRemoteVideoReceived(endpointId) && !await this.isRemoteVideoDisplayed(endpointId), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.name}`
|
||||
});
|
||||
} else {
|
||||
await this.driver.waitUntil(async () =>
|
||||
await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
|
||||
endpointId) && await this.driver.$(
|
||||
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
|
||||
await this.isRemoteVideoReceivedAndDisplayed(endpointId), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
|
||||
});
|
||||
|
||||
44
tests/helpers/TestsConfig.ts
Normal file
44
tests/helpers/TestsConfig.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Global configuration that the tests are run with. Loaded from environment variables.
|
||||
*/
|
||||
export const config = {
|
||||
/** Enable debug logging. Note this includes private information from .env */
|
||||
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
|
||||
iframe: {
|
||||
customerId: process.env.IFRAME_TENANT?.trim()?.replace('vpaas-magic-cookie-', ''),
|
||||
tenant: process.env.IFRAME_TENANT?.trim(),
|
||||
/** Whether the configuration specifies a JaaS account for the iFrame API tests. */
|
||||
usesJaas: Boolean(process.env.JWT_PRIVATE_KEY_PATH && process.env.JWT_KID?.startsWith('vpaas-magic-cookie-')),
|
||||
},
|
||||
jaas: {
|
||||
/** Whether the configuration for JaaS specific tests is enabled. */
|
||||
enabled: Boolean(process.env.JAAS_TENANT && process.env.JAAS_PRIVATE_KEY_PATH && process.env.JAAS_KID),
|
||||
/** The JaaS key ID, used to sign the tokens. */
|
||||
kid: process.env.JAAS_KID?.trim(),
|
||||
/** The path to the JaaS private key, used to sign JaaS tokens. */
|
||||
privateKeyPath: process.env.JAAS_PRIVATE_KEY_PATH?.trim(),
|
||||
/** The JaaS tenant (vpaas-magic-cookie-<ID>) . */
|
||||
tenant: process.env.JAAS_TENANT?.trim(),
|
||||
},
|
||||
jwt: {
|
||||
kid: process.env.JWT_KID?.trim(),
|
||||
/** A pre-configured token used by some tests. */
|
||||
preconfiguredToken: process.env.JWT_ACCESS_TOKEN?.trim(),
|
||||
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH?.trim()
|
||||
},
|
||||
roomName: {
|
||||
/** Optional prefix for room names used for tests. */
|
||||
prefix: process.env.ROOM_NAME_PREFIX?.trim(),
|
||||
/** Optional suffix for room names used for tests. */
|
||||
suffix: process.env.ROOM_NAME_SUFFIX?.trim()
|
||||
},
|
||||
webhooksProxy: {
|
||||
enabled: Boolean(process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET),
|
||||
sharedSecret: process.env.WEBHOOKS_PROXY_SHARED_SECRET?.trim(),
|
||||
url: process.env.WEBHOOKS_PROXY_URL?.trim(),
|
||||
}
|
||||
};
|
||||
|
||||
if (config.debug) {
|
||||
console.log('TestsConfig:', JSON.stringify(config, null, 2));
|
||||
}
|
||||
@@ -107,7 +107,7 @@ export default class WebhookProxy {
|
||||
* @param eventType
|
||||
* @param timeout
|
||||
*/
|
||||
async waitForEvent(eventType: string, timeout = 4000): Promise<any> {
|
||||
async waitForEvent(eventType: string, timeout = 120000): Promise<any> {
|
||||
// we create the error here so we have a meaningful stack trace
|
||||
const error = new Error(`Timeout waiting for event:${eventType}`);
|
||||
|
||||
|
||||
@@ -1,36 +1,55 @@
|
||||
import process from 'node:process';
|
||||
|
||||
import { P1, P2, P3, P4, Participant } from './Participant';
|
||||
import { IToken, generateToken } from './token';
|
||||
import { IContext, IJoinOptions } from './types';
|
||||
import { config } from './TestsConfig';
|
||||
import { generateToken } from './token';
|
||||
import { IJoinOptions, IParticipantOptions } from './types';
|
||||
|
||||
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
|
||||
|
||||
/**
|
||||
* Ensure that there is on participant.
|
||||
* Ensure that the first participant is moderator if there is such an option.
|
||||
*
|
||||
* @param {IContext} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
export async function ensureOneParticipant(options?: IJoinOptions): Promise<void> {
|
||||
const participantOps = { name: P1 } as IParticipantOptions;
|
||||
|
||||
if (!options?.skipFirstModerator) {
|
||||
const jwtPrivateKeyPath = config.jwt.privateKeyPath;
|
||||
|
||||
// we prioritize the access token when iframe is not used and private key is set,
|
||||
// otherwise if private key is not specified we use the access token if set
|
||||
if (config.jwt.preconfiguredToken
|
||||
&& ((jwtPrivateKeyPath && !ctx.testProperties.useIFrameApi && !options?.preferGenerateToken)
|
||||
|| !jwtPrivateKeyPath)) {
|
||||
participantOps.token = { jwt: config.jwt.preconfiguredToken };
|
||||
} else if (jwtPrivateKeyPath) {
|
||||
participantOps.token = generateToken({
|
||||
...options?.tokenOptions,
|
||||
displayName: participantOps.name,
|
||||
moderator: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the first participant is moderator, if supported by deployment
|
||||
await joinParticipant(participantOps, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are three participants.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureThreeParticipants(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
export async function ensureThreeParticipants(options?: IJoinOptions): Promise<void> {
|
||||
await ensureOneParticipant(options);
|
||||
|
||||
// these need to be all, so we get the error when one fails
|
||||
await Promise.all([
|
||||
_joinParticipant(P2, ctx, options),
|
||||
_joinParticipant(P3, ctx, options)
|
||||
joinParticipant({ name: P2 }, options),
|
||||
joinParticipant({ name: P3 }, options)
|
||||
]);
|
||||
|
||||
if (options?.skipInMeetingChecks) {
|
||||
@@ -52,51 +71,47 @@ export async function ensureThreeParticipants(ctx: IContext, options?: IJoinOpti
|
||||
/**
|
||||
* Creates the first participant instance or prepares one for re-joining.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function joinFirstParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
|
||||
return joinTheModeratorAsP1(ctx, options);
|
||||
export function joinFirstParticipant(options: IJoinOptions = { }): Promise<void> {
|
||||
return ensureOneParticipant(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the second participant instance or prepares one for re-joining.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<Participant>}
|
||||
*/
|
||||
export function joinSecondParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
return _joinParticipant(P2, ctx, options);
|
||||
export function joinSecondParticipant(options?: IJoinOptions): Promise<Participant> {
|
||||
return joinParticipant({ name: P2 }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the third participant instance or prepares one for re-joining.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<Participant>}
|
||||
*/
|
||||
export function joinThirdParticipant(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
return _joinParticipant(P3, ctx, options);
|
||||
export function joinThirdParticipant(options?: IJoinOptions): Promise<Participant> {
|
||||
return joinParticipant({ name: P3 }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are four participants.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureFourParticipants(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
export async function ensureFourParticipants(options?: IJoinOptions): Promise<void> {
|
||||
await ensureOneParticipant(options);
|
||||
|
||||
// these need to be all, so we get the error when one fails
|
||||
await Promise.all([
|
||||
_joinParticipant(P2, ctx, options),
|
||||
_joinParticipant(P3, ctx, options),
|
||||
_joinParticipant(P4, ctx, options)
|
||||
joinParticipant({ name: P2 }, options),
|
||||
joinParticipant({ name: P3 }, options),
|
||||
joinParticipant({ name: P4 }, options)
|
||||
]);
|
||||
|
||||
if (options?.skipInMeetingChecks) {
|
||||
@@ -117,28 +132,27 @@ export async function ensureFourParticipants(ctx: IContext, options?: IJoinOptio
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the first participant is moderator.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to join.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
|
||||
// make sure the first participant is moderator, if supported by deployment
|
||||
await _joinParticipant(P1, ctx, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are two participants.
|
||||
*
|
||||
* @param {Object} ctx - The context.
|
||||
* @param {IJoinOptions} options - The options to join.
|
||||
*/
|
||||
export async function ensureTwoParticipants(ctx: IContext, options?: IJoinOptions): Promise<void> {
|
||||
await joinTheModeratorAsP1(ctx, options);
|
||||
export async function ensureTwoParticipants(options?: IJoinOptions): Promise<void> {
|
||||
await ensureOneParticipant(options);
|
||||
|
||||
await _joinParticipant(P2, ctx, options);
|
||||
const participantOptions = { name: P2 } as IParticipantOptions;
|
||||
|
||||
if (options?.preferGenerateToken) {
|
||||
participantOptions.token = generateToken({
|
||||
...options?.tokenOptions,
|
||||
displayName: participantOptions.name,
|
||||
});
|
||||
}
|
||||
|
||||
await joinParticipant({
|
||||
...participantOptions,
|
||||
name: P2
|
||||
}, options);
|
||||
|
||||
if (options?.skipInMeetingChecks) {
|
||||
return Promise.resolve();
|
||||
@@ -155,81 +169,57 @@ export async function ensureTwoParticipants(ctx: IContext, options?: IJoinOption
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a participant instance or prepares one for re-joining.
|
||||
* @param name - The name of the participant.
|
||||
* @param {IContext} ctx - The context.
|
||||
* Creates a new participant instance, or returns an existing one if it is already joined.
|
||||
* @param participantOptions - The participant options, with required name set.
|
||||
* @param {boolean} options - Join options.
|
||||
* @param reuse whether to reuse an existing participant instance if one is available.
|
||||
* @returns {Promise<Participant>} - The participant instance.
|
||||
*/
|
||||
async function _joinParticipant( // eslint-disable-line max-params
|
||||
name: string,
|
||||
ctx: IContext,
|
||||
options?: IJoinOptions) {
|
||||
async function joinParticipant( // eslint-disable-line max-params
|
||||
participantOptions: IParticipantOptions,
|
||||
options?: IJoinOptions
|
||||
): Promise<Participant> {
|
||||
|
||||
participantOptions.iFrameApi = ctx.testProperties.useIFrameApi;
|
||||
|
||||
// @ts-ignore
|
||||
const p = ctx[name] as Participant;
|
||||
const p = ctx[participantOptions.name] as Participant;
|
||||
|
||||
if (p) {
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
await p.switchInPage();
|
||||
await p.switchToIFrame();
|
||||
}
|
||||
|
||||
if (await p.isInMuc()) {
|
||||
return;
|
||||
return p;
|
||||
}
|
||||
|
||||
if (ctx.testProperties.useIFrameApi) {
|
||||
// when loading url make sure we are on the top page context or strange errors may occur
|
||||
await p.switchToAPI();
|
||||
await p.switchToMainFrame();
|
||||
}
|
||||
|
||||
// Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty
|
||||
await p.driver.url('/base.html');
|
||||
|
||||
// we want the participant instance re-recreated so we clear any kept state, like endpoint ID
|
||||
}
|
||||
|
||||
let token: IToken = { jwt: '' };
|
||||
|
||||
if (name === P1) {
|
||||
if (!options?.skipFirstModerator) {
|
||||
// we prioritize the access token when iframe is not used and private key is set,
|
||||
// otherwise if private key is not specified we use the access token if set
|
||||
if (process.env.JWT_ACCESS_TOKEN
|
||||
&& ((ctx.jwtPrivateKeyPath && !ctx.testProperties.useIFrameApi && !options?.preferGenerateToken)
|
||||
|| !ctx.jwtPrivateKeyPath)) {
|
||||
token = { jwt: process.env.JWT_ACCESS_TOKEN };
|
||||
} else if (ctx.jwtPrivateKeyPath) {
|
||||
token = generateToken({
|
||||
...options?.tokenOptions,
|
||||
displayName: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (name === P2) {
|
||||
if (options?.preferGenerateToken) {
|
||||
token = generateToken({
|
||||
...options?.tokenOptions,
|
||||
displayName: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newParticipant = new Participant({ name, token, iFrameApi: ctx.testProperties.useIFrameApi });
|
||||
const newParticipant = new Participant(participantOptions);
|
||||
|
||||
// set the new participant instance
|
||||
// @ts-ignore
|
||||
ctx[name] = newParticipant;
|
||||
ctx[participantOptions.name] = newParticipant;
|
||||
|
||||
let forceTenant;
|
||||
let tenant = options?.tenant;
|
||||
|
||||
if (options?.preferGenerateToken && !ctx.testProperties.useIFrameApi
|
||||
&& process.env.JWT_KID?.startsWith('vpaas-magic-cookie-') && process.env.IFRAME_TENANT) {
|
||||
forceTenant = process.env.IFRAME_TENANT;
|
||||
&& config.iframe.usesJaas && config.iframe.tenant) {
|
||||
tenant = config.iframe.tenant;
|
||||
}
|
||||
await newParticipant.joinConference({
|
||||
|
||||
return await newParticipant.joinConference({
|
||||
...options,
|
||||
forceTenant,
|
||||
roomName: ctx.roomName,
|
||||
tenant: tenant,
|
||||
roomName: options?.roomName || ctx.roomName,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import fs from 'fs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { config } from './TestsConfig';
|
||||
|
||||
export type ITokenOptions = {
|
||||
displayName?: string;
|
||||
/**
|
||||
@@ -10,12 +12,12 @@ export type ITokenOptions = {
|
||||
exp?: string;
|
||||
/**
|
||||
* The key ID to use for the token.
|
||||
* If not provided, JWT_KID will be used from the environment variables.
|
||||
* If not provided, the kid configured with environment variables will be used (see env.example).
|
||||
*/
|
||||
keyId?: string;
|
||||
/**
|
||||
* The path to the private key file used to sign the token.
|
||||
* If not provided, JWT_PRIVATE_KEY_PATH will be used from the environment variables.
|
||||
* If not provided, the path configured with environment variables will be used (see env.example).
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
@@ -91,8 +93,8 @@ export function generatePayload(options: ITokenOptions): any {
|
||||
* Generate a signed token.
|
||||
*/
|
||||
export function generateToken(options: ITokenOptions): IToken {
|
||||
const keyId = options.keyId || process.env.JWT_KID;
|
||||
const keyPath = options.keyPath || process.env.JWT_PRIVATE_KEY_PATH;
|
||||
const keyId = options.keyId || config.jwt.kid;
|
||||
const keyPath = options.keyPath || config.jwt.privateKeyPath;
|
||||
const headers = {
|
||||
algorithm: 'RS256',
|
||||
noTimestamp: true,
|
||||
@@ -101,11 +103,11 @@ export function generateToken(options: ITokenOptions): IToken {
|
||||
};
|
||||
|
||||
if (!keyId) {
|
||||
throw new Error('JWT_KID is not set');
|
||||
throw new Error('No keyId provided (JWT_KID is not set?)');
|
||||
}
|
||||
|
||||
if (!keyPath) {
|
||||
throw new Error('JWT_PRIVATE_KEY_PATH is not set');
|
||||
throw new Error('No keyPath provided (JWT_PRIVATE_KEY_PATH is not set?)');
|
||||
}
|
||||
|
||||
const key = fs.readFileSync(keyPath);
|
||||
|
||||
@@ -7,20 +7,30 @@ import { IToken, ITokenOptions } from './token';
|
||||
|
||||
export type IContext = {
|
||||
/**
|
||||
* Whether the configuration specifies a JaaS account for the iFrame API tests.
|
||||
*/
|
||||
iFrameUsesJaas: boolean;
|
||||
jwtKid: string;
|
||||
jwtPrivateKeyPath: string;
|
||||
keepAlive: Array<any>;
|
||||
* The up-to-four browser instances provided by the framework. These can be initialized using
|
||||
* ensureOneParticipant, ensureTwoParticipants, etc. from participants.ts.
|
||||
**/
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
/** A room name automatically generated by the framework for convenience. */
|
||||
roomName: string;
|
||||
/**
|
||||
* A flag that tests can set, which signals to the framework that the (rest of the) test suite should be skipped.
|
||||
*/
|
||||
skipSuiteTests: boolean;
|
||||
/**
|
||||
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
|
||||
* set up the context appropriately.
|
||||
**/
|
||||
testProperties: ITestProperties;
|
||||
times: any;
|
||||
/**
|
||||
* A WebhooksProxy instance generated by the framework and available for tests to use, if configured.
|
||||
* Note that this is only configured for roomName, if a test wishes to use a different room name it can set up
|
||||
* a WebhooksProxy instance itself.
|
||||
*/
|
||||
webhooksProxy: WebhookProxy;
|
||||
};
|
||||
|
||||
@@ -37,21 +47,12 @@ export type IParticipantOptions = {
|
||||
* Options for joinConference.
|
||||
*/
|
||||
export type IParticipantJoinOptions = {
|
||||
/**
|
||||
* Overwrites the base url set in the config.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
|
||||
/**
|
||||
* Config overwrites to use.
|
||||
*/
|
||||
configOverwrite?: IConfig;
|
||||
|
||||
/**
|
||||
* An optional tenant to use. If provided the URL is prepended with /$forceTenant
|
||||
*/
|
||||
forceTenant?: string;
|
||||
|
||||
/** The name of the room to join */
|
||||
roomName: string;
|
||||
|
||||
@@ -65,17 +66,17 @@ export type IParticipantJoinOptions = {
|
||||
* based on the logic of the test.
|
||||
*/
|
||||
skipWaitToJoin?: boolean;
|
||||
|
||||
/**
|
||||
* An optional tenant to use. If provided it overrides the default.
|
||||
*/
|
||||
tenant?: string;
|
||||
};
|
||||
|
||||
export type IJoinOptions = {
|
||||
|
||||
/**
|
||||
* Overwrites the base url set in the config.
|
||||
*/
|
||||
baseUrl?: string;
|
||||
|
||||
/**
|
||||
* Config overwrites to use.
|
||||
* Config overwrites to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
configOverwrite?: IConfig;
|
||||
|
||||
@@ -86,7 +87,12 @@ export type IJoinOptions = {
|
||||
preferGenerateToken?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip setting display name.
|
||||
* To be able to override the ctx generated room name. If missing the one from the context will be used.
|
||||
*/
|
||||
roomName?: string;
|
||||
|
||||
/**
|
||||
*The skip display name setting to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
skipDisplayName?: boolean;
|
||||
|
||||
@@ -101,11 +107,15 @@ export type IJoinOptions = {
|
||||
skipInMeetingChecks?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip waiting for the participant to join the room. Cases like lobby where we do not succeed to join
|
||||
* based on the logic of the test.
|
||||
* The skip waiting for the participant to join the room setting to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
skipWaitToJoin?: boolean;
|
||||
|
||||
/**
|
||||
* An optional tenant to use. If provided the default tenant is changed to it.
|
||||
*/
|
||||
tenant?: string;
|
||||
|
||||
/**
|
||||
* Options used when generating a token.
|
||||
*/
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { config as testsConfig } from './TestsConfig';
|
||||
|
||||
const https = require('https');
|
||||
|
||||
/**
|
||||
* Generates a random number between 1 and the specified maximum value (inclusive).
|
||||
*
|
||||
* @param {number} max - The maximum value for the random number (must be a positive integer).
|
||||
* @param numberOfDigits - The number of digits to pad the random number with leading zeros.
|
||||
* @return {string} The random number formatted with leading zeros if needed.
|
||||
*/
|
||||
export function getRandomNumberAsStr(max: number, numberOfDigits: number): string {
|
||||
const randomNumber = Math.floor(Math.random() * max) + 1;
|
||||
export function generateRoomName(testName: string) {
|
||||
// XXX why chose between 1 and 40 and then always pad with an extra 0?
|
||||
const rand = (Math.floor(Math.random() * 40) + 1).toString().padStart(3, '0');
|
||||
let roomName = `${testName}-${rand}`;
|
||||
|
||||
return randomNumber.toString().padStart(numberOfDigits, '0');
|
||||
if (testsConfig.roomName.prefix) {
|
||||
roomName = `${testsConfig.roomName.prefix}_${roomName}`;
|
||||
}
|
||||
if (testsConfig.roomName.suffix) {
|
||||
roomName += `_${testsConfig.roomName.suffix}`;
|
||||
}
|
||||
|
||||
return roomName.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,6 +41,10 @@ export default class IframeAPI extends BasePageObject {
|
||||
addEventListener(eventName: string) {
|
||||
return this.participant.execute(
|
||||
(event, prefix) => {
|
||||
// we want to add it once as we use static .test[event] to store the last event
|
||||
if (window.jitsiAPI.listenerCount(event) > 0) {
|
||||
return;
|
||||
}
|
||||
console.log(`${new Date().toISOString()} ${prefix}iframeAPI - Adding listener for event: ${event}`);
|
||||
window.jitsiAPI.addListener(event, evt => {
|
||||
console.log(
|
||||
|
||||
@@ -19,6 +19,7 @@ const REENABLE_SELF_VIEW_CLOSE_NOTIFICATION = 'notify.selfViewTitle-dismiss';
|
||||
const REENABLE_SELF_VIEW_NOTIFICATION_ID = 'notify.selfViewTitle';
|
||||
const YOU_ARE_MUTED_TEST_ID = 'notify.mutedTitle';
|
||||
|
||||
export const MAX_USERS_TEST_ID = 'dialog.maxUsersLimitReached';
|
||||
export const TOKEN_AUTH_FAILED_TEST_ID = 'dialog.tokenAuthFailed';
|
||||
export const TOKEN_AUTH_FAILED_TITLE_TEST_ID = 'dialog.tokenAuthFailedTitle';
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
if (event.role === "moderator" && event.id === window.jitsiAPI.test.myEndpointId) {
|
||||
window.jitsiAPI.test.isModerator = true;
|
||||
}
|
||||
window.jitsiAPI.test['participantRoleChanged'] = event;
|
||||
});
|
||||
window.jitsiAPI.addEventListener('audioAvailabilityChanged', function(event) {
|
||||
log(`audioAvailabilityChanged: ${JSON.stringify(event)}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('Audio only', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants(ctx));
|
||||
it('joining the meeting', () => ensureTwoParticipants());
|
||||
|
||||
/**
|
||||
* Enables audio only mode for p1 and verifies that the other participant sees participant1 as video muted.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
|
||||
describe('DisplayName', () => {
|
||||
it('joining the meeting', () => ensureTwoParticipants(ctx, { skipDisplayName: true }));
|
||||
it('joining the meeting', () => ensureTwoParticipants({ skipDisplayName: true }));
|
||||
|
||||
it('check change', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
@@ -32,7 +32,7 @@ describe('DisplayName', () => {
|
||||
|
||||
await p2.hangup();
|
||||
|
||||
await ensureTwoParticipants(ctx, {
|
||||
await ensureTwoParticipants({
|
||||
skipDisplayName: true
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user