Compare commits

..

31 Commits

Author SHA1 Message Date
Hristo Terezov
e7cb719be0 feat(jibri-queue): JibriQueue support. 2020-08-18 12:57:48 -05:00
Tudor-Ovidiu Avram
e6676bb09a fix(prejoin) fix css loading path 2020-08-12 10:55:26 +03:00
Hristo Terezov
8e9a51f742 feat(Watermarks): Add deafault logo prop. 2020-08-11 18:24:44 -05:00
Saúl Ibarra Corretgé
004c1b65ad misc: drop babel.config.js file
This leaves jest testing non-functional, but having a babel.config.js interferes
with React Native.

Fixes: https://github.com/jitsi/jitsi-meet/issues/7450
2020-08-10 16:39:50 +02:00
Saúl Ibarra Corretgé
eabcc078ef android: fix adb reverse command in case of non-default port 2020-08-10 16:39:29 +02:00
Saúl Ibarra Corretgé
5b1f852783 rn: always reset the transform cache when starting the packager
It can help catch very weird bugs which are only triggered after a reset.
2020-08-10 16:39:29 +02:00
Saúl Ibarra Corretgé
5cf9a76f9e android: bump minimum API level to 23
Android < 23 is currently less than 3% for us so let's try to lower the
maintenance burden. Users can still download an older version no problem.
2020-08-10 16:37:20 +02:00
Saúl Ibarra Corretgé
3f33adc5d0 android: target API level 29
This will be mandatory later in the year for updating apps and it already is for
new apps.

https://support.google.com/googleplay/android-developer/answer/113469#targetsdk
2020-08-10 16:37:20 +02:00
Saúl Ibarra Corretgé
d9250aa986 android: bring back activity to the foreground when exiting PiP
When exiting PiP with by pressing the X the onPictureInPictureModeChanged method
is called. Since onResume is called a while after, in case the maximize button
is called, it's not easy to know if the user pressed the X button, and that was
the cause for exiting PiP.

So, in order to avoid show the user they are still in the meeting, bring the
activity to the foregound so they can hangup.
2020-08-10 16:36:30 +02:00
James Carbine
5b10d8f5ef Add config to hide logo-deep-linking on deeplinking page (#7346)
This adds the ability to configure hiding the logo on the deep linking page.
HIDE_DEEP_LINKING_LOGO defaults to false in the config.
The implementation also defaults to showing the logo if HIDE_DEEP_LINKING_LOGO
is missing from the config.
2020-08-10 09:30:16 -05:00
settyan117
02885ea716 lang: updated Japanese translation 2020-08-10 09:34:39 +02:00
Florian
7e70a57eb3 lang: update French translation 2020-08-10 09:33:04 +02:00
Alexey Napalkov
dbaa1168b3 lang: update Russian translation 2020-08-10 09:32:16 +02:00
Fabricio Oliveira
b1d691ca07 lang: update brazilian portuguese translation 2020-08-10 09:30:30 +02:00
Jaroslav Lichtblau
10a4612230 lang: update Czech translation 2020-08-10 09:29:42 +02:00
Aaron van Meerten
21767fa7cf Merge pull request #7481 from jitsi/aaronkvanmeerten/token-util-cache-in-object
FEAT: jwt pubkey cache inside object
2020-08-07 14:18:24 -05:00
Aaron van Meerten
0bd100f027 FIX: prosody: comment on destroy_request 2020-08-07 13:16:17 -05:00
Aaron van Meerten
f14a595462 FIX: prosody: destroy_request check 2020-08-07 13:15:55 -05:00
Hristo Terezov
5e4b8c747c feat(prejoin): Hide invite link 2020-08-07 12:28:05 -05:00
Aaron van Meerten
11ee71a51c FEAT: jwt pubkey cache inside object
Allows each module that does token validation to have its own cache
2020-08-07 11:51:44 -05:00
Mihai Uscat
c998d83f34 feat(welcome): Add variables. 2020-08-07 12:02:14 +03:00
Jaya Allamsetty
1a957ed85b feat(video-quality): Impose max-bitrates on video sender based on videoQuality settings (#7467)
* feat(video-quality): Impose max-bitrates on video sender based on config.js settings
Update to lib-jitsi-meet@latest
f74cd0abe9
2020-08-06 14:24:33 -04:00
Jaya Allamsetty
1dbb47b84f deps: lib-jitsi-meet@latest 2020-08-06 08:33:28 -04:00
paweldomas
4adaa6f1fd fix(last-n-limits): crash on undefined 2020-08-06 09:02:21 +02:00
Paweł Domas
b3b561f27a feat(video quality): add maxFullResolutionParticipants (#7403)
Add a config option with the default value of 2, which will cap the max recv video quality to SD if there's more than 2 participants in the conference while in the tile view mode.
2020-08-05 10:10:14 -05:00
Jesús Espino
a6a19a3002 Fix lint error 2020-08-05 10:07:18 -04:00
Jesús Espino
1426a5b4bc Restore dots menu on mobile 2020-08-05 10:07:18 -04:00
sellth
858ee557d4 lang: update German translation 2020-08-05 09:34:15 +02:00
Jaya Allamsetty
bd64c14aaa fix: Send SS stopped analytics event when SS was started in video mute state 2020-08-04 19:30:10 -04:00
Manuel Garcia
ce286f9be8 lang: fix typo 2020-08-04 10:02:21 +02:00
Paweł Domas
cc9cb6a874 feat(config): add last N limit mapping (#7422)
Adds 'lastNLimits' config value which allows to define last N value per number of participants.
See config.js for more details.
2020-08-03 12:39:17 -05:00
63 changed files with 8826 additions and 790 deletions

View File

@@ -23,7 +23,6 @@ import android.content.IntentFilter;
import android.content.RestrictionEntry;
import android.content.RestrictionsManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
@@ -96,7 +95,7 @@ public class MainActivity extends JitsiMeetActivity {
// In Debug builds React needs permission to write over other apps in
// order to display the warning and error overlays.
if (BuildConfig.DEBUG) {
if (canRequestOverlayPermission() && !Settings.canDrawOverlays(this)) {
if (!Settings.canDrawOverlays(this)) {
Intent intent
= new Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
@@ -186,8 +185,7 @@ public class MainActivity extends JitsiMeetActivity {
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE
&& canRequestOverlayPermission()) {
if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) {
if (Settings.canDrawOverlays(this)) {
initialize();
return;
@@ -210,6 +208,18 @@ public class MainActivity extends JitsiMeetActivity {
return super.onKeyUp(keyCode, event);
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode);
Log.d(TAG, "Is in picture-in-picture mode: " + isInPictureInPictureMode);
if (!isInPictureInPictureMode) {
this.startActivity(new Intent(this, getClass())
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
}
}
// Helper methods
//
@@ -220,10 +230,4 @@ public class MainActivity extends JitsiMeetActivity {
return null;
}
}
private boolean canRequestOverlayPermission() {
return
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.M;
}
}

View File

@@ -142,10 +142,10 @@ allprojects {
}
ext {
buildToolsVersion = "28.0.3"
compileSdkVersion = 28
minSdkVersion = 21
targetSdkVersion = 28
buildToolsVersion = "29.0.3"
compileSdkVersion = 29
minSdkVersion = 23
targetSdkVersion = 29
supportLibVersion = "28.0.0"
// The Maven artifact groupdId of the third-party react-native modules which

View File

@@ -0,0 +1,5 @@
#!/bin/bash
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
exec ${THIS_DIR}/../../node_modules/react-native/scripts/launchPackager.command --reset-cache

View File

@@ -8,7 +8,7 @@ THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOUR
export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${THIS_DIR}/../../node_modules/react-native/scripts/.packager.env"
adb reverse tcp:8081 tcp:8081
adb reverse tcp:$RCT_METRO_PORT tcp:$RCT_METRO_PORT
if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then
@@ -16,11 +16,10 @@ if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
exit 2
fi
else
CMD="${THIS_DIR}/../../node_modules/react-native/scripts/launchPackager.command"
CMD="$THIS_DIR/run-packager-helper.command"
if [[ `uname` == "Darwin" ]]; then
open -g "${CMD}" || echo "Can't start packager automatically"
else
xdg-open "${CMD}" || echo "Can't start packager automatically"
fi
fi

View File

@@ -16,11 +16,8 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import androidx.annotation.RequiresApi;
import java.util.HashSet;
import java.util.Set;
@@ -34,7 +31,6 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
* default it's only used on versions < O, since versions >= O use ConnectionService, but it
* can be disabled.
*/
@RequiresApi(Build.VERSION_CODES.M)
class AudioDeviceHandlerGeneric implements
AudioModeModule.AudioDeviceHandlerInterface,
AudioManager.OnAudioFocusChangeListener {

View File

@@ -1,230 +0,0 @@
/*
* Copyright @ 2017-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 android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* {@link AudioModeModule.AudioDeviceHandlerInterface} module implementing device handling for
* legacy (pre-M) Android versions.
*/
class AudioDeviceHandlerLegacy implements
AudioModeModule.AudioDeviceHandlerInterface,
AudioManager.OnAudioFocusChangeListener,
BluetoothHeadsetMonitor.Listener {
private final static String TAG = AudioDeviceHandlerLegacy.class.getSimpleName();
/**
* Reference to the main {@code AudioModeModule}.
*/
private AudioModeModule module;
/**
* Indicator that we have lost audio focus.
*/
private boolean audioFocusLost = false;
/**
* {@link AudioManager} instance used to interact with the Android audio
* subsystem.
*/
private AudioManager audioManager;
/**
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
* old (< M) Android versions.
*/
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
public AudioDeviceHandlerLegacy(AudioManager audioManager) {
this.audioManager = audioManager;
}
/**
* Helper method to trigger an audio route update when Bluetooth devices are
* connected / disconnected.
*/
@Override
public void onBluetoothDeviceChange(final boolean deviceAvailable) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
if (deviceAvailable) {
module.addDevice(AudioModeModule.DEVICE_BLUETOOTH);
} else {
module.removeDevice(AudioModeModule.DEVICE_BLUETOOTH);
}
module.updateAudioRoute();
}
});
}
/**
* Helper method to trigger an audio route update when a headset is plugged
* or unplugged.
*/
private void onHeadsetDeviceChange() {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
// XXX: isWiredHeadsetOn is not deprecated when used just for
// knowing if there is a wired headset connected, regardless of
// audio being routed to it.
//noinspection deprecation
if (audioManager.isWiredHeadsetOn()) {
module.addDevice(AudioModeModule.DEVICE_HEADPHONES);
} else {
module.removeDevice(AudioModeModule.DEVICE_HEADPHONES);
}
module.updateAudioRoute();
}
});
}
/**
* {@link AudioManager.OnAudioFocusChangeListener} interface method. Called
* when the audio focus of the system is updated.
*
* @param focusChange - The type of focus change.
*/
@Override
public void onAudioFocusChange(final int focusChange) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN: {
JitsiMeetLogger.d(TAG + " Audio focus gained");
// Some other application potentially stole our audio focus
// temporarily. Restore our mode.
if (audioFocusLost) {
module.updateAudioRoute();
}
audioFocusLost = false;
break;
}
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
JitsiMeetLogger.d(TAG + " Audio focus lost");
audioFocusLost = true;
break;
}
}
}
});
}
/**
* Helper method to set the output route to a Bluetooth device.
*
* @param enabled true if Bluetooth should use used, false otherwise.
*/
private void setBluetoothAudioRoute(boolean enabled) {
if (enabled) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
}
@Override
public void start(AudioModeModule audioModeModule) {
JitsiMeetLogger.i("Using " + TAG + " as the audio device handler");
module = audioModeModule;
Context context = module.getContext();
// Setup runtime device change detection.
//
// Detect changes in wired headset connections.
IntentFilter wiredHeadSetFilter = new IntentFilter(AudioManager.ACTION_HEADSET_PLUG);
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
JitsiMeetLogger.d(TAG + " Wired headset added / removed");
onHeadsetDeviceChange();
}
};
context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
// Detect Bluetooth device changes.
bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(context, this);
// On Android < M, detect if we have an earpiece.
PackageManager pm = context.getPackageManager();
if (pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
module.addDevice(AudioModeModule.DEVICE_EARPIECE);
}
// Always assume there is a speaker.
module.addDevice(AudioModeModule.DEVICE_SPEAKER);
}
@Override
public void stop() {
bluetoothHeadsetMonitor.stop();
bluetoothHeadsetMonitor = null;
}
@Override
public void setAudioRoute(String device) {
// Turn speaker on / off
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
}
@Override
public boolean setMode(int mode) {
if (mode == AudioModeModule.DEFAULT) {
audioFocusLost = false;
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(this);
audioManager.setSpeakerphoneOn(false);
setBluetoothAudioRoute(false);
return true;
}
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setMicrophoneMute(false);
if (audioManager.requestAudioFocus(this, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
JitsiMeetLogger.w(TAG + " Audio focus request failed");
return false;
}
return true;
}
}

View File

@@ -222,10 +222,8 @@ class AudioModeModule extends ReactContextBaseJavaModule {
if (useConnectionService()) {
audioDeviceHandler = new AudioDeviceHandlerConnectionService(audioManager);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
audioDeviceHandler = new AudioDeviceHandlerGeneric(audioManager);
} else {
audioDeviceHandler = new AudioDeviceHandlerLegacy(audioManager);
audioDeviceHandler = new AudioDeviceHandlerGeneric(audioManager);
}
audioDeviceHandler.start(this);
@@ -427,15 +425,6 @@ class AudioModeModule extends ReactContextBaseJavaModule {
}
}
/**
* Needed on the legacy handler...
*
* @return Context for the application.
*/
Context getContext() {
return getReactApplicationContext();
}
/**
* Interface for the modules implementing the actual audio device management.
*/

View File

@@ -1,191 +0,0 @@
/*
* Copyright @ 2017-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 android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* Helper class to detect and handle Bluetooth device changes. It monitors
* Bluetooth headsets being connected / disconnected and notifies the module
* about device changes when this occurs.
*/
class BluetoothHeadsetMonitor {
private final static String TAG = BluetoothHeadsetMonitor.class.getSimpleName();
/**
* The {@link Context} in which this module executes.
*/
private final Context context;
/**
* Reference to the {@link BluetoothAdapter} object, used to access Bluetooth functionality.
*/
private BluetoothAdapter adapter;
/**
* Reference to a proxy object which allows us to query connected devices.
*/
private BluetoothHeadset headset;
/**
* receiver registered for receiving Bluetooth connection state changes.
*/
private BroadcastReceiver receiver;
/**
* Listener for receiving Bluetooth device change events.
*/
private Listener listener;
public BluetoothHeadsetMonitor(Context context, Listener listener) {
this.context = context;
this.listener = listener;
}
private boolean getBluetoothHeadsetProfileProxy() {
adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
JitsiMeetLogger.w(TAG + " Device doesn't support Bluetooth");
return false;
}
// XXX: The profile listener listens for system services of the given
// type being available to the application. That is, if our Bluetooth
// adapter has the "headset" profile.
BluetoothProfile.ServiceListener listener
= new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.HEADSET) {
headset = (BluetoothHeadset) proxy;
updateDevices();
}
}
@Override
public void onServiceDisconnected(int profile) {
// The logic is the same as the logic of onServiceConnected.
onServiceConnected(profile, /* proxy */ null);
}
};
return adapter.getProfileProxy(context, listener, BluetoothProfile.HEADSET);
}
private void onBluetoothReceiverReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
// XXX: This action will be fired when a Bluetooth headset is
// connected or disconnected to the system. This is not related to
// audio routing.
int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99);
switch (state) {
case BluetoothHeadset.STATE_CONNECTED:
case BluetoothHeadset.STATE_DISCONNECTED:
JitsiMeetLogger.d(TAG + " BT headset connection state changed: " + state);
updateDevices();
break;
}
} else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) {
// XXX: This action will be fired when the connection established
// with a Bluetooth headset (called a SCO connection) changes state.
// When the SCO connection is active we route audio to it.
int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
switch (state) {
case AudioManager.SCO_AUDIO_STATE_CONNECTED:
case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
JitsiMeetLogger.d(TAG + " BT SCO connection state changed: " + state);
updateDevices();
break;
}
}
}
private void registerBluetoothReceiver() {
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onBluetoothReceiverReceive(context, intent);
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
context.registerReceiver(receiver, filter);
}
/**
* Detects if there are new devices connected / disconnected and fires the
* {@link Listener} registered event.
*/
private void updateDevices() {
boolean headsetAvailable = (headset != null) && !headset.getConnectedDevices().isEmpty();
listener.onBluetoothDeviceChange(headsetAvailable);
}
public void start() {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (!audioManager.isBluetoothScoAvailableOffCall()) {
JitsiMeetLogger.w(TAG + " Bluetooth SCO is not available");
return;
}
if (!getBluetoothHeadsetProfileProxy()) {
JitsiMeetLogger.w(TAG + " Couldn't get BT profile proxy");
return;
}
registerBluetoothReceiver();
// Initial detection.
updateDevices();
}
public void stop() {
if (receiver != null) {
context.unregisterReceiver(receiver);
}
if (adapter != null && headset != null) {
adapter.closeProfileProxy(BluetoothProfile.HEADSET, headset);
}
receiver = null;
headset = null;
adapter = null;
}
interface Listener {
void onBluetoothDeviceChange(boolean deviceAvailable);
}
}

View File

@@ -1,6 +1,5 @@
/*
* Copyright @ 2019-present 8x8, Inc.
* Copyright @ 2018 Atlassian Pty Ltd
* Copyright @ 2018-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.
@@ -17,10 +16,8 @@
package org.jitsi.meet.sdk;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Callback;
@@ -178,7 +175,6 @@ public class JitsiMeetActivityDelegate {
};
}
@TargetApi(Build.VERSION_CODES.M)
public static void requestPermissions(Activity activity, String[] permissions, int requestCode, PermissionListener listener) {
permissionListener = listener;
activity.requestPermissions(permissions, requestCode);

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-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.
@@ -33,21 +33,10 @@ import com.facebook.react.module.annotations.ReactModule;
* is used with the conference audio-only mode.
*/
@ReactModule(name = ProximityModule.NAME)
class ProximityModule
extends ReactContextBaseJavaModule {
class ProximityModule extends ReactContextBaseJavaModule {
public static final String NAME = "Proximity";
/**
* This type of wake lock (the one activated by the proximity sensor) has
* been available for a while, but the constant was only exported in API
* level 21 (Android Marshmallow) so make no assumptions and use its value
* directly.
*
* TODO: Remove when we bump the API level to 21.
*/
private static final int PROXIMITY_SCREEN_OFF_WAKE_LOCK = 32;
/**
* {@link WakeLock} instance.
*/
@@ -71,7 +60,7 @@ class ProximityModule
try {
wakeLock
= powerManager.newWakeLock(
PROXIMITY_SCREEN_OFF_WAKE_LOCK,
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
"jitsi:"+NAME);
} catch (Throwable ignored) {
wakeLock = null;

View File

@@ -1588,10 +1588,6 @@ export default {
if (didHaveVideo) {
promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] }))
.then(([ stream ]) => this.useVideoStream(stream))
.then(() => {
sendAnalytics(createScreenSharingEvent('stopped'));
logger.log('Screen sharing stopped.');
})
.catch(error => {
logger.error('failed to switch back to local video', error);
@@ -1608,6 +1604,8 @@ export default {
return promise.then(
() => {
this.videoSwitchInProgress = false;
sendAnalytics(createScreenSharingEvent('stopped'));
logger.info('Screen sharing stopped.');
},
error => {
this.videoSwitchInProgress = false;

View File

@@ -123,6 +123,10 @@ var config = {
// Sets the preferred resolution (height) for local video. Defaults to 720.
// resolution: 720,
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
// Use -1 to disable.
// maxFullResolutionParticipants: 2
// w3c spec-compliant video constraints to use for video capture. Currently
// used by browsers that return true from lib-jitsi-meet's
// util#browser#usesNewGumFlow. The constraints are independent from
@@ -212,6 +216,37 @@ var config = {
// Default value for the channel "last N" attribute. -1 for unlimited.
channelLastN: -1,
// Provides a way to use different "last N" values based on the number of participants in the conference.
// The keys in an Object represent number of participants and the values are "last N" to be used when number of
// participants gets to or above the number.
//
// For the given example mapping, "last N" will be set to 20 as long as there are at least 5, but less than
// 29 participants in the call and it will be lowered to 15 when the 30th participant joins. The 'channelLastN'
// will be used as default until the first threshold is reached.
//
// lastNLimits: {
// 5: 20,
// 30: 15,
// 50: 10,
// 70: 5,
// 90: 2
// },
// Specify the settings for video quality optimizations on the client.
// videoQuality: {
//
// // Provides a way to configure the maximum bitrates that will be enforced on the simulcast streams for
// // video tracks. The keys in the object represent the type of the stream (LD, SD or HD) and the values
// // are the max.bitrates to be set on that particular type of stream. The actual send may vary based on
// // the available bandwidth calculated by the browser, but it will be capped by the values specified here.
// // This is currently not implemented on app based clients on mobile.
// maxBitratesVideo: {
// low: 200000,
// standard: 500000,
// high: 1500000
// }
// },
// // Options for the recording limit notification.
// recordingLimit: {
//

74
css/_notifications.scss Normal file
View File

@@ -0,0 +1,74 @@
@include keyframes(exiting) {
from {
opacity: 1;
transform: translate(0, 0);
}
to {
opacity: 0;
transform: translate(-200px, 0);
}
}
.notificationsContainer {
display: flex;
position: absolute;
bottom: 0px;
flex-direction: column;
width: 400px;
left: 80px;
.topContainer {
padding-bottom: 16px;
// transition: height 0.4s ease-in-out;
.notification {
width: 400px;
z-index: 5;
&:nth-child(n+2) {
margin-top: 16px;
}
&.exiting {
animation-name: exiting;
animation-duration: 0.4s;
}
}
}
.bottomContainer {
margin-bottom: 64px;
position: relative;
height: 100%;
.notification {
width: 400px;
bottom: 0px;
&:nth-child(1) {
z-index: 5;
}
&:nth-child(n+2) {
transition: transform 0.4s ease-in-out;
z-index: 4;
position: absolute;
transform: translateY(100%) translateY(16px);
}
&:nth-child(n+4) {
display: none;
}
&.exiting {
animation-name: exiting;
animation-duration: 0.4s;
}
&.exiting+.notification {
transform: translateY(0);
}
}
}
}

View File

@@ -115,3 +115,19 @@
font-size: 12px;
}
}
.jibri-queue-info {
display: flex;
flex-flow: column;
.footer {
background: #a4b8a4D1;
border-radius: 3px;
font-weight: bold;
color: #5e6d7a;
padding: 5px;
margin-right: 30px;
margin-top: 5px;
}
}

View File

@@ -164,6 +164,9 @@ $unsupportedDesktopBrowserTextFontSize: 21px;
$watermarkWidth: 186px;
$watermarkHeight: 74px;
$welcomePageWatermarkWidth: 186px;
$welcomePageWatermarkHeight: 74px;
/**
* Welcome page variables.
*/
@@ -178,9 +181,12 @@ $welcomePageHeaderBackgroundPosition: none;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: none;
$welcomePageHeaderPaddingBottom: 0px;
$welcomePageHeaderMinHeight: fit-content;
$welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextDisplay: flex;
$welcomePageHeaderTextWidth: 650px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextTitleFontSize: 2.5rem;
@@ -195,6 +201,7 @@ $welcomePageHeaderTextDescriptionLineHeight: 24px;
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
$welcomePageEnterRoomDisplay: flex;
$welcomePageEnterRoomWidth: 680px;
$welcomePageEnterRoomPadding: 25px 30px;
$welcomePageEnterRoomBorderRadius: 0px;

View File

@@ -21,18 +21,18 @@ body.welcome-page {
align-items: center;
display: flex;
flex-direction: column;
min-height: fit-content;
min-height: $welcomePageHeaderMinHeight;
overflow: hidden;
position: relative;
text-align: center;
.header-text {
display: flex;
display: $welcomePageHeaderTextDisplay;
flex-direction: column;
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
margin-bottom: $welcomePageHeaderTextMarginBottom;
max-width: calc(100% - 40px);
width: 650px;
width: $welcomePageHeaderTextWidth;
z-index: $zindex2;
}
@@ -56,7 +56,7 @@ body.welcome-page {
}
#enter_room {
display: flex;
display: $welcomePageEnterRoomDisplay;
align-items: center;
max-width: calc(100% - 40px);
width: $welcomePageEnterRoomWidth;
@@ -211,5 +211,10 @@ body.welcome-page {
position: absolute;
width: 100%;
height: 100%;
.watermark.leftwatermark {
width: $welcomePageWatermarkWidth;
height: $welcomePageWatermarkHeight;
}
}
}

View File

@@ -102,5 +102,6 @@ $flagsImagePath: "../images/";
@import 'premeeting-screens';
@import 'e2ee';
@import 'responsive';
@import 'notifications';
/* Modules END */

View File

@@ -48,6 +48,7 @@ var interfaceConfig = {
DEFAULT_LOCAL_DISPLAY_NAME: 'me',
DEFAULT_LOGO_URL: 'images/watermark.png',
DEFAULT_REMOTE_DISPLAY_NAME: 'Fellow Jitster',
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.png',
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
@@ -101,6 +102,11 @@ var interfaceConfig = {
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
/**
* Hide the logo on the deep linking pages.
*/
HIDE_DEEP_LINKING_LOGO: false,
/**
* Hide the invite prompt in the header when alone in the meeting.
*/

View File

@@ -0,0 +1,5 @@
#!/bin/bash
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
exec ${THIS_DIR}/../../node_modules/react-native/scripts/launchPackager.command --reset-cache

View File

@@ -3,6 +3,8 @@
# This script is executed from Xcode to start the React packager for Debug
# targets.
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../../node_modules/react-native/scripts/.packager.env"
@@ -13,7 +15,6 @@ if [[ "$CONFIGURATION" = "Debug" ]]; then
exit 2
fi
else
open -g "$SRCROOT/../../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
open -g "$THIS_DIR/run-packager-helper.command" || echo "Can't start packager automatically"
fi
fi

9
jest.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
moduleFileExtensions: [
'js'
],
testMatch: [
'<rootDir>/react/**/?(*.)+(test)?(.web).js?(x)'
],
verbose: true
};

View File

@@ -534,7 +534,7 @@
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"startAudioMuted": "Při připojení všem zlumit zvuk",
"startVideoMuted": "Všechny připojovat jako skrýté",
"startVideoMuted": "Všechny připojovat jako skryté",
"title": "Nastavení",
"speakers": "Reproduktory",
"microphones": "Mikrofony"
@@ -567,7 +567,7 @@
"name": "Řečník",
"seconds": "",
"speakerStats": "Statistika řečníků",
"speakerTime": "Mluvil již"
"speakerTime": "Mluvil(a) již"
},
"startupoverlay": {
"policyText": " ",
@@ -638,7 +638,7 @@
"openChat": "",
"pip": "",
"profile": "Upravit váš profil",
"raiseHand": "Příhlásit / Odhlásit se o slovo",
"raiseHand": "Přihlásit / Odhlásit se o slovo",
"raiseYourHand": "",
"Settings": "Nastavení",
"sharedvideo": "Sdílet obraz YouTube videa",

View File

@@ -3,14 +3,15 @@
"add": "Einladen",
"addContacts": "Laden Sie Ihre Kontakte ein",
"copyInvite": "Sitzungseinladung kopieren",
"copyLink": "Meeting-Link kopieren",
"copyStream": "Live-Streaming-Link kopieren",
"copyLink": "Konferenzlink kopieren",
"copyStream": "Livestreaminglink kopieren",
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
"defaultEmail": "Ihre Standard-E-Mail",
"disabled": "Sie können keine Teilnehmer einladen.",
"failedToAdd": "Fehler beim Hinzufügen von Teilnehmern",
"footerText": "Abgehender Ruf ist deaktiviert.",
"googleEmail": "Google-E-Mail",
"inviteMoreHeader": "Sie sind alleine in der Sitzung",
"inviteMoreMailSubject": "An {{appName}} Meeting teilnehmen",
"inviteMorePrompt": "Mehr Leute einladen",
@@ -20,14 +21,16 @@
"loadingPeople": "Suche nach einzuladenden Teilnehmern",
"noResults": "Keine passenden Ergebnisse",
"noValidNumbers": "Telefonnummer eingeben",
"outlookEmail": "Outlook-E-Mail",
"searchNumbers": "Telefonnummern hinzufügen",
"searchPeople": "Nach Teilnehmern suchen",
"searchPeopleAndNumbers": "Nach Teilnehmen suchen oder deren Telefonnummern hinzufügen",
"shareInvite": "Einladung zur Versammlung teilen",
"shareLink": "Teilen Sie den Meeting-Link, um andere einzuladen",
"shareStream": "Den Live-Streaming-Link freigeben",
"shareLink": "Teilen Sie den Konferenzlink, um andere einzuladen",
"shareStream": "Den Livestreaminglink freigeben",
"telephone": "Telefon: {{number}}",
"title": "Teilnehmer zu dieser Konferenz einladen"
"title": "Teilnehmer zu dieser Konferenz einladen",
"yahooEmail": "Yahoo-E-Mail"
},
"audioDevices": {
"bluetooth": "Bluetooth",
@@ -40,7 +43,7 @@
"audioOnly": "Geringe Bandbreite"
},
"calendarSync": {
"addMeetingURL": "Meeting-Link hinzufügen",
"addMeetingURL": "Konferenzlink hinzufügen",
"confirmAddLink": "Möchten Sie einen Jitsi-Link zu diesem Termin hinzufügen?",
"error": {
"appConfiguration": "Kalenderintegration ist nicht richtig konfiguriert.",
@@ -89,9 +92,9 @@
"DISCONNECTED": "Getrennt",
"DISCONNECTING": "Verbindung wird getrennt",
"ERROR": "Fehler",
"FETCH_SESSION_ID": "Sitzungs-ID erhalten...",
"FETCH_SESSION_ID": "Sitzungs-ID abrufen ",
"GET_SESSION_ID_ERROR": "Sitzungs-ID-Fehler erhalten: {{code}}",
"GOT_SESSION_ID": "Sitzungs-ID erhalten... Beendet",
"GOT_SESSION_ID": "Sitzungs-ID abrufen … beendet",
"LOW_BANDWIDTH": "Video für {{displayName}} wurde ausgeschaltet, um Bandbreite einzusparen"
},
"connectionindicator": {
@@ -107,6 +110,7 @@
"localaddress_plural": "Lokale Adressen:",
"localport": "Lokaler Port:",
"localport_plural": "Lokale Ports:",
"maxEnabledResolution": "max. senden",
"more": "Mehr anzeigen",
"packetloss": "Paketverlust:",
"quality": {
@@ -139,7 +143,7 @@
"ifHaveApp": "Wenn Sie die App bereits haben:",
"joinInApp": "An dem Meeting teilnehmen mit der App",
"launchWebButton": "Im Web öffnen",
"title": "Die Konferenz wird in {{app}} geöffnet...",
"title": "Die Konferenz wird in {{app}} geöffnet …",
"tryAgainButton": "Erneut mit der nativen Applikation versuchen"
},
"defaultLink": "Bsp.: {{url}}",
@@ -160,6 +164,7 @@
"accessibilityLabel": {
"liveStreaming": "Livestream"
},
"add": "Hinzufügen",
"allow": "Erlauben",
"alreadySharedVideoMsg": "Ein anderer Teilnehmer gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
"alreadySharedVideoTitle": "Nur ein geteiltes Video gleichzeitig",
@@ -174,9 +179,9 @@
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
"Cancel": "Abbrechen",
"close": "Schließen",
"conferenceDisconnectMsg": "Prüfen Sie allenfalls Ihre Netzwerkverbindung. Verbinde in {{seconds}} Sekunden...",
"conferenceDisconnectMsg": "Prüfen Sie allenfalls Ihre Netzwerkverbindung. Verbinde in {{seconds}} Sekunden …",
"conferenceDisconnectTitle": "Ihre Verbindung ist getrennt worden.",
"conferenceReloadMsg": "Wir versuchen das zu beheben. Verbinde in {{seconds}} Sekunden...",
"conferenceReloadMsg": "Wir versuchen das zu beheben. Verbinde in {{seconds}} Sekunden …",
"conferenceReloadTitle": "Leider ist etwas schiefgegangen.",
"confirm": "Bestätigen",
"confirmNo": "Nein",
@@ -185,21 +190,25 @@
"connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: {{msg}}",
"connecting": "Verbindung wird hergestellt",
"contactSupport": "Support kontaktieren",
"copied": "Kopiert",
"copy": "Kopieren",
"dismiss": "OK",
"displayNameRequired": "Hallo! Wie ist Ihr Name?",
"done": "Fertig",
"e2eeDescription": "Ende-zu-Ende-Verschlüsselung ist derzeit noch EXPERIMENTELL. Bitte beachten Sie, dass das Aktivieren der Ende-zu-Ende-Verschlüsselung diverse serverseitige Funktionen deaktiviert: Aufnahmen, Livestreaming und Telefoneinwahl. Bitte beachten Sie außerdem, dass der Konferenz dann nur noch mit Browsern beigetreten werden kann, die Insertable Streams unterstützen.",
"e2eeLabel": "E2EE-Schlüssel",
"e2eeNoKey": "Keiner",
"e2eeToggleSet": "Schlüssel festlegen",
"e2eeSet": "Setzen",
"e2eeWarning": "WARNUNG: Nicht alle Teilnehmer dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Teilnehmer nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"error": "Fehler",
"externalInstallationMsg": "Die Bildschirmfreigabe-Erweiterung muss installiert werden.",
"externalInstallationTitle": "Erweiterung erforderlich",
"goToStore": "Zum Store",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"grantModeratorDialog": "Möchten Sie diesen Teilnehmer wirklich zum Moderator machen?",
"grantModeratorTitle": "Zum Moderator machen",
"IamHost": "Ich bin der Organisator",
"incorrectRoomLockPassword": "Falsches Passwort",
"incorrectPassword": "Benutzername oder Passwort ungültig",
"inlineInstallationMsg": "Die Bildschirmfreigabe-Erweiterung muss installiert werden.",
"inlineInstallExtension": "Jetzt installieren",
"internalError": "Oh! Es hat etwas nicht funktioniert. Der folgende Fehler ist aufgetreten: {{error}}",
"internalErrorTitle": "Interner Fehler",
"kickMessage": "Sie können sich für mehr Details an {{participantDisplayName}} wenden.",
@@ -208,10 +217,11 @@
"kickParticipantTitle": "Teilnehmer entfernen?",
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus dem Meeting geworfen",
"liveStreaming": "Livestreaming",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Während einer Aufnahme nicht möglich",
"liveStreamingDisabledForGuestTooltip": "Gäste können kein Livestreaming starten.",
"liveStreamingDisabledTooltip": "Starten des Livestreams deaktiviert.",
"lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
"lockRoom": "Meeting-$t(lockRoomPasswordUppercase) hinzufügen",
"lockRoom": "Konferenz$t(lockRoomPassword) hinzufügen",
"lockTitle": "Sperren fehlgeschlagen",
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
"logoutTitle": "Abmelden",
@@ -234,13 +244,15 @@
"muteParticipantDialog": "Wollen Sie diesen Teilnehmer wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, der Teilnehmer kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Teilnehmer stummschalten?",
"Ok": "OK",
"passwordLabel": "Dieses Meeting wurde von einem Teilnehmer gesichert. Bitte geben Sie das $t(lockRoomPassword) ein, um dem Meeting beizutreten.",
"passwordNotSupported": "Das Festlegen von einem $t(lockRoomPassword) für das Meeting wird nicht unterstützt.",
"passwordLabel": "Dieses Meeting wurde von einem Teilnehmer gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
"popupError": "Ihr Browser blockiert Pop-ups von dieser Website. Bitte aktivieren Sie Pop-ups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
"popupErrorTitle": "Pop-up blockiert",
"readMore": "mehr",
"recording": "Aufnahme",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Während eines Livestreams nicht möglich",
"recordingDisabledForGuestTooltip": "Gäste können keine Aufzeichnungen starten.",
"recordingDisabledTooltip": "Start der Aufzeichnung deaktiviert.",
"rejoinNow": "Jetzt erneut beitreten",
@@ -252,17 +264,16 @@
"remoteControlStopMessage": "Die Fernsteuerung wurde beendet!",
"remoteControlTitle": "Fernsteuerung",
"Remove": "Entfernen",
"removePassword": "$t(lockRoomPassword) entfernen",
"removePassword": "$t(lockRoomPasswordUppercase) entfernen",
"removeSharedVideoMsg": "Sind Sie sicher, dass Sie das geteilte Video entfernen möchten?",
"removeSharedVideoTitle": "Freigegebenes Video entfernen",
"reservationError": "Fehler im Reservierungssystem",
"reservationErrorMsg": "Fehler, Nummer: {{code}}, Nachricht: {{msg}}",
"retry": "Wiederholen",
"screenSharingFailedToInstall": "Oh! Die Erweiterung für die Bildschirmfreigabe konnte nicht installiert werden.",
"screenSharingFailedToInstallTitle": "Bildschirmfreigabe-Erweiterung konnte nicht installiert werden",
"screenSharingFirefoxPermissionDeniedError": "Die Bildschirmfreigabe ist leider fehlgeschlagen. Bitte stellen Sie sicher, dass die Berechtigung für die Bildschirmfreigabe im Browser erteilt wurde. ",
"screenSharingFirefoxPermissionDeniedTitle": "Die Bildschirmfreigabe konnte nicht gestartet werden!",
"screenSharingPermissionDeniedError": "Oh! Beim Anfordern der Bildschirmfreigabe-Berechtigungen hat etwas nicht funktioniert. Bitte aktualisieren und erneut versuchen.",
"screenSharingAudio": "Audio teilen",
"screenSharingFailed": "Ups! Beim Teilen des Bildschirms ist etwas schiefgegangen!",
"screenSharingFailedTitle": "Bildschirmfreigabe fehlgeschlagen!",
"screenSharingPermissionDeniedError": "Ups! Etwas stimmt nicht mit Ihren Berechtigungen zur Bildschirmfreigabe. Bitte neu laden und erneut versuchen.",
"sendPrivateMessage": "Sie haben kürzlich eine private Nachricht erhalten. Hatten Sie die Absicht, darauf privat zu antworten, oder wollen Sie Ihre Nachricht an die Gruppe senden?",
"sendPrivateMessageCancel": "An die Gruppe senden",
"sendPrivateMessageOk": "Privat antworten",
@@ -275,13 +286,13 @@
"shareYourScreen": "Bildschirm freigeben",
"shareYourScreenDisabled": "Bildschirmfreigabe deaktiviert.",
"shareYourScreenDisabledForGuest": "Gäste können den Bildschirm nicht freigeben.",
"startLiveStreaming": "Einen Livestream starten",
"startLiveStreaming": "Livestream starten",
"startRecording": "Aufnahme starten",
"startRemoteControlErrorMessage": "Beim Versuch, die Fernsteuerung zu starten, ist ein Fehler aufgetreten!",
"stopLiveStreaming": "Livestreaming stoppen",
"stopLiveStreaming": "Livestream stoppen",
"stopRecording": "Aufnahme stoppen",
"stopRecordingWarning": "Sind Sie sicher, dass Sie die Aufnahme stoppen möchten?",
"stopStreamingWarning": "Sind Sie sicher, dass Sie das Livestreaming stoppen möchten?",
"stopStreamingWarning": "Sind Sie sicher, dass Sie den Livestream stoppen möchten?",
"streamKey": "Streamschlüssel",
"Submit": "OK",
"thankYou": "Danke für die Verwendung von {{appName}}!",
@@ -289,14 +300,13 @@
"tokenAuthFailed": "Sie sind nicht berechtigt, dieser Konferenz beizutreten.",
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
"transcribing": "Wird transkribiert",
"unlockRoom": "Meeting-$t(lockRoomPassword) entfernen",
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
"userPassword": "Benutzerpasswort",
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Wenn Sie der Organisator sind, authentifizieren Sie sich. Warten Sie andernfalls, bis der Organisator erscheint.",
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Wenn Sie der Organisator sind, drücken Sie zum Authentifizieren auf OK. Warten Sie andernfalls, bis der Organisator erscheint.",
"WaitingForHost": "Warten auf den Organisator...",
"WaitingForHost": "Warten auf den Organisator …",
"Yes": "Ja",
"yourEntireScreen": "Ganzer Bildschirm",
"screenSharingAudio": "Audio austauschen"
"yourEntireScreen": "Ganzer Bildschirm"
},
"dialOut": {
"statusMessage": "ist jetzt {{status}}"
@@ -304,6 +314,12 @@
"documentSharing": {
"title": "Freigegebenes Dokument"
},
"e2ee": {
"labelToolTip": "Audio- und Videodaten dieser Unterhaltung sind jetzt zwischen den Teilnehmern verschlüsselt"
},
"embedMeeting": {
"title": "Diese Konferenz einbetten"
},
"feedback": {
"average": "Durchschnittlich",
"bad": "Schlecht",
@@ -322,8 +338,8 @@
},
"info": {
"accessibilityLabel": "Informationen anzeigen",
"addPassword": "$t(lockRoomPassword) hinzufügen",
"cancelPassword": "$t(lockRoomPassword) löschen",
"addPassword": "$t(lockRoomPasswordUppercase) hinzufügen",
"cancelPassword": "$t(lockRoomPasswordUppercase) löschen",
"conferenceURL": "Link:",
"country": "Land",
"dialANumber": "Um am Meeting teilzunehmen, müssen Sie eine dieser Nummern wählen und dann die PIN eingeben.",
@@ -335,7 +351,7 @@
"genericError": "Es ist leider etwas schiefgegangen.",
"inviteLiveStream": "Klicken Sie auf {{url}}, um den Livestream dieser Konferenz zu öffnen",
"invitePhone": "Wenn Sie stattdessen per Telefon beitreten möchten, wählen sie: {{number}},,{{conferenceID}}#\n",
"invitePhoneAlternatives": "Suchen Sie nach einer anderen Einwahlnummer ?\nMeeting-Einwahlnummern anzeigen: {{url}}\n\n\nWenn Sie sich auch über ein Raumtelefon einwählen, nehmen Sie teil, ohne sich mit dem Ton zu verbinden: {{silentUrl}}",
"invitePhoneAlternatives": "Suchen Sie nach einer anderen Einwahlnummer ?\nEinwahlnummern der Konferenz anzeigen: {{url}}\n\n\nWenn Sie sich auch über ein Raumtelefon einwählen, nehmen Sie teil, ohne sich mit dem Ton zu verbinden: {{silentUrl}}",
"inviteURLFirstPartGeneral": "Sie wurden zur Teilnahme an einem Meeting eingeladen.",
"inviteURLFirstPartPersonal": "{{name}} lädt Sie zu einem Meeting ein.\n",
"inviteURLSecondPart": "\nAm Meeting teilnehmen:\n{{url}}\n",
@@ -348,7 +364,7 @@
"password": "$t(lockRoomPasswordUppercase):",
"title": "Teilen",
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
"label": "Meeting-Informationen"
"label": "Konferenzinformationen"
},
"inviteDialog": {
"alertText": "Die Einladung einiger Teilnehmer ist fehlgeschlagen.",
@@ -382,6 +398,8 @@
"videoQuality": "Anrufqualität verwalten"
},
"liveStreaming": {
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihr Stream auf {{limit}} min. begrenzt. Für unlimitiertes Streaming nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Ihr Stream ist begrenzt auf {{limit}} min. Für unlimitiertes Streaming, nutzen Sie bitte {{app}}.",
"busy": "Es werden Ressourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
"changeSignIn": "Konten wechseln.",
@@ -391,17 +409,17 @@
"error": "Das Livestreaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"errorAPI": "Beim Abrufen der YouTube-Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie, sich erneut anzumelden.",
"errorLiveStreamNotEnabled": "Livestreaming ist für {{email}} nicht aktiviert. Aktivieren Sie das Livestreaming oder melden Sie sich bei einem Konto mit aktiviertem Livestreaming an.",
"expandedOff": "Livestreaming wurde angehalten",
"expandedOff": "Livestream wurde angehalten",
"expandedOn": "Das Meeting wird momentan an YouTube gestreamt.",
"expandedPending": "Livestreaming wird gestartet...",
"failedToStart": "Livestreaming konnte nicht gestartet werden",
"expandedPending": "Livestream wird gestartet …",
"failedToStart": "Livestream konnte nicht gestartet werden",
"getStreamKeyManually": "Wir waren nicht in der Lage, Livestreams abzurufen. Versuchen Sie, Ihren Livestream-Schlüssel von YouTube zu erhalten.",
"invalidStreamKey": "Der Livestream-Schlüssel ist u. U. falsch.",
"off": "Livestreaming gestoppt",
"offBy": "{{name}} stoppte das Livestreaming",
"on": "Livestreaming",
"onBy": "{{name}} startete das Livestreaming",
"pending": "Livestream wird gestartet...",
"off": "Livestream gestoppt",
"offBy": "{{name}} stoppte den Livestream",
"on": "Livestream",
"onBy": "{{name}} startete den Livestream",
"pending": "Livestream wird gestartet …",
"serviceName": "Livestreaming-Dienst",
"signedInAs": "Sie sind derzeit angemeldet als:",
"signIn": "Mit Google anmelden",
@@ -442,7 +460,7 @@
"stop": "Aufnahme stoppen",
"yes": "Ja"
},
"lockRoomPassword": "Passwort",
"lockRoomPassword": "passwort",
"lockRoomPasswordUppercase": "Passwort",
"me": "ich",
"notify": {
@@ -474,11 +492,51 @@
"unmute": "Stummschaltung aufheben",
"newDeviceCameraTitle": "Neue Kamera erkannt",
"newDeviceAudioTitle": "Neues Audiogerät erkannt",
"newDeviceAction": "Verwenden"
"newDeviceAction": "Verwenden",
"OldElectronAPPTitle": "Sicherheitslücke!",
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
"oldElectronClientDescription2": "aktuelle Version",
"oldElectronClientDescription3": "!"
},
"passwordSetRemotely": "von einem anderen Teilnehmer gesetzt",
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
"poweredby": "Betrieben von",
"prejoin": {
"audioAndVideoError": "Audio- und Videofehler:",
"audioOnlyError": "Audiofehler:",
"audioTrackError": "Audiotrack konnte nicht erstellt werden.",
"calling": "Rufaufbau",
"callMe": "Mich anrufen",
"callMeAtNumber": "Mich unter dieser Nummer anrufen:",
"configuringDevices": "Geräte werden eingerichtet …",
"connectedWithAudioQ": "Sie sind mit Audio verbunden?",
"copyAndShare": "Konferenzlink kopieren & teilen",
"dialInMeeting": "Telefoneinwahl",
"dialInPin": "In die Konferenz einwählen und PIN eingeben:",
"dialing": "Wählen",
"doNotShow": "Nicht mehr anzeigen",
"errorDialOut": "Anruf fehlgeschlagen",
"errorDialOutDisconnected": "Anruf fehlgeschlagen. Verbindungsabbruch",
"errorDialOutFailed": "Anruf fehlgeschlagen. Anruf fehlgeschlagen",
"errorDialOutStatus": "Fehler beim Abrufen des Anrufstatus",
"errorStatusCode": "Anruf fehlgeschlagen. Statuscode: {{status}}",
"errorValidation": "Nummerverifikation fehlgeschlagen",
"iWantToDialIn": "Ich möchte mich einwählen",
"joinAudioByPhone": "Per Telefon teilnehmen",
"joinMeeting": "Konferenz beitreten",
"joinWithoutAudio": "Ohne Ton beitreten",
"initiated": "Anruf gestartet",
"linkCopied": "Link in die Zwischenablage kopiert",
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
"or": "oder",
"premeeting": "Vorraum",
"showScreen": "Konferenzvorraum aktivieren",
"startWithPhone": "Mit Telefonaudio starten",
"screenSharingError": "Fehler bei Bildschirmfreigabe:",
"videoOnlyError": "Videofehler:",
"videoTrackError": "Videotrack konnte nicht erstellt werden.",
"viewAllNumbers": "alle Nummern anzeigen"
},
"presenceStatus": {
"busy": "Beschäftigt",
"calling": "Wird angerufen …",
@@ -501,6 +559,8 @@
},
"raisedHand": "Ich möchte sprechen",
"recording": {
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} min begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
"authDropboxText": "In Dropbox hochladen",
"availableSpace": "Verfügbarer Speicherplatz: {{spaceLeft}} MB (ca. {{duration}} Minuten Aufzeichnung)",
"beta": "BETA",
@@ -511,7 +571,7 @@
"expandedOn": "Das Meeting wird momentan aufgezeichnet.",
"expandedPending": "Aufzeichnung wird gestartet…",
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
"fileSharingdescription": "Aufzeichnung mit Meeting-Teilnehmer teilen",
"fileSharingdescription": "Aufzeichnung mit Konferenzteilnehmer teilen",
"live": "LIVE",
"loggedIn": "Als {{userName}} angemeldet",
"off": "Aufnahme gestoppt",
@@ -531,7 +591,8 @@
"pullToRefresh": "Ziehen, um zu aktualisieren"
},
"security": {
"about": "Sie können einen Passwort zu Ihrer Sitzung hinzufügen. Die Teilnehmer müssen dieses ebenfalls eingeben, bevor sie an der Sitzung teilnehmen dürfen",
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"aboutReadOnly": "Moderatoren können die Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten",
"securityOptions": "Sicherheitsoptionen"
},
@@ -563,12 +624,15 @@
"settingsView": {
"advanced": "Erweitert",
"alertOk": "OK",
"alertCancel": "Abbrechen",
"alertTitle": "Warnung",
"alertURLText": "Die angegebene Server-URL ist ungültig",
"buildInfoSection": "Build-Informationen",
"conferenceSection": "Konferenz",
"disableCallIntegration": "Native Anrufintegration deaktivieren",
"disableP2P": "Ende-zu-Ende-Modus deaktivieren",
"disableCrashReporting": "Absturzberichte deaktivieren",
"disableCrashReportingWarning": "Möchten Sie die Absturzberichte wirklich deaktivieren? Diese Einstellung wird nach einem Neustart der App wirksam.",
"displayName": "Anzeigename",
"email": "E-Mail",
"header": "Einstellungen",
@@ -610,14 +674,18 @@
"chat": "Chatfenster ein-/ausblenden",
"document": "Geteiltes Dokument schließen",
"download": "Unsere Apps herunterladen",
"embedMeeting": "Konferenz einbetten",
"e2ee": "Ende-zu-Ende-Verschlüsselung",
"feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus aktivieren/deaktivieren",
"fullScreen": "Vollbildmodus ein-/ausschalten",
"grantModerator": "Zum Moderator machen",
"hangup": "Anruf beenden",
"help": "Hilfe",
"invite": "Teilnehmer einladen",
"kick": "Teilnehmer entfernen",
"lobbyButton": "Lobbymodus ein-/ausschalten",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
"lockRoom": "Meeting-Passwort ein-/auschalten",
"lockRoom": "Konferenzpasswort ein-/auschalten",
"moreActions": "Menü „Weitere Aktionen“ ein-/ausschalten",
"moreActionsMenu": "Menü „Weitere Aktionen“",
"moreOptions": "Menü „Weitere Optionen“",
@@ -639,6 +707,7 @@
"speakerStats": "Sprecherstatistik ein-/ausblenden",
"tileView": "Kachelansicht ein-/ausschalten",
"toggleCamera": "Kamera wechseln",
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
"videomute": "„Video stummschalten“ ein-/ausschalten",
"videoblur": "Video-Unschärfe ein-/ausschalten"
},
@@ -653,6 +722,8 @@
"documentClose": "Geteiltes Dokument schließen",
"documentOpen": "Geteiltes Dokument öffnen",
"download": "Unsere Apps herunterladen",
"e2ee": "Ende-zu-Ende-Verschlüsselung",
"embedMeeting": "Konferenz einbetten",
"enterFullScreen": "Vollbildmodus",
"enterTileView": "Kachelansicht einschalten",
"exitFullScreen": "Vollbildmodus verlassen",
@@ -661,6 +732,8 @@
"hangup": "Verlassen",
"help": "Hilfe",
"invite": "Teilnehmer einladen",
"lobbyButtonDisable": "Lobbymodus deaktivieren",
"lobbyButtonEnable": "Lobbymodus aktivieren",
"login": "Anmelden",
"logout": "Abmelden",
"lowerYourHand": "Hand senken",
@@ -755,6 +828,7 @@
"domute": "Stummschalten",
"domuteOthers": "Alle anderen stummschalten",
"flip": "Spiegeln",
"grantModerator": "Zum Moderator machen",
"kick": "Hinauswerfen",
"moderator": "Moderator",
"mute": "Teilnehmer ist stumm geschaltet",
@@ -768,7 +842,7 @@
"join": "Zum Teilnehmen tippen",
"roomname": "Konferenzname eingeben"
},
"appDescription": "Auf geht's! Starten Sie eine Videokonferenz mit dem ganzen Team. Oder besser noch: Laden Sie alle ein, die Sie kennen. {{app}} ist eine vollständig verschlüsselte, aus 100 % Open-Source-Software bestehende Videokonferenzlösung, die Sie den ganzen Tag kostenlos verwenden können ohne Registrierung.",
"appDescription": "Auf geht's! Starten Sie eine Videokonferenz mit ihrem Team oder besser noch: Laden Sie alle ein, die Sie kennen. {{app}} ist eine vollständig verschlüsselte und 100 % quelloffene Videokonferenzlösung, die Sie immer und überall kostenlos verwenden können ohne Registrierung.",
"audioVideoSwitch": {
"audio": "Audio",
"video": "Video"
@@ -777,24 +851,62 @@
"connectCalendarButton": "Kalender verbinden",
"connectCalendarText": "Verbinden Sie Ihren Kalender, um all Ihre Meetings in {{app}} anzuzeigen. Fügen Sie zudem {{provider}}-Meetings in Ihren Kalender ein und starten Sie sie mit nur einem Klick.",
"enterRoomTitle": "Neues Meeting starten",
"roomNameAllowedChars": "Der Meeting-Name sollte keines der folgenden Zeichen enthalten: ?, &, :, ', \", %, #.",
"getHelp": "Hilfe",
"go": "Los",
"goSmall": "Los",
"join": "Beitreten",
"info": "Informationen",
"join": "ERSTELLEN / BEITRETEN",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, unter der Sie der einzige Moderator sind.",
"privacy": "Datenschutz",
"recentList": "Letzte",
"recentList": "Verlauf",
"recentListDelete": "Löschen",
"recentListEmpty": "Die Liste „Letzte“ ist momentan leer. Chatten Sie mit Ihrem Team. Sie finden all Ihre letzten Meetings hier.",
"recentListEmpty": "Ihr Konferenzverlauf ist derzeit leer. Reden Sie mit Ihrem Team und Ihre vergangenen Konferenzen landen hier.",
"reducedUIText": "Willkommen bei {{app}}!",
"roomNameAllowedChars": "Der Konferenzname sollte keines der folgenden Zeichen enthalten: ?, &, :, ', \", %, #.",
"roomname": "Konferenzname eingeben",
"roomnameHint": "Name oder URL der Konferenz, der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Teilnehmern übermittelt werden, damit diese der gleichen Konferenz beitreten.",
"sendFeedback": "Feedback senden",
"terms": "AGB",
"title": "Sichere, mit umfassenden Funktionen ausgestattete und vollkommen kostenlose Videokonferenzen"
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen"
},
"lonelyMeetingExperience": {
"button": "Andere einladen",
"youAreAlone": "Nur Sie sind in diesem Meeting"
"youAreAlone": "Sie sind alleine in dieser Konferenz"
},
"helpView": {
"header": "Hilfecenter"
},
"lobby": {
"knockingParticipantList": "Liste anklopfender Teilnehmer",
"allow": "Annehmen",
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
"dialogTitle": "Lobbymodus",
"disableDialogContent": "Lobbymodus derzeit deaktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion aktivieren?",
"disableDialogSubmit": "Deaktivieren",
"emailField": "E-Mail-Adresse eingeben",
"enableDialogPasswordField": "Passwort setzen (optional)",
"enableDialogSubmit": "Aktivieren",
"enableDialogText": "Mit dem Lobbymodus schützen Sie Ihre Konferenz, da nur von einem Moderator angenommene Teilnehmer beitreten können.",
"enterPasswordButton": "Konferenzpasswort eingeben",
"enterPasswordTitle": "Passwort zum Beitreten benutzen",
"invalidPassword": "Ungültiges Passwort",
"joiningMessage": "Sie treten der Konferenz bei, sobald jemand Ihre Anfrage annimmt.",
"joinWithPasswordMessage": "Beitrittsversuch mit Passwort, bitte warten …",
"joinRejectedMessage": "Ihr Beitrittsanfrage wurde von einem Moderator abgelehnt.",
"joinTitle": "Konferenz beitreten",
"joiningTitle": "Beitritt anfragen …",
"joiningWithPasswordTitle": "Mit Passwort beitreten …",
"knockButton": "Beitritt anfragen",
"knockTitle": "Jemand möchte der Konferenz beitreten",
"nameField": "Geben Sie Ihren Namen ein",
"notificationLobbyAccessDenied": "{{targetParticipantName}} wurde von {{originParticipantName}} der Zutritt verwehrt",
"notificationLobbyAccessGranted": "{{targetParticipantName}} wurde von {{originParticipantName}} der Zutritt gestattet",
"notificationLobbyDisabled": "{{originParticipantName}} hat die Lobby deaktiviert",
"notificationLobbyEnabled": "{{originParticipantName}} hat die Lobby aktiviert",
"notificationTitle": "Lobby",
"passwordField": "Konferenzpasswort eingeben",
"passwordJoinButton": "Beitreten",
"reject": "Ablehnen",
"toggleLabel": "Lobby aktivieren"
}
}

View File

@@ -580,7 +580,7 @@
},
"security": {
"about": "Usted puede agregar una contraseña a la reunión. Los participantes necesitaran la contraseña para unirse a la reunión.",
"insecureRoomNameWarning": "El nombre de la sala es inseguro. Participantes no desseados pueden llegar a unirse a la reunión.",
"insecureRoomNameWarning": "El nombre de la sala es inseguro. Participantes no deseados pueden llegar a unirse a la reunión.",
"securityOptions": "Opciones de seguridad"
},
"settings": {

View File

@@ -578,7 +578,7 @@
},
"security": {
"about": "Usted puede agregar una contraseña a la reunión. Los participantes necesitaran la contraseña para unirse a la reunión.",
"insecureRoomNameWarning": "El nombre de la sala es inseguro. Participantes no desseados pueden llegar a unirse a la reunión.",
"insecureRoomNameWarning": "El nombre de la sala es inseguro. Participantes no deseados pueden llegar a unirse a la reunión.",
"securityOptions": "Opciones de seguridad"
},
"settings": {

View File

@@ -194,6 +194,8 @@
"done": "Terminé",
"enterDisplayName": "Merci de saisir votre nom ici",
"error": "Erreur",
"grantModeratorDialog": "Êtes vous sûr de vouloir rendre ce participant modérateur?",
"grantModeratorTitle": "Nommer modérateur",
"externalInstallationMsg": "Vous devez installer notre extension de partage de bureau.",
"externalInstallationTitle": "Extension requise",
"goToStore": "Aller sur le webstore",
@@ -788,6 +790,7 @@
"domute": "Couper le micro",
"domuteOthers": "Couper le micro de tous les autres",
"flip": "Balancer",
"grantModerator": "Nommer modérateur",
"kick": "Exclure",
"moderator": "Modérateur",
"mute": "Un participant a coupé son micro",

View File

@@ -325,7 +325,7 @@
"keyboardShortcuts": "キーボードショートカット",
"localRecording": "ローカル録画コントロールの表示/非表示",
"mute": "マイクの消音 ( ミュート )",
"pushToTalk": "話すために押す",
"pushToTalk": "プッシュ・トゥ・トーク",
"raiseHand": "手を上げる/下げる",
"showSpeakerStats": "演説者のデータを表示",
"toggleChat": "チャットを表示/非表示",
@@ -566,7 +566,7 @@
"shortcuts": "ショートカットに切り替える",
"show": "",
"speakerStats": "スピーカー統計に切り替える",
"tileView": "",
"tileView": "タイルビュー",
"toggleCamera": "カメラを切り替える",
"videomute": "ミュートビデオに切り替える",
"videoblur": ""
@@ -582,9 +582,9 @@
"documentClose": "共有ドキュメントを閉じる",
"documentOpen": "共有ドキュメントを開く",
"enterFullScreen": "フルスクリーン表示",
"enterTileView": "タイルビューを開始",
"enterTileView": "タイルビューを開始",
"exitFullScreen": "フルスクリーンを終了",
"exitTileView": "タイルビューを終了",
"exitTileView": "タイルビューを終了",
"feedback": "フィードバックを残す",
"hangup": "退出",
"invite": "メンバーを招待する",
@@ -609,7 +609,7 @@
"stopSubtitles": "字幕停止",
"stopSharedVideo": "YouTube動画を停止する",
"talkWhileMutedPopup": "話そうとしていますか? あなたはミュートされています。",
"tileViewToggle": "",
"tileViewToggle": "タイルビューを切り替え",
"toggleCamera": "カメラを切り替える",
"videomute": "カメラの開始 / 停止",
"startvideoblur": "",

View File

@@ -269,7 +269,7 @@
"userPassword": "senha do usuário",
"WaitForHostMsg": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, faça a autenticação. Do contrário, aguarde a chegada do anfitrião.",
"WaitForHostMsgWOk": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, pressione Ok para autenticar. Do contrário, aguarde a chegada do anfitrião.",
"WaitingForHost": "Esperando o hospedeiro...",
"WaitingForHost": "Esperando o anfitrião...",
"Yes": "Sim",
"yourEntireScreen": "Toda sua tela",
"screenSharingAudio": "Compartilhar áudio",

View File

@@ -1,28 +1,43 @@
{
"addPeople": {
"add": "Пригласить",
"addContacts": "Пригласите других людей",
"copyInvite": "Скопировать приглашение на встречу",
"copyLink": "Скопировать ссылку на встречу",
"copyStream": "Скопировать ссылку на прямую транасляцию",
"countryNotSupported": "Эта страна пока не поддерживается.",
"countryReminder": "Вызов не в США? Пожалуйста, убедитесь, что указали код страны!",
"defaultEmail": "Ваш адрес электронной почты",
"disabled": "Поиск не дал результата.",
"failedToAdd": "Не удалось добавить участников",
"footerText": "Вызов номера отключен.",
"googleEmail": "Электронная почта Google",
"inviteMoreHeader": "Сейчас вы одни в этой встрече",
"inviteMoreMailSubject": "Присоединиться к встрече {{appName}} ",
"inviteMorePrompt": "Пригласить других людей",
"linkCopied": "Ссылка скопирована в буфер обмена",
"loading": "Поиск людей и номеров телефонов",
"loadingNumber": "Проверка номера телефона",
"loadingPeople": "Поиск людей для приглашения",
"noResults": "Поиск не дал результата",
"noValidNumbers": "Пожалуйста, введите номер телефона",
"outlookEmail": "Электронная почта Outlook",
"searchNumbers": "Добавить номера телефонов",
"searchPeople": "Поиск людей",
"searchPeopleAndNumbers": "Поиск людей или добавление их телефонов",
"shareInvite": "Поделиться приглашением на встречу",
"shareLink": "Поделиться ссылкой на встречу чтобы пригласить других",
"shareStream": "Поделиться ссылкой на прямую трансляцию",
"telephone": "Номер: {{number}}",
"title": "Пригласить людей на эту встречу"
"title": "Пригласить людей на эту встречу",
"yahooEmail": "Электронная почта Yahoo"
},
"audioDevices": {
"bluetooth": "Bluetooth",
"headphones": "Наушники",
"none": "Не обнаружены звуковые устройства",
"phone": "Телефон",
"speaker": "Колонка",
"none": "Не обнаружены звуковые устройства"
"speaker": "Колонка"
},
"audioOnly": {
"audioOnly": "Только звук"
@@ -48,21 +63,21 @@
"chat": {
"error": "Ошибка: Ваше сообщение не было отправлено. Причина: {{error}}",
"fieldPlaceHolder": "Введите здесь ваше сообщение",
"messagebox": "Введите сообщение",
"messageTo": "Личное сообщение пользователю {{recipient}}",
"noMessagesMessage": "В конференции пока нет никаких сообщений. Начните разговор!",
"messagebox": "Введите сообщение",
"nickname": {
"popover": "Выберите имя",
"title": "Введите имя для использования чата"
},
"noMessagesMessage": "В конференции пока нет никаких сообщений. Начните разговор!",
"privateNotice": "Личное сообщение пользователю {{recipient}}",
"title": "Чат",
"you": "вы"
},
"chromeExtensionBanner": {
"installExtensionText": "Установите расширение для интеграции с Google Календарь и Office 365",
"buttonText": "Установить расширение Chrome",
"dontShowAgain": "Не показывай мне это снова"
"dontShowAgain": "Не показывай мне это снова",
"installExtensionText": "Установите расширение для интеграции с Google Календарь и Office 365"
},
"connectingOverlay": {
"joiningRoom": "Пытаемся присоединиться к вашей конференции..."
@@ -77,10 +92,11 @@
"DISCONNECTED": "Отключено",
"DISCONNECTING": "Отключение",
"ERROR": "Ошибка",
"RECONNECTING": "Проблема с сетью. Переподключение...",
"FETCH_SESSION_ID": "Получение идентификатора сеанса…",
"GET_SESSION_ID_ERROR": "Ошибка получения идентификатора сеанса: {{code}}",
"GOT_SESSION_ID": "Получение идентификатора сеанса… Готово",
"LOW_BANDWIDTH": "Видео для {{displayName}} приостановлено из-за низкой пропускной способности",
"GOT_SESSION_ID": "Получение идентификатора сеанса … Готово",
"GET_SESSION_ID_ERROR": "Ошибка получения идентификатора сеанса: {{code}}"
"RECONNECTING": "Проблема с сетью. Переподключение..."
},
"connectionindicator": {
"address": "Адрес:",
@@ -115,8 +131,7 @@
"status": "Связь:",
"transport_0": "Метод отправки:",
"transport_1": "Метода отправки:",
"transport_2": "Методов отправки:",
"e2e_rtt": ""
"transport_2": "Методов отправки:"
},
"dateUtils": {
"earlier": "Ранее",
@@ -128,6 +143,9 @@
"description": "Ничего не случилось? Мы попытались запустить вашу встречу в настольном приложении {{app}}. Повторите попытку или запустите ее в веб-приложении {{app}}.",
"descriptionWithoutWeb": "Ничего не произошло? Мы попытались запустить вашу конференцию в настольном приложении {{app}}",
"downloadApp": "Скачать приложение",
"ifDoNotHaveApp": "Если у вас еще нет приложения:",
"ifHaveApp": "Если вы уже установили приложение:",
"joinInApp": "Подключиться к этой встрече используя приложение",
"launchWebButton": "Запустить в браузере",
"openApp": "Перейти к приложению",
"title": "Запуск вашей встречи в {{app}}...",
@@ -147,15 +165,29 @@
"selectADevice": "Выбор устройства",
"testAudio": "Протестировать звук"
},
"dialOut": {
"statusMessage": "сейчас {{status}}"
},
"dialog": {
"Back": "Назад",
"Cancel": "Отмена",
"IamHost": "Я организатор",
"Ok": "Ok",
"Remove": "Удалить",
"Share": "Поделиться",
"Submit": "ОК",
"WaitForHostMsg": "Конференция <b>{{room}}</b> еще не началась. Если вы организатор, пожалуйста, авторизируйтесь. В противном случае дождитесь организатора.",
"WaitForHostMsgWOk": "Конференция <b>{{room}}</b> еще не началась. Если вы организатор, пожалуйста, нажмите Ok для аутентификации. В противном случае, дождитесь организатора.",
"WaitingForHost": "Ждем организатора...",
"Yes": "Да",
"accessibilityLabel": {
"liveStreaming": "Трансляция"
},
"add": "Добавить",
"allow": "Разрешить",
"alreadySharedVideoMsg": "Другой участник уже поделился ссылкой на видео. Данная конференция позволяет одновременно делиться только одним видео.",
"alreadySharedVideoTitle": "Допускается показ только одного видео",
"applicationWindow": "Окно приложения",
"Back": "Назад",
"cameraConstraintFailedError": "Камера не отвечает определенным требованиям.",
"cameraNotFoundError": "Камера не обнаружена.",
"cameraNotSendingData": "Ошибка доступа к камере. Пожалуйста, проверьте, не использует ли камеру какая-нибудь другая программа. Вы можете также выбрать другое устройство из меню настроек или попробовать перезапустить приложение.",
@@ -163,7 +195,6 @@
"cameraPermissionDeniedError": "Нет доступа к камере. Вы можете участвовать во встрече, но другие не будут вас видеть. Используйте значок камеры в адресной строке браузера, чтобы устранить проблему.",
"cameraUnknownError": "Неизвестная ошибка использования камеры.",
"cameraUnsupportedResolutionError": "Ваша камера не поддерживает необходимое разрешение видео.",
"Cancel": "Отмена",
"close": "Закрыть",
"conferenceDisconnectMsg": "Следует проверить интернет-соединение. Попытка восстановления связи через {{seconds}} с.",
"conferenceDisconnectTitle": "Вы отключены.",
@@ -176,21 +207,29 @@
"connectErrorWithMsg": "Ошибка. Невозможно установить связь для вашей встречи: {{msg}}",
"connecting": "Подключение",
"contactSupport": "Связь с поддержкой",
"copied": "Скопировано",
"copy": "Копировать",
"dismiss": "Отклонить",
"displayNameRequired": "Привет! Как тебя зовут?",
"done": "Готово",
"e2eeDescription": "Сквозное шифрование в настоящее время является ЭКСПЕРИМЕНТАЛЬНЫМ. Имейте в виду, что включение сквозного шифрования эффективно отключит сервисы, предоставляемые на стороне сервера, такие как: запись, потоковое вещание и участие по телефону. Также имейте в виду, что собрание будет работать только для людей, присоединяющихся из браузеров с поддержкой вставляемых потоков.",
"e2eeLabel": "E2EE ключ",
"e2eeNoKey": "Отсутствует",
"e2eeSet": "Установить",
"e2eeToggleSet": "Установить ключ",
"e2eeWarning": "ПРЕДУПРЕЖДЕНИЕ. Похоже, что не все участники этой встречи поддерживают сквозное шифрование. Если вы включите его, они не смогут вас ни видеть, ни слышать.",
"enterDisplayName": "Пожалуйста, введите свое имя",
"error": "Ошибка",
"externalInstallationMsg": "Вам необходимо установить наше дополнение для совместного использования рабочего стола.",
"externalInstallationTitle": "Требуется расширение",
"goToStore": "Перейти к интернет-магазину",
"gracefulShutdown": "Технические работы. Пожалуйста, попробуйте позже.",
"IamHost": "Я организатор",
"incorrectRoomLockPassword": "Неверный пароль",
"grantModeratorDialog": "Вы уверены, что хотите сделать этого участника модератором?",
"grantModeratorTitle": "Сделать модератором",
"incorrectPassword": "Ошибка имени пользователя или пароля",
"inlineInstallationMsg": "Вам необходимо установить наше дополнение для совместного использования рабочего стола.",
"incorrectRoomLockPassword": "Неверный пароль",
"inlineInstallExtension": "Установить",
"inlineInstallationMsg": "Вам необходимо установить наше дополнение для совместного использования рабочего стола.",
"internalError": "Что-то пошло не так. Ошибка: {{error}}",
"internalErrorTitle": "Внутренняя ошибка",
"kickMessage": "Вы можете связаться с {{participantDisplayName}} для получения более подробной информации.",
@@ -199,6 +238,7 @@
"kickParticipantTitle": "Выгнать этого участника?",
"kickTitle": "Ай! {{participantDisplayName}} выгнал вас из конференции.",
"liveStreaming": "Трансляция",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Невозможно пока активна запись",
"liveStreamingDisabledForGuestTooltip": "Гости не могут начать трансляцию",
"liveStreamingDisabledTooltip": "Возможность трансляции отключена",
"lockMessage": "Не удалось запереть конференцию",
@@ -214,18 +254,25 @@
"micNotSendingDataTitle": "Ваш микрофон отключен системными настройками",
"micPermissionDeniedError": "Нет доступа к микрофону. Вы можете участвовать во встрече, но другие не будут вас слышать. Используйте значок камеры в адресной строке браузера, чтобы устранить проблему.",
"micUnknownError": "Неизвестная ошибка использования микрофона.",
"muteParticipantBody": "Вы не можете включить им звук, но они могут сделать это сами в любое время.",
"muteParticipantButton": "Выключить звук",
"muteEveryoneDialog": "Вы уверены, что хотите отключить микрофоны у всех? Вы не сможете включить их, но они могут включить себя в любой момент.",
"muteEveryoneElseDialog": "После отключения микрофонов у участников вы не сможете включить их, но они могут включить себя в любой момент.",
"muteEveryoneElseTitle": "Заглушить всех, за исключением {{whom}}?",
"muteEveryoneSelf": "себя",
"muteEveryoneStartMuted": "Теперь у всех микрофоны выключены",
"muteEveryoneTitle": "Заглушить всех?",
"muteParticipantBody": "Вы не можете включить им микрофон, но они могут сделать это сами в любое время.",
"muteParticipantButton": "Заглушить",
"muteParticipantDialog": "Вы уверены, что хотите отключить микрофон у данного пользователя? Вы не сможете отменить это действие, однако он сможет сам снова включить микрофон в любое время.",
"muteParticipantTitle": "Приглушить этого участника?",
"Ok": "Ok",
"passwordLabel": "$t(lockRoomPasswordUppercase)",
"muteParticipantTitle": "Заглушить этого участника?",
"passwordLabel": "Встреча была защищена участником. Пожалуйста, введите $t(lockRoomPasswordUppercase) чтобы присоединиться.",
"passwordNotSupported": "Установка $t(lockRoomPassword) для конференции не поддерживается.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) не поддерживается",
"passwordRequired": "Требуется $t(lockRoomPasswordUppercase)",
"popupError": "Ваш браузер блокирует всплывающие окна этого сайта. Пожалуйста, разрешите всплывающие окна в настройках безопасности браузера и попробуйте снова.",
"popupErrorTitle": "Заблокировано всплывающее окно",
"readMore": "больше",
"recording": "Запись",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Невозможно пока активно потоковое вещание",
"recordingDisabledForGuestTooltip": "Гости не могут записывать",
"recordingDisabledTooltip": "Невозможно начать запись",
"rejoinNow": "Подключиться снова",
@@ -236,13 +283,15 @@
"remoteControlShareScreenWarning": "Если нажмете \"Разрешить\", то поделитесь своим экраном!",
"remoteControlStopMessage": "Сессия удаленного управления завершена!",
"remoteControlTitle": "Удаленное управление рабочим столом",
"Remove": "Удалить",
"removePassword": "Убрать $t(lockRoomPassword)",
"removeSharedVideoMsg": "Уверены, что хотите убрать видео, которым поделились?",
"removeSharedVideoTitle": "Убрать видео",
"reservationError": "Ошибка системы резервирования",
"reservationErrorMsg": "Код ошибки: {{code}}, сообщение: {{msg}}",
"retry": "Повторить",
"screenSharingAudio": "Поделиться аудио",
"screenSharingFailed": "Ой! Кажется что-то пошло не так, мы не можем начать показ экрана!",
"screenSharingFailedTitle": "Сбой показа экрана!",
"screenSharingFailedToInstall": "Ошибка установки расширения для показа экрана.",
"screenSharingFailedToInstallTitle": "Расширение для показа экрана не установлено",
"screenSharingFirefoxPermissionDeniedError": "Что-то пошло не так, когда мы пытались поделиться вашим экраном. Пожалуйста, убедитесь, что вы дали нам разрешение на это. ",
@@ -254,7 +303,6 @@
"sendPrivateMessageTitle": "Отправить личное сообщение?",
"serviceUnavailable": "Служба недоступна",
"sessTerminated": "Связь прервана",
"Share": "Поделиться",
"shareVideoLinkError": "Пожалуйста, укажите корректную ссылку Youtube.",
"shareVideoTitle": "Поделиться видео",
"shareYourScreen": "Показать экран",
@@ -268,7 +316,6 @@
"stopRecordingWarning": "Уверены, что хотите остановить запись?",
"stopStreamingWarning": "Уверены, что хотите остановить трансляцию?",
"streamKey": "Ключ трансляции",
"Submit": "ОК",
"thankYou": "Спасибо, что используете {{appName}}!",
"token": "токен",
"tokenAuthFailed": "Извините, вам не разрешено присоединиться к этому сеансу связи.",
@@ -276,21 +323,17 @@
"transcribing": "Расшифровка",
"unlockRoom": "Убрать $t(lockRoomPassword)",
"userPassword": "пароль пользователя",
"WaitForHostMsg": "Конференция <b>{{room}}</b> еще не началась. Если вы организатор, пожалуйста, авторизируйтесь. В противном случае дождитесь организатора.",
"WaitForHostMsgWOk": "Конференция <b>{{room}}</b> еще не началась. Если вы организатор, пожалуйста, нажмите Ok для аутентификации. В противном случае, дождитесь организатора.",
"WaitingForHost": "Ждем организатора...",
"Yes": "Да",
"yourEntireScreen": "Весь экран",
"muteEveryoneElseTitle": "Заглушить всех, за исключением {{whom}}?",
"screenSharingAudio": "Поделиться аудио",
"muteEveryoneSelf": "себя"
},
"dialOut": {
"statusMessage": "сейчас {{status}}"
"yourEntireScreen": "Весь экран"
},
"documentSharing": {
"title": "Общий Документ"
},
"e2ee": {
"labelToolTip": "Аудио и видео связь по этому вызову защищена сквозным шифрованием"
},
"embedMeeting": {
"title": "Встроить эту встречу"
},
"feedback": {
"average": "Средне",
"bad": "Плохо",
@@ -300,6 +343,9 @@
"veryBad": "Очень плохо",
"veryGood": "Очень хорошо"
},
"helpView": {
"header": "Справка"
},
"incomingCall": {
"answer": "Ответ",
"audioCallTitle": "Входящий звонок",
@@ -313,8 +359,8 @@
"cancelPassword": "Отменить $t(lockRoomPassword)",
"conferenceURL": "Ссылка:",
"country": "Страна",
"dialANumber": "Чтобы присоединиться к конференции, наберите один из этих номеров и введите pin-код",
"dialInConferenceID": "PIN:",
"dialANumber": "Чтобы присоединиться к конференции, наберите один из этих номеров и введите пин-код",
"dialInConferenceID": "ПИН:",
"dialInNotSupported": "К сожалению, набор номера в настоящее время не поддерживается.",
"dialInNumber": "Номер:",
"dialInSummaryError": "Ошибка получения информации о наборе номера. Пожалуйста, повторите попытку позже",
@@ -326,6 +372,7 @@
"inviteURLFirstPartGeneral": "Вас приглашают присоединиться к конференции.",
"inviteURLFirstPartPersonal": "{{name}} приглашает Вас присоединиться к конференции. \n",
"inviteURLSecondPart": "\nПрисоединиться к конференции:\n{{url}}\n",
"label": "Информация о конференции",
"liveStreamURL": "Трансляция:",
"moreNumbers": "Больше номеров",
"noNumbers": "Нет номеров для набора.",
@@ -334,8 +381,13 @@
"numbers": "Номера для набора",
"password": "$t(lockRoomPasswordUppercase):",
"title": "Поделиться",
"tooltip": "Поделитесь ссылкой и номером для подключения к этой конференции",
"label": "Информация о конференции"
"tooltip": "Поделитесь ссылкой и номером для подключения к этой конференции"
},
"inlineDialogFailure": {
"msg": "Небольшая заминка.",
"retry": "Попробовать снова",
"support": "Поддержка",
"supportMsg": "Если это продолжится, свяжитесь с"
},
"inviteDialog": {
"alertText": "Не удалось пригласить некоторых участников.",
@@ -345,12 +397,6 @@
"searchPlaceholder": "Участник или номер телефона",
"send": "Отправить"
},
"inlineDialogFailure": {
"msg": "Небольшая заминка.",
"retry": "Попробовать снова",
"support": "Поддержка",
"supportMsg": "Если это продолжится, свяжитесь с"
},
"keyboardShortcuts": {
"focusLocal": "Фокус на ваше видео",
"focusRemote": "Фокус на видео другого участника",
@@ -383,23 +429,55 @@
"expandedPending": "Начинается прямая трансляция...",
"failedToStart": "Ошибка трансляции видео",
"getStreamKeyManually": "Прямые трансляций не найдены. Попробуйте получить ключ прямой трансляции от YouTube.",
"googlePrivacyPolicy": "Политика конфиденциальности Google",
"invalidStreamKey": "Похоже ключ прямой трансляции неверен.",
"limitNotificationDescriptionNative": "Ваша трансляция будет ограничена {{limit}} мин. Для неограниченного просмотра попробуйте {{app}}.",
"limitNotificationDescriptionWeb": "Из-за высокой нагрузки ваша потоковая передача будет ограничена {{limit}} мин. Для неограниченной потоковой передачи попробуйте <a href={{url}} rel='noopener noreferrer' target='_blank'> {{app}} </a>.",
"off": "Трансляция остановлена",
"offBy": "{{name}} остановил прямую трансляцию",
"on": "Трансляция",
"onBy": "{{name}} начал прямую трансляцию",
"pending": "Начинаем трансляцию...",
"serviceName": "Служба трансляции",
"signedInAs": "В настоящее время вы вошли в систему как:",
"signIn": "Войти через Google",
"signInCTA": "Войдите или введите свой ключ трансляции YouTube.",
"signOut": "Выход",
"signedInAs": "В настоящее время вы вошли в систему как:",
"start": "Начать трансляцию",
"streamIdHelp": "Что это?",
"unavailableTitle": "Трансляция недоступна",
"googlePrivacyPolicy": "Политика конфиденциальности Google",
"youtubeTerms": "Условия использования YouTube"
},
"lobby": {
"disableDialogContent": "В настоящее время включен режим лобби. Эта функция гарантирует, что нежелательные участники не смогут присоединиться к вашей встрече. Вы хотите его отключить?",
"disableDialogSubmit": "Отключить",
"emailField": "Введите ваш адрес электронной почты",
"enableDialogPasswordField": "Установите пароль (необязательно)",
"enableDialogSubmit": "Включить",
"enableDialogText": "Режим лобби позволяет защитить вашу встречу, позволяя людям входить только после официального одобрения модератором.",
"enterPasswordButton": "Введите пароль встречи",
"enterPasswordTitle": "Введите пароль чтобы присоединиться к встрече",
"invalidPassword": "Неверный пароль",
"joinRejectedMessage": "Ваш запрос на присоединение был отклонен модератором.",
"joinTitle": "Присоединиться к встрече",
"joinWithPasswordMessage": "Пытаюсь присоединиться с паролем, подождите...",
"joiningMessage": "Вы присоединитесь к встрече, как только кто-то примет ваш запрос",
"joiningTitle": "Просьба присоединиться к встрече...",
"joiningWithPasswordTitle": "Присоединение с паролем...",
"knockButton": "Попросить присоединиться",
"knockTitle": "Кто-то хочет присоединиться к встрече",
"knockingParticipantList": "Список ожидающих участников",
"nameField": "Введите ваше имя",
"notificationLobbyAccessDenied": "{{originParticipantName}} запретил присоединиться {{targetParticipantName}}",
"notificationLobbyAccessGranted": "{{originParticipantName}}разрешил присоединиться {{targetParticipantName}} ",
"notificationLobbyDisabled": "Лобби отключено пользователем {{originParticipantName}}",
"notificationLobbyEnabled": "Лобби включено пользователем {{originParticipantName}}",
"notificationTitle": "Лобби",
"passwordField": "Введите пароль встречи",
"passwordJoinButton": "Присоединиться",
"reject": "Отказать",
"toggleLabel": "Включить лобби"
},
"localRecording": {
"clientState": {
"off": "Отключен",
@@ -431,8 +509,15 @@
},
"lockRoomPassword": "пароль",
"lockRoomPasswordUppercase": "Пароль",
"lonelyMeetingExperience": {
"button": "Пригласить",
"getHelp": "Получить помощь",
"title": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций",
"youAreAlone": "Вы один в видеоконференции"
},
"me": "я",
"notify": {
"OldElectronAPPTitle": "Уязвимость в системе безопасности!",
"connectedOneMember": "{{name}} присоединился к конференции",
"connectedThreePlusMembers": "{{name}} и {{count}} других пользователей присоединились к конференции",
"connectedTwoMembers": "{{first}} и {{second}} присоединились к конференции",
@@ -447,25 +532,64 @@
"me": "Я",
"moderator": "Получены права модератора!",
"muted": "Вы начали разговор без звука.",
"mutedTitle": "Вы без звука!",
"mutedRemotelyTitle": "{{participantDisplayName}} отключил Вам микрофон!",
"mutedRemotelyDescription": "Вы всегда можете включить микрофон, когда будете готовы говорить. Отключите его, когда закончите, чтобы не транслировать шумы в конференцию.",
"mutedRemotelyTitle": "{{participantDisplayName}} отключил Вам микрофон!",
"mutedTitle": "Вы без звука!",
"newDeviceAction": "Использовать",
"newDeviceAudioTitle": "Обнаружено новое аудиоустройство",
"newDeviceCameraTitle": "Обнаружена новая камера",
"oldElectronClientDescription1": "Похоже, вы используете старую версию клиента Jitsi Meet, которая имеет известные уязвимости в системе безопасности. Убедитесь, что вы обновили до нашей ",
"oldElectronClientDescription2": "последней версии",
"oldElectronClientDescription3": " сейчас!",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) удален другим участником.",
"passwordSetRemotely": "Другой участник установил $t(lockRoomPasswordUppercase)",
"raisedHand": "{{name}} хотел бы выступить.",
"somebody": "Кто-то",
"startSilentTitle": "У вас отсутствует звук!",
"startSilentDescription": "Перезайдите в конференцию, чтобы включить звук",
"startSilentTitle": "У вас отсутствует звук!",
"suboptimalBrowserWarning": "К сожалению, ваш браузер не полностью поддерживает данную систему вэбконференций. Мы работаем над проблемой, однако, пока рекомендуем вам воспользоваться <a href='{{recommendedBrowserPageLink}}' target='_blank'> следующими браузерами</a>.",
"suboptimalExperienceTitle": "Предупреждение браузера",
"unmute": "Включить микрофон",
"newDeviceCameraTitle": "Обнаружена новая камера",
"newDeviceAudioTitle": "Обнаружено новое аудиоустройство",
"newDeviceAction": "Использовать"
"unmute": "Включить микрофон"
},
"passwordSetRemotely": "установлен другим участником",
"passwordDigitsOnly": "До {{number}} цифр",
"passwordSetRemotely": "установлен другим участником",
"poweredby": "работает на",
"prejoin": {
"audioAndVideoError": "Ошибка звука и видео:",
"audioOnlyError": "Ошибка звука:",
"audioTrackError": "Не удалось создать аудио дорожку.",
"callMe": "Позвоните мне",
"callMeAtNumber": "Позвоните мне по этому номеру:",
"calling": "Вызываем",
"configuringDevices": "Настраиваются устройства...",
"connectedWithAudioQ": "Вы подключили звук?",
"copyAndShare": "Скопировать и поделиться ссылкой на встречу",
"dialInMeeting": "Дозвониться до встречи",
"dialInPin": "Дозвониться до встречи и ввести ПИН код:",
"dialing": "Дозвон",
"doNotShow": "Не показывать снова",
"errorDialOut": "Не удалось дозвониться",
"errorDialOutDisconnected": "Не удалось дозвониться. Отключено",
"errorDialOutFailed": "Не удалось дозвониться. Сбой вызова",
"errorDialOutStatus": "Ошибка получения статуса вызова",
"errorStatusCode": "Ошибка вызова, код статуса: {{status}}",
"errorValidation": "Проверка номера не удалась",
"iWantToDialIn": "Я хочу дозвониться",
"initiated": "Вызов инициирован",
"joinAudioByPhone": "Подключиться с телефонной связью",
"joinMeeting": "Присоединиться ко встрече",
"joinWithoutAudio": "Присоединиться без звука",
"linkCopied": "Ссылка скопирована в буфер обмена",
"lookGood": "Кажется ваш микрофон работает правильно",
"or": "или",
"premeeting": "Перед подключением",
"screenSharingError": "Ошибка показа экрана:",
"showScreen": "Включить экран перед подключением",
"startWithPhone": "Начать с телефонной связью",
"videoOnlyError": "Ошибка видео:",
"videoTrackError": "Не удалось создать видео дорожку.",
"viewAllNumbers": "посмотреть всех участников"
},
"presenceStatus": {
"busy": "Занят",
"calling": "Вызываю...",
@@ -499,14 +623,16 @@
"expandedPending": "Начинаем запись конференции...",
"failedToStart": "Ошибка начала записи",
"fileSharingdescription": "Поделиться записью с участниками конференции",
"live": "Прямая трансляция",
"limitNotificationDescriptionNative": "Из-за высокой нагрузки ваша запись будет ограничена {{limit}} мин. Для неограниченного количества записей попробуйте <3> {{app}} </3>.",
"limitNotificationDescriptionWeb": "Из-за высокой нагрузки ваша запись будет ограничена {{limit}} мин. Для неограниченного количества записей попробуйте <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"live": "В ЭФИРЕ",
"loggedIn": "Вошел как {{userName}}",
"off": "Запись остановлена",
"offBy": "{{name}} остановил запись",
"on": "Запись",
"onBy": "{{name}} включил запись",
"pending": "Подготовка записи конференции. . .",
"rec": дет запись",
"rec": ДЕТ ЗАПИСЬ",
"serviceDescription": "Ваша запись будет сохранена соответствующей службой",
"serviceName": "Служба записи",
"signIn": "Вход",
@@ -517,6 +643,12 @@
"sectionList": {
"pullToRefresh": "Потяните для обновления"
},
"security": {
"about": "Вы можете добавить к собранию $t(lockRoomPassword). Участникам необходимо будет предоставить $t(lockRoomPassword), прежде чем им будет разрешено присоединиться к собранию.",
"aboutReadOnly": "Участники-модераторы могут добавить к собранию $t(lockRoomPassword). Участникам необходимо будет предоставить $t(lockRoomPassword), прежде чем им будет разрешено присоединиться к собранию.",
"insecureRoomNameWarning": "Имя комнаты небезопасно. Нежелательные участники могут присоединиться к вашей конференции. Подумайте о том, чтобы защитить вашу встречу используя настройки безопасности.",
"securityOptions": "Настройки безопасности"
},
"settings": {
"calendar": {
"about": "Интеграция с календарем {{appName}} используется для безопасного доступа к вашему календарю и синхронизации запланированных мероприятий.",
@@ -529,6 +661,7 @@
"followMe": "Все следуют за мной",
"language": "Язык",
"loggedIn": "Вошел как {{name}}",
"microphones": "Микрофоны",
"moderator": "Модератор",
"more": "Больше опций",
"name": "Имя",
@@ -536,11 +669,10 @@
"selectAudioOutput": "Звуковой выход",
"selectCamera": "Камера",
"selectMic": "Микрофон",
"speakers": "Динамики",
"startAudioMuted": "Все начинают с выключенным звуком",
"startVideoMuted": "Все начинают в скрытом режиме",
"title": "Настройки",
"speakers": "Динамики",
"microphones": "Микрофоны"
"title": "Настройки"
},
"settingsView": {
"advanced": "Дополнительные",
@@ -550,6 +682,8 @@
"buildInfoSection": "Информация о сборке",
"conferenceSection": "Номера для набора",
"disableCallIntegration": "Отключить встроенную интеграцию вызовов",
"disableCrashReporting": "Отключить отправку отчетов о сбоях",
"disableCrashReportingWarning": "Вы действительно хотите отключить отчеты о сбоях? Настройка будет применена после перезапуска приложения.",
"disableP2P": "Отключить режим Peer-To-Peer",
"displayName": "Отображаемое имя",
"email": "Email",
@@ -565,7 +699,7 @@
"dialInfoText": "\n\n=====\n\nПросто хотите набрать номер на Вашем телефоне?\n\n{{defaultDialInNumber}}Щелкните на эту ссылку, чтобы просмотреть телефонные номера для этой конференции\n{{dialInfoPageUrl}}",
"mainText": "Нажмите на ссылку чтобы присоединиться к конференции:\n{{roomUrl}}"
},
"speaker": "Колонка",
"speaker": "Спикер",
"speakerStats": {
"hours": "{{count}}ч",
"minutes": "{{count}}м",
@@ -584,7 +718,9 @@
"title": "Видеосвязь прервана. Причина: этот компьютер перешел в режим сна."
},
"toolbar": {
"Settings": "Настройки",
"accessibilityLabel": {
"Settings": "Вкл/Выкл меню настроек",
"audioOnly": "Вкл/Выкл только звук",
"audioRoute": "Выбрать аудиоустройство",
"callQuality": "Качество связи",
@@ -592,36 +728,41 @@
"chat": "Показать/скрыть окно чата",
"document": "Закрыть общий документ",
"download": "Скачать приложение",
"e2ee": "Сквозное шифрование",
"embedMeeting": "Встроить встречу",
"feedback": "Оставить отзыв",
"fullScreen": "Полноэкранный/оконный режим",
"grantModerator": "Сделать модератором",
"hangup": "Завершить звонок",
"help": "Справка",
"invite": "Пригласить",
"kick": "Выкинуть участника",
"lobbyButton": "Вкл/Выкл режим лобби",
"localRecording": "Вкл/Выкл кнопки записи",
"lockRoom": "Установить пароль",
"moreActions": "Показать/скрыть меню доп. настроек",
"moreOptions": "Меню доп. настроек",
"moreActionsMenu": "Меню доп. настроек",
"moreOptions": "Меню доп. настроек",
"mute": "Вкл/Выкл звук",
"muteEveryone": "Выкл. микрофон у всех",
"pip": "Вкл/Выкл режим Картинка-в-картинке",
"privateMessage": "Отправить личное сообщение",
"profile": "Редактировать профиль",
"raiseHand": "Поднять руку",
"recording": "Вкл/Выкл запись",
"remoteMute": "Отключить участнику микрофон",
"Settings": "Вкл/Выкл меню настроек",
"sharedvideo": "Вкл/Выкл Youtube - трансляцию",
"security": "Настройки безопасности",
"shareRoom": "Отправить приглашение",
"shareYourScreen": "Вкл/Выкл демонстрацию экрана",
"sharedvideo": "Вкл/Выкл Youtube - трансляцию",
"shortcuts": "Вкл/Выкл значки",
"show": "Показать крупным планом",
"speakerStats": "Вкл/Выкл статистику",
"tileView": "Вкл/Выкл плитку",
"toggleCamera": "Переключить камеру",
"videomute": "Вкл/Выкл видео",
"muteEveryone": "Выкл. микрофон у всех",
"videoblur": "Вкл/Выкл размытие фона"
"toggleFilmstrip": "Включить диафильм",
"videoblur": "Вкл/Выкл размытие фона",
"videomute": "Вкл/Выкл видео"
},
"addPeople": "Добавить людей к вашему сеансу связи",
"audioOnlyOff": "Отключить режим экономии пропуской способности",
@@ -634,6 +775,8 @@
"documentClose": "Закрыть общий документ",
"documentOpen": "Открыть общий документ",
"download": "Скачать приложение",
"e2ee": "Сквозное шифрование",
"embedMeeting": "Встроить встречу",
"enterFullScreen": "Полный экран",
"enterTileView": "Общий план",
"exitFullScreen": "Полный экран",
@@ -642,39 +785,44 @@
"hangup": "Выход",
"help": "Справка",
"invite": "Пригласить",
"lobbyButtonDisable": "Отключить режим лобби",
"lobbyButtonEnable": "Включить режим лобби",
"login": "Войти",
"logout": "Завершить сеанс",
"lowerYourHand": "Опустить руку",
"moreActions": "Больше",
"moreActions": "Больше действий",
"moreOptions": "Больше настроек",
"mute": "Микрофон (вкл./выкл.)",
"muteEveryone": "Выкл. микрофон у всех",
"noAudioSignalTitle": "От вашего микрофона не идет звуковой сигнал!",
"noAudioSignalDesc": "Если вы специально не отключали микрофон в системных настройках, подумайте о том, чтобы поменять его.",
"noAudioSignalDescSuggestion": "Если вы специально не отключали микрофон в системных настройках, вы можете попробовать использовать следующее устройство:",
"noisyAudioInputTitle": "Похоже, ваш микрофон создает шум!",
"noAudioSignalDialInDesc": "Вы можете также дозвониться используя:",
"noAudioSignalDialInLinkDesc": "Номера для дозвона",
"noAudioSignalTitle": "От вашего микрофона не идет звуковой сигнал!",
"noisyAudioInputDesc": "Возможно, ваш микрофон создает шум. Вы можете выключить его или смените устройство.",
"noisyAudioInputTitle": "Похоже, ваш микрофон создает шум!",
"openChat": "Открыть чат",
"pip": "Вкл режим Картинка-в-картинке",
"privateMessage": "Отправить личное сообщение",
"profile": "Редактировать профиль",
"raiseHand": "Хочу говорить",
"raiseYourHand": "Поднять руку",
"Settings": "Настройки",
"sharedvideo": "Видео YouTube",
"security": "Настройки безопасности",
"shareRoom": "Отправить приглашение",
"sharedvideo": "Видео YouTube",
"shortcuts": "Комбинации клавиш",
"speakerStats": "Статистика",
"startScreenSharing": "Начать трансляцию с экрана",
"startSubtitles": "Включить субтитры",
"startvideoblur": "Размыть фон на видео",
"stopScreenSharing": "Остановить трансляцию с экрана",
"stopSubtitles": "Отключить субтитры",
"stopSharedVideo": "Остановить видео на YouTube",
"stopSubtitles": "Отключить субтитры",
"stopvideoblur": "Отключить размытие фона",
"talkWhileMutedPopup": "Пытаетесь говорить? У вас отключен звук.",
"tileViewToggle": "Вкл/выкл плитку",
"toggleCamera": "Вкл/выкл камеру",
"videomute": "Камера",
"startvideoblur": "Размыть фон на видео",
"stopvideoblur": "Отключить размытие фона"
"videomute": "Камера"
},
"transcribing": {
"ccButtonTooltip": "Вкл. / Выкл. субтитры",
@@ -685,8 +833,7 @@
"off": "Расшифровка остановлена",
"pending": "Подготовка расшифровки конференции...",
"start": "Вкл/Выкл показ субтитров",
"stop": "Вкл/Выкл показ субтитров",
"tr": ""
"stop": "Вкл/Выкл показ субтитров"
},
"userMedia": {
"androidGrantPermissions": "Выберите <b><i>Разрешить</i></b>, когда браузер спросит о разрешениях.",
@@ -732,6 +879,7 @@
"domute": "Выключить звук",
"domuteOthers": "Выключить остальных",
"flip": "Отразить",
"grantModerator": "Сделать модератором",
"kick": "Выкинуть",
"moderator": "Модератор",
"mute": "Без звука",
@@ -754,26 +902,22 @@
"connectCalendarButton": "Привязать календарь",
"connectCalendarText": "Подключите календарь, чтобы увидеть все ваши конференции в {{app}}. Кроме того, добавив {{provider}} конференций в календарь, вы сможете запускать их одним щелчком мышки.",
"enterRoomTitle": "Начать новую видеоконференцию",
"roomNameAllowedChars": "Название конференции не должно содержать следующие символы: ?, &, :, ', \", %, #.",
"getHelp": "Справка",
"go": "ОК",
"goSmall": "ОК",
"join": "СОЗДАТЬ / ПРИСОЕДИНИТЬСЯ",
"info": "Инфо",
"join": "СОЗДАТЬ / ПРИСОЕДИНИТЬСЯ",
"moderatedMessage": "Или заранее <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">зарезервируйте URL-адрес встречи</a>, где вы будете единственным модератором.",
"privacy": "Приватность",
"recentList": "Недавние",
"recentListDelete": "Удалить",
"recentListEmpty": "Сейчас ваш список недавно проведенных конференций пуст. По мере вашего пользования сервисом он будет пополняться.",
"reducedUIText": "Добро пожаловать в {{app}}!",
"roomNameAllowedChars": "Название конференции не должно содержать следующие символы: ?, &, :, ', \", %, #.",
"roomname": "Укажите название комнаты",
"roomnameHint": "Укажите название комнаты или ее адрес. Можете сами создать название и передать его будущим участникам встречи, чтобы они использовали именно его.",
"sendFeedback": "Обратная связь",
"terms": "Условия",
"title": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций"
},
"lonelyMeetingExperience": {
"button": "Пригласить",
"youAreAlone": "Вы один в видеоконференции",
"title": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций",
"getHelp": "Получить помощь"
}
}

View File

@@ -216,7 +216,9 @@
"kickParticipantDialog": "Are you sure you want to kick this participant?",
"kickParticipantTitle": "Kick this participant?",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"leaveJibriQueue": "Exit queue",
"liveStreaming": "Live Streaming",
"leaveJibriQueueWarning": "Are you sure you would like to exit the queue?",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
"liveStreamingDisabledTooltip": "Start live stream disabled.",
@@ -908,5 +910,22 @@
"passwordJoinButton": "Join",
"reject": "Reject",
"toggleLabel": "Enable lobby"
},
"jibriQueue": {
"recording": {
"title": "You have joined a recording queue!",
"time": "Estimated time for starting the recording: {{time}}",
"footer": "For unlimited recordings you should subscribe to 8x8 Meetings",
"left": "You have left the recording queue!"
},
"livestreaming": {
"title": "You have joined a live streaming queue!",
"time": "Estimated time for starting the live streaming: {{time}}",
"footer": "For unlimited live streaming you should subscribe to 8x8 Meetings",
"left": "You have left the live streaming queue!"
},
"position": "{{count}} more person is waiting in front of you.",
"position_plural": "{{count}} more people are waiting in front of you.",
"exit": "Exit queue"
}
}

View File

@@ -238,6 +238,7 @@ function initCommands() {
return;
}
const jibriQueueJID = state['features/base/config'].jibriQueueJID;
let recordingConfig;
if (mode === JitsiRecordingConstants.mode.FILE) {
@@ -251,7 +252,8 @@ function initCommands() {
'token': dropboxToken
}
}
})
}),
jibriQueueJID
};
} else {
recordingConfig = {
@@ -260,12 +262,14 @@ function initCommands() {
'file_recording_metadata': {
'share': shouldShare
}
})
}),
jibriQueueJID
};
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
recordingConfig = {
broadcastId: youtubeBroadcastID,
jibriQueueJID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey
};
@@ -275,7 +279,9 @@ function initCommands() {
return;
}
conference.startRecording(recordingConfig);
conference.startRecording(recordingConfig).catch(() => {
// prevent unhandled promise rejection.
});
},
/**
@@ -302,8 +308,10 @@ function initCommands() {
const activeSession = getActiveSession(state, mode);
if (activeSession && activeSession.id) {
conference.stopRecording(activeSession.id);
if (activeSession && (activeSession.id || activeSession.queueID)) {
conference.stopRecording(activeSession.id, activeSession.queueID).catch(() => {
// prevent unhandled promise rejection.
});
} else {
logger.error('No recording or streaming session found');
}

7298
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,7 @@
"@atlaskit/tabs": "8.0.11",
"@atlaskit/theme": "7.0.2",
"@atlaskit/toggle": "5.0.14",
"@atlaskit/portal": "4.0.0",
"@atlaskit/tooltip": "12.1.13",
"@jitsi/js-utils": "1.0.1",
"@microsoft/microsoft-graph-client": "1.1.0",
@@ -56,7 +57,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9abec6faeea9698db40ed3adc98ce03ec546cedb",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f74cd0abe9c696a9c3ca7dbb9ca170e6e84d6756",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -125,6 +126,7 @@
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"jest": "26.1.0",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.14.1",
@@ -144,6 +146,7 @@
"scripts": {
"lint": "eslint . && flow",
"postinstall": "jetify",
"test": "jest",
"validate": "npm ls"
},
"browser": {

View File

@@ -11,6 +11,7 @@ import '../base/dialog/reducer';
import '../base/flags/reducer';
import '../base/jwt/reducer';
import '../base/known-domains/reducer';
import '../base/lastn/reducer';
import '../base/lib-jitsi-meet/reducer';
import '../base/logging/reducer';
import '../base/media/reducer';

View File

@@ -122,6 +122,7 @@ export default [
'ignoreStartMuted',
'liveStreamingEnabled',
'localRecording',
'maxFullResolutionParticipants',
'minParticipants',
'nick',
'openBridgeChannel',

View File

@@ -0,0 +1,56 @@
/**
* Checks if the given Object is a correct last N limit mapping, coverts both keys and values to numbers and sorts
* the keys in ascending order.
*
* @param {Object} lastNLimits - The Object to be verified.
* @returns {undefined|Map<number, number>}
*/
export function validateLastNLimits(lastNLimits) {
// Checks if only numbers are used
if (typeof lastNLimits !== 'object'
|| !Object.keys(lastNLimits).length
|| Object.keys(lastNLimits)
.find(limit => limit === null || isNaN(Number(limit))
|| lastNLimits[limit] === null || isNaN(Number(lastNLimits[limit])))) {
return undefined;
}
// Converts to numbers and sorts the keys
const sortedMapping = new Map();
const orderedLimits = Object.keys(lastNLimits)
.map(n => Number(n))
.sort((n1, n2) => n1 - n2);
for (const limit of orderedLimits) {
sortedMapping.set(limit, Number(lastNLimits[limit]));
}
return sortedMapping;
}
/**
* Returns "last N" value which corresponds to a level defined in the {@code lastNLimits} mapping. See
* {@code config.js} for more detailed explanation on how the mapping is defined.
*
* @param {number} participantsCount - The current number of participants in the conference.
* @param {Map<number, number>} [lastNLimits] - The mapping of number of participants to "last N" values. NOTE that
* this function expects a Map that has been preprocessed by {@link validateLastNLimits}, because the keys must be
* sorted in ascending order and both keys and values should be numbers.
* @returns {number|undefined} - A "last N" number if there was a corresponding "last N" value matched with the number
* of participants or {@code undefined} otherwise.
*/
export function limitLastN(participantsCount, lastNLimits) {
if (!lastNLimits || !lastNLimits.keys) {
return undefined;
}
let selectedLimit;
for (const participantsN of lastNLimits.keys()) {
if (participantsCount >= participantsN) {
selectedLimit = participantsN;
}
}
return selectedLimit ? lastNLimits.get(selectedLimit) : undefined;
}

View File

@@ -0,0 +1,103 @@
import { limitLastN, validateLastNLimits } from './functions';
describe('limitLastN', () => {
it('handles undefined mapping', () => {
expect(limitLastN(0, undefined)).toBe(undefined);
});
describe('when a correct limit mapping is given', () => {
const limits = new Map();
limits.set(5, -1);
limits.set(10, 8);
limits.set(20, 5);
it('returns undefined when less participants that the first limit', () => {
expect(limitLastN(2, limits)).toBe(undefined);
});
it('picks the first limit correctly', () => {
expect(limitLastN(5, limits)).toBe(-1);
expect(limitLastN(9, limits)).toBe(-1);
});
it('picks the middle limit correctly', () => {
expect(limitLastN(10, limits)).toBe(8);
expect(limitLastN(13, limits)).toBe(8);
expect(limitLastN(19, limits)).toBe(8);
});
it('picks the top limit correctly', () => {
expect(limitLastN(20, limits)).toBe(5);
expect(limitLastN(23, limits)).toBe(5);
expect(limitLastN(100, limits)).toBe(5);
});
});
});
describe('validateLastNLimits', () => {
describe('validates the input by returning undefined', () => {
it('if lastNLimits param is not an Object', () => {
expect(validateLastNLimits(5)).toBe(undefined);
});
it('if any key is not a number', () => {
const limits = {
'abc': 8,
5: -1,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if any value is not a number', () => {
const limits = {
8: 'something',
5: -1,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if any value is null', () => {
const limits = {
1: 1,
5: null,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if any value is undefined', () => {
const limits = {
1: 1,
5: undefined,
20: 5
};
expect(validateLastNLimits(limits)).toBe(undefined);
});
it('if the map is empty', () => {
expect(validateLastNLimits({})).toBe(undefined);
});
});
it('sorts by the keys', () => {
const mappingKeys = validateLastNLimits({
10: 5,
3: 3,
5: 4
}).keys();
expect(mappingKeys.next().value).toBe(3);
expect(mappingKeys.next().value).toBe(5);
expect(mappingKeys.next().value).toBe(10);
expect(mappingKeys.next().done).toBe(true);
});
it('converts keys and values to numbers', () => {
const mapping = validateLastNLimits({
3: 3,
5: 4,
10: 5
});
for (const key of mapping.keys()) {
expect(typeof key).toBe('number');
expect(typeof mapping.get(key)).toBe('number');
}
});
});

View File

@@ -7,9 +7,18 @@ import { SCREEN_SHARE_PARTICIPANTS_UPDATED, SET_TILE_VIEW } from '../../video-la
import { shouldDisplayTileView } from '../../video-layout/functions';
import { SET_AUDIO_ONLY } from '../audio-only/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import { getParticipantById } from '../participants/functions';
import {
PARTICIPANT_JOINED,
PARTICIPANT_KICKED,
PARTICIPANT_LEFT
} from '../participants/actionTypes';
import {
getParticipantById,
getParticipantCount
} from '../participants/functions';
import { MiddlewareRegistry } from '../redux';
import { limitLastN } from './functions';
import logger from './logger';
declare var APP: Object;
@@ -21,6 +30,9 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_STATE_CHANGED:
case CONFERENCE_JOINED:
case PARTICIPANT_JOINED:
case PARTICIPANT_KICKED:
case PARTICIPANT_LEFT:
case SCREEN_SHARE_PARTICIPANTS_UPDATED:
case SELECT_LARGE_VIDEO_PARTICIPANT:
case SET_AUDIO_ONLY:
@@ -47,6 +59,8 @@ function _updateLastN({ getState }) {
const { appState } = state['features/background'] || {};
const { enabled: filmStripEnabled } = state['features/filmstrip'];
const config = state['features/base/config'];
const { lastNLimits } = state['features/base/lastn'];
const participantCount = getParticipantCount(state);
if (!conference) {
logger.debug('There is no active conference, not updating last N');
@@ -57,6 +71,13 @@ function _updateLastN({ getState }) {
const defaultLastN = typeof config.channelLastN === 'undefined' ? -1 : config.channelLastN;
let lastN = defaultLastN;
// Apply last N limit based on the # of participants
const limitedLastN = limitLastN(participantCount, lastNLimits);
if (limitedLastN !== undefined) {
lastN = limitedLastN;
}
if (typeof appState !== 'undefined' && appState !== 'active') {
lastN = 0;
} else if (audioOnly) {

View File

@@ -0,0 +1,27 @@
import {
SET_CONFIG
} from '../config';
import { ReducerRegistry, set } from '../redux';
import { validateLastNLimits } from './functions';
ReducerRegistry.register('features/base/lastn', (state = { }, action) => {
switch (action.type) {
case SET_CONFIG:
return _setConfig(state, action);
}
return state;
});
/**
* Reduces a specific Redux action SET_CONFIG.
*
* @param {Object} state - The Redux state of feature base/lastn.
* @param {Action} action - The Redux action SET_CONFIG to reduce.
* @private
* @returns {Object} The new state after the reduction of the specified action.
*/
function _setConfig(state, { config }) {
return set(state, 'lastNLimits', validateLastNLimits(config.lastNLimits));
}

View File

@@ -47,6 +47,11 @@ type Props = {
*/
_welcomePageIsVisible: boolean,
/**
* The default value for the Jitsi logo URL.
*/
defaultJitsiLogoURL: ?string,
/**
* Invoked to obtain translated strings.
*/
@@ -218,13 +223,14 @@ class Watermarks extends Component<Props, State> {
let reactElement = null;
const {
_customLogoUrl,
_customLogoLink
_customLogoLink,
defaultJitsiLogoURL
} = this.props;
if (this._canDisplayJitsiWatermark()) {
const link = _customLogoLink || this.state.jitsiWatermarkLink;
const style = {
backgroundImage: `url(${_customLogoUrl || interfaceConfig.DEFAULT_LOGO_URL})`,
backgroundImage: `url(${_customLogoUrl || defaultJitsiLogoURL || interfaceConfig.DEFAULT_LOGO_URL})`,
maxWidth: 140,
maxHeight: 70
};

View File

@@ -72,7 +72,7 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
*/
render() {
const { t } = this.props;
const { NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig;
const { HIDE_DEEP_LINKING_LOGO, NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig;
const rightColumnStyle
= SHOW_DEEP_LINKING_IMAGE ? null : { width: '100%' };
@@ -82,9 +82,13 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
<AtlasKitThemeProvider mode = 'light'>
<div className = 'deep-linking-desktop'>
<div className = 'header'>
<img
className = 'logo'
src = 'images/logo-deep-linking.png' />
{
HIDE_DEEP_LINKING_LOGO
? null
: <img
className = 'logo'
src = 'images/logo-deep-linking.png' />
}
</div>
<div className = 'content'>
{

View File

@@ -91,7 +91,7 @@ class DeepLinkingMobilePage extends Component<Props> {
*/
render() {
const { _downloadUrl, _room, t } = this.props;
const { NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig;
const { HIDE_DEEP_LINKING_LOGO, NATIVE_APP_NAME, SHOW_DEEP_LINKING_IMAGE } = interfaceConfig;
const downloadButtonClassName
= `${_SNS}__button ${_SNS}__button_primary`;
@@ -115,9 +115,13 @@ class DeepLinkingMobilePage extends Component<Props> {
return (
<div className = { _SNS }>
<div className = 'header'>
<img
className = 'logo'
src = 'images/logo-deep-linking.png' />
{
HIDE_DEEP_LINKING_LOGO
? null
: <img
className = 'logo'
src = 'images/logo-deep-linking.png' />
}
</div>
<div className = { `${_SNS}__body` }>
{

View File

@@ -1,7 +1,8 @@
// @flow
import { FlagGroup } from '@atlaskit/flag';
import Portal from '@atlaskit/portal';
import React from 'react';
import { Transition, TransitionGroup } from 'react-transition-group';
import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, {
@@ -27,6 +28,16 @@ type Props = AbstractProps & {
* @extends {Component}
*/
class NotificationsContainer extends AbstractNotificationsContainer<Props> {
/**
* Creates new NotificationContainer instance.
*
* @param {Props} props - The props of the react component.
*/
constructor(props: Props) {
super(props);
this._renderNotification = this._renderNotification.bind(this);
}
/**
* Implements React's {@link Component#render()}.
@@ -40,39 +51,92 @@ class NotificationsContainer extends AbstractNotificationsContainer<Props> {
}
return (
<FlagGroup onDismissed = { this._onDismissed }>
{ this._renderFlags() }
</FlagGroup>
<Portal zIndex = { 600 }>
<div className = 'notificationsContainer'>
{ this._renderTopNotificationsContainer() }
{ this._renderBottomNotificationsContainer() }
</div>
</Portal>
);
}
_onDismissed: number => void;
/**
* Renders notifications to display as ReactElements. An empty array will
* be returned if notifications are disabled.
* Renders the bottom notification container.
*
* @private
* @returns {ReactElement[]}
* @returns {ReactElement}
*/
_renderFlags() {
_renderBottomNotificationsContainer() {
const { _notifications } = this.props;
return _notifications.map(notification => {
const { props, uid } = notification;
return (
<TransitionGroup className = 'bottomContainer'>
{
_notifications.filter(n => n.props.position !== 'top').map((notification, index) => {
const { props, uid } = notification;
// The id attribute is necessary as {@code FlagGroup} looks for
// either id or key to set a key on notifications, but accessing
// props.key will cause React to print an error.
return (
<Notification
{ ...props }
id = { uid }
key = { uid }
uid = { uid } />
return this._renderNotification({
...props,
isDismissAllowed: index > 0 ? false : props.isDismissAllowed
}, uid);
})
}
</TransitionGroup>
);
}
);
});
_renderNotification: (string, number) => Function;
/**
* Renders a notification.
*
* @param {Object} props - The props for the Notification component.
* @param {string} uid - A unique ID for the notification.
* @returns {Function} - Returns a transition function for the Transition component.
*/
_renderNotification(props, uid) {
return (
<Transition
key = { uid }
timeout = { 400 }>
{
transitionState => (
<div className = { `notification ${transitionState}` }>
<Notification
{ ...props }
id = { uid }
key = { uid }
onDismissed = { this._onDismissed }
uid = { uid } />
</div>
)
}
</Transition>
);
}
/**
* Renders the top notifications container.
*
* @private
* @returns {ReactElement}
*/
_renderTopNotificationsContainer() {
const { _notifications } = this.props;
return (
<TransitionGroup className = 'topContainer'>
{
_notifications.filter(n => n.props.position === 'top').map(notification => {
const { props, uid } = notification;
return this._renderNotification(props, uid);
})
}
</TransitionGroup>
);
}
}

View File

@@ -11,6 +11,7 @@ import { ActionButton, InputField, PreMeetingScreen, ToggleButton } from '../../
import { connect } from '../../base/redux';
import { getDisplayName, updateSettings } from '../../base/settings';
import { getLocalJitsiVideoTrack } from '../../base/tracks';
import { isButtonEnabled } from '../../toolbox/functions.web';
import {
joinConference as joinConferenceAction,
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
@@ -28,6 +29,8 @@ import {
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
import DeviceStatus from './preview/DeviceStatus';
declare var interfaceConfig: Object;
type Props = {
/**
@@ -100,6 +103,11 @@ type Props = {
*/
showJoinActions: boolean,
/**
* Flag signaling the visibility of the conference URL section.
*/
showConferenceInfo: boolean,
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
@@ -139,6 +147,7 @@ class Prejoin extends Component<Props, State> {
* @static
*/
static defaultProps = {
showConferenceInfo: true,
showJoinActions: true,
showSkipPrejoin: true
};
@@ -257,6 +266,7 @@ class Prejoin extends Component<Props, State> {
showAvatar,
showCameraPreview,
showDialog,
showConferenceInfo,
showJoinActions,
t,
videoTrack
@@ -270,7 +280,7 @@ class Prejoin extends Component<Props, State> {
footer = { this._renderFooter() }
name = { name }
showAvatar = { showAvatar }
showConferenceInfo = { showJoinActions }
showConferenceInfo = { showConferenceInfo }
skipPrejoinButton = { this._renderSkipPrejoinButton() }
title = { t('prejoin.joinMeeting') }
videoMuted = { !showCameraPreview }
@@ -368,11 +378,22 @@ class Prejoin extends Component<Props, State> {
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The props passed to the component.
* @returns {Object}
*/
function mapStateToProps(state): Object {
function mapStateToProps(state, ownProps): Object {
const name = getDisplayName(state);
const joinButtonDisabled = isDisplayNameRequired(state) && !name;
const { showJoinActions } = ownProps;
const isInviteButtonEnabled = isButtonEnabled('invite');
// Hide conference info when interfaceConfig is available and the invite button is disabled.
// In all other cases we want to preserve the behaviour and control the the conference info
// visibility trough showJoinActions.
const showConferenceInfo
= typeof isInviteButtonEnabled === 'undefined' || isInviteButtonEnabled === true
? showJoinActions
: false;
return {
buttonIsToggled: isPrejoinSkipped(state),
@@ -383,6 +404,7 @@ function mapStateToProps(state): Object {
showDialog: isJoinByPhoneDialogVisible(state),
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
showCameraPreview: !isVideoMutedByUser(state),
showConferenceInfo,
videoTrack: getLocalJitsiVideoTrack(state)
};
}

View File

@@ -46,3 +46,17 @@ export const SET_PENDING_RECORDING_NOTIFICATION_UID
* }
*/
export const SET_STREAM_KEY = 'SET_STREAM_KEY';
/**
* The type of Redux action which sets the waiting in queue recording notification UID to
* use it for when hiding the notification is necessary, or unsets it when
* undefined (or no param) is passed.
*
* {
* type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: ?number
* }
* @public
*/
export const SET_WAITING_IN_RECORDING_NOTIFICATION_UID = 'SET_WAITING_IN_RECORDING_NOTIFICATION_UID';

View File

@@ -1,8 +1,12 @@
// @flow
import React from 'react';
import { openDialog } from '../base/dialog';
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import {
NOTIFICATION_TIMEOUT,
NOTIFICATION_TYPE,
hideNotification,
showErrorNotification,
showNotification
@@ -12,8 +16,10 @@ import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
SET_STREAM_KEY,
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
} from './actionTypes';
import { QueueInfo, StopLiveStreamDialog, StopRecordingDialog } from './components';
/**
* Clears the data of every recording sessions.
@@ -50,6 +56,25 @@ export function hidePendingRecordingNotification(streamType: string) {
};
}
/**
* Signals that the waiting in queue recording notification should be removed from the screen.
*
* @param {string} streamType - The type of the stream ({@code 'file'} or
* {@code 'stream'}).
* @returns {Function}
*/
export function hideWaitingInQueueRecordingNotification(streamType: string) {
return (dispatch: Function, getState: Function) => {
const { waitingInQueueNotificationUids } = getState()['features/recording'];
const waitingInQueueNotificationUid = waitingInQueueNotificationUids[streamType];
if (waitingInQueueNotificationUid) {
dispatch(hideNotification(waitingInQueueNotificationUid));
dispatch(_setWaitingInQueueRecordingNotificationUid(undefined, streamType));
}
};
}
/**
* Sets the stream key last used by the user for later reuse.
*
@@ -97,6 +122,22 @@ export function showPendingRecordingNotification(streamType: string) {
};
}
/**
* Signals that the jibri queue has been left and notification should be shown on the
* screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {showNotification}
*/
export function showQueueLeftRecordingNotification(streamType: string) {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
return showNotification({
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.left`
}, NOTIFICATION_TIMEOUT);
}
/**
* Signals that the recording error notification should be shown.
*
@@ -175,6 +216,15 @@ export function updateRecordingSessionData(session: Object) {
= status === JitsiRecordingConstants.status.ON
? Date.now() / 1000
: undefined;
const queueID = session.getQueueID();
let queueEstimatedTimeOfStart, queuePosition;
if (status === JitsiRecordingConstants.status.WAITING_IN_QUEUE) {
const { position, estimatedTimeLeft } = session.getQueueMetrics();
queuePosition = position;
queueEstimatedTimeOfStart = (new Date()).getTime() + (estimatedTimeLeft * 1000);
}
return {
type: RECORDING_SESSION_UPDATED,
@@ -186,7 +236,10 @@ export function updateRecordingSessionData(session: Object) {
mode: session.getMode(),
status,
terminator: session.getTerminator(),
timestamp
timestamp,
queueID,
queuePosition,
queueEstimatedTimeOfStart
}
};
}
@@ -212,3 +265,57 @@ function _setPendingRecordingNotificationUid(uid: ?number, streamType: string) {
uid
};
}
/**
* Sets UID of the the pending streaming notification to use it when hiding
* the notification is necessary, or unsets it when undefined (or no param) is
* passed.
*
* @param {?number} uid - The UID of the notification.
* @param {string} streamType - The type of the stream ({@code file} or {@code stream}).
* @returns {{
* type: SET_PENDING_RECORDING_NOTIFICATION_UID,
* streamType: string,
* uid: number
* }}
*/
function _setWaitingInQueueRecordingNotificationUid(uid: ?number, streamType: string) {
return {
type: SET_WAITING_IN_RECORDING_NOTIFICATION_UID,
streamType,
uid
};
}
/**
* Signals that the recording queue notification should be shown on the screen.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {Function}
*/
export function showWaitingInQueueRecordingNotification(streamType: string) {
return (dispatch: Function) => {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
const showNotificationAction = showNotification({
appearance: NOTIFICATION_TYPE.INFO,
customActionNameKey: 'jibriQueue.exit',
customActionHandler: () => {
if (isLiveStreaming) {
dispatch(openDialog(StopLiveStreamDialog));
} else {
dispatch(openDialog(StopRecordingDialog));
}
return false;
},
position: 'top',
titleKey: `jibriQueue.${isLiveStreaming ? 'livestreaming' : 'recording'}.title`,
description: <QueueInfo />
});
dispatch(showNotificationAction);
dispatch(_setWaitingInQueueRecordingNotificationUid(
showNotificationAction.uid, streamType));
};
}

View File

@@ -217,6 +217,8 @@ export default class AbstractStartLiveStreamDialog<P: Props>
broadcastId: selectedBroadcastID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: key
}).catch(() => {
// prevent unhandled promise rejection.
});
return true;

View File

@@ -65,7 +65,9 @@ export default class AbstractStopLiveStreamDialog extends Component<Props> {
const { _session } = this.props;
if (_session) {
this.props._conference.stopRecording(_session.id);
this.props._conference.stopRecording(_session.id, _session.queueID).catch(() => {
// prevent unhandled promise rejection.
});
}
return true;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import AbstractStopLiveStreamDialog, {
_mapStateToProps
@@ -24,13 +25,17 @@ class StopLiveStreamDialog extends AbstractStopLiveStreamDialog {
* @returns {ReactElement}
*/
render() {
const { _session = {}, t } = this.props;
const isInQueue = _session.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
return (
<Dialog
okKey = 'dialog.stopLiveStreaming'
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.stopLiveStreaming' }
onSubmit = { this._onSubmit }
titleKey = 'dialog.liveStreaming'
width = 'small'>
{ this.props.t('dialog.stopStreamingWarning') }
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopStreamingWarning') }
</Dialog>
);
}

View File

@@ -280,6 +280,8 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
_conference.startRecording({
mode: JitsiRecordingConstants.mode.FILE,
appData
}).catch(() => {
// prevent unhandled promise rejection.
});
if (_autoCaptionOnRecord) {

View File

@@ -65,7 +65,9 @@ export default class AbstractStopRecordingDialog<P: Props>
const { _fileRecordingSession } = this.props;
if (_fileRecordingSession) {
this.props._conference.stopRecording(_fileRecordingSession.id);
this.props._conference.stopRecording(_fileRecordingSession.id, _fileRecordingSession.queueID).catch(() => {
// prevent unhandled promise rejection.
});
}
return true;

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import AbstractStopRecordingDialog, {
type Props,
@@ -24,15 +25,17 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { _fileRecordingSession = {}, t } = this.props;
const isInQueue = _fileRecordingSession.status === JitsiRecordingConstants.status.WAITING_IN_QUEUE;
return (
<Dialog
okKey = 'dialog.confirm'
okKey = { isInQueue ? 'dialog.leaveJibriQueue' : 'dialog.confirm' }
onSubmit = { this._onSubmit }
titleKey = 'dialog.recording'
width = 'small'>
{ t('dialog.stopRecordingWarning') }
{ t(isInQueue ? 'dialog.leaveJibriQueueWarning' : 'dialog.stopRecordingWarning') }
</Dialog>
);
}

View File

@@ -0,0 +1,230 @@
// @flow
import React, { Component } from 'react';
import { getLocalizedDurationFormatter, translate } from '../../../base/i18n';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { connect } from '../../../base/redux';
import { getActiveSession } from '../../functions';
type Props = {
/**
* The current position of the participant in the queue.
*/
_position: ?string,
/**
* The recording mode.
*/
_mode: string,
/**
* The ID of the queue.
*/
_queueID: string,
/**
* The time when the recording is expected to start.
*/
_estimatedTimeOfStart: number,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link QueueInfo}.
*/
type State = {
/**
* The current value of the timer for estimated time left.
*/
timerValue: ?string
};
/**
* Implements a React {@link Component} which displays the current state of the Jibri Queue.
*
* @extends {Component}
*/
class QueueInfo extends Component<Props, State> {
/**
* Handle for setInterval timer.
*/
_interval: IntervalID;
/**
* Initializes a new {@code QueueInfo} instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
timerValue: undefined
};
}
/**
* Stops the timer when component will be unmounted.
*
* @inheritdoc
*/
componentWillUnmount() {
this._stopTimer();
}
/**
* Starts the timer when component will be mounted.
*
* @inheritdoc
*/
componentDidMount() {
if (typeof this.props._estimatedTimeOfStart !== 'undefined') {
this._startTimer();
}
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props._estimatedTimeOfStart !== prevProps._estimatedTimeOfStart) {
this._stopTimer(false);
this._startTimer();
}
}
/**
* Sets the current state values that will be used to render the timer.
*
* @param {number} refValueUTC - The initial UTC timestamp value.
* @param {number} currentValueUTC - The current UTC timestamp value.
*
* @returns {void}
*/
_setStateFromUTC(refValueUTC, currentValueUTC) {
if (!refValueUTC || !currentValueUTC) {
return;
}
const timerMsValue = currentValueUTC > refValueUTC ? 0 : refValueUTC - currentValueUTC;
const localizedTime = getLocalizedDurationFormatter(timerMsValue);
this.setState({
timerValue: localizedTime
});
}
/**
* Starts the timer.
*
* @returns {void}
*/
_startTimer() {
const { _estimatedTimeOfStart } = this.props;
if (!this._interval && typeof _estimatedTimeOfStart !== 'undefined') {
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
this._interval = setInterval(() => {
this._setStateFromUTC(_estimatedTimeOfStart, (new Date()).getTime());
}, 1000);
}
}
/**
* Stops the timer.
*
* @param {boolean} [clearState] - If true, the timer value in the state will be cleared.
* @returns {void}
*/
_stopTimer(clearState = true) {
if (this._interval) {
clearInterval(this._interval);
delete this._interval;
}
if (clearState) {
this.setState({
timerValue: undefined
});
}
}
/**
* Implements React {@code Component}'s render.
*
* @inheritdoc
*/
render() {
const { _estimatedTimeOfStart, _mode, _position = 0, t } = this.props;
const { STREAM } = JitsiRecordingConstants.mode;
const timeTextKey = `jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.time`;
const { timerValue } = this.state;
const footerText = t(`jibriQueue.${_mode === STREAM ? 'livestreaming' : 'recording'}.footer`);
const showFooter = typeof footerText === 'string' && footerText.length > 0;
return (
<div className = 'jibri-queue-info'>
<span className = 'position'>
{ t('jibriQueue.position', { count: _position }) }
</span>
{
typeof _estimatedTimeOfStart === 'undefined' || timerValue === 'undefined'
? null : <span className = 'time'>
{ t(timeTextKey, { time: timerValue }) }
</span>
}
{
showFooter ? <div className = 'footer'>{ footerText }</div> : null
}
</div>
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code AbstractRecordingLabel}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _estimatedTimeOfStart: number,
* _mode: string,
* _position: string,
* _queueID: string,
* t: Function
* }}
*/
export function _mapStateToProps(state: Object) {
const session = getActiveSession(state);
if (!session) {
return {};
}
const { id, mode, queueEstimatedTimeOfStart, queueID, queuePosition } = session;
return {
_sessionID: id,
_mode: mode,
_queueID: queueID,
_position: queuePosition,
_estimatedTimeOfStart: queueEstimatedTimeOfStart
};
}
export default translate(connect(_mapStateToProps)(QueueInfo));

View File

@@ -2,3 +2,4 @@
export { default as RecordingLabel } from './RecordingLabel';
export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';
export { default as QueueInfo } from './QueueInfo';

View File

@@ -9,16 +9,17 @@ import { RECORDING_STATUS_PRIORITIES } from './constants';
* passed in mode.
*
* @param {Object} state - The redux state to search in.
* @param {string} mode - Find an active recording session of the given mode.
* @param {string|undefined} mode - Find an active recording session of the given mode.
* @returns {Object|undefined}
*/
export function getActiveSession(state: Object, mode: string) {
export function getActiveSession(state: Object, mode: ?string) {
const { sessionDatas } = state['features/recording'];
const { status: statusConstants } = JitsiRecordingConstants;
return sessionDatas.find(sessionData => sessionData.mode === mode
return sessionDatas.find(sessionData => (typeof mode === 'undefined' || sessionData.mode === mode)
&& (sessionData.status === statusConstants.ON
|| sessionData.status === statusConstants.PENDING));
|| sessionData.status === statusConstants.PENDING
|| sessionData.status === statusConstants.WAITING_IN_QUEUE));
}
/**
@@ -37,6 +38,8 @@ export function getRecordingDurationEstimation(size: ?number) {
* Searches in the passed in redux state for a recording session that matches
* the passed in recording session ID.
*
* NOTE: The sessoins in WAITING_IN_QUEUE status don't have ID yet.
*
* @param {Object} state - The redux state to search in.
* @param {string} id - The ID of the recording session to find.
* @returns {Object|undefined}
@@ -51,6 +54,8 @@ export function getSessionById(state: Object, id: string) {
* there is a session with the status OFF and one with PENDING, then the PENDING
* one will be shown, because that is likely more important for the user to see.
*
* NOTE: For all "queue" statuses the function returns undefined because we don't want to show label.
*
* @param {Object} state - The redux state to search in.
* @param {string} mode - The recording mode to get status for.
* @returns {string|undefined}

View File

@@ -24,11 +24,14 @@ import { RECORDING_SESSION_UPDATED } from './actionTypes';
import {
clearRecordingSessions,
hidePendingRecordingNotification,
hideWaitingInQueueRecordingNotification,
showPendingRecordingNotification,
showQueueLeftRecordingNotification,
showRecordingError,
showRecordingLimitNotification,
showStartedRecordingNotification,
showStoppedRecordingNotification,
showWaitingInQueueRecordingNotification,
updateRecordingSessionData
} from './actions';
import {
@@ -110,15 +113,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
conference.on(
JitsiConferenceEvents.RECORDER_STATE_CHANGED,
recorderSession => {
if (recorderSession) {
recorderSession.getID()
&& dispatch(
if (recorderSession.getID() || recorderSession.getQueueID()) {
dispatch(
updateRecordingSessionData(recorderSession));
}
recorderSession.getError()
&& _showRecordingErrorNotification(
if (recorderSession.getError()) {
_showRecordingErrorNotification(
recorderSession, dispatch);
}
}
return;
@@ -142,75 +146,91 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
break;
}
const updatedSessionData
= getSessionById(getState(), action.sessionData.id);
const { initiator, mode, terminator } = updatedSessionData;
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
const updatedSessionData = getSessionById(getState(), action.sessionData.id);
const { initiator, mode, status: newStatus, terminator } = updatedSessionData;
const { PENDING, OFF, ON, WAITING_IN_QUEUE, QUEUE_LEFT } = JitsiRecordingConstants.status;
if (updatedSessionData.status === PENDING
&& (!oldSessionData || oldSessionData.status !== PENDING)) {
dispatch(showPendingRecordingNotification(mode));
} else if (updatedSessionData.status !== PENDING) {
if (oldSessionData && oldSessionData.status === newStatus) {
return result;
}
if (newStatus !== WAITING_IN_QUEUE) {
dispatch(hideWaitingInQueueRecordingNotification(mode));
}
if (newStatus !== PENDING) {
dispatch(hidePendingRecordingNotification(mode));
}
if (updatedSessionData.status === ON
&& (!oldSessionData || oldSessionData.status !== ON)) {
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
switch (newStatus) {
case WAITING_IN_QUEUE:
dispatch(showWaitingInQueueRecordingNotification(mode));
break;
case QUEUE_LEFT:
dispatch(showQueueLeftRecordingNotification(mode));
break;
case PENDING:
dispatch(showPendingRecordingNotification(mode));
break;
case ON: {
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
break;
}
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundID) {
dispatch(playSound(soundID));
}
} else if (updatedSessionData.status === OFF
&& (!oldSessionData || oldSessionData.status !== OFF)) {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
break;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
soundOn = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundOff && soundOn) {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
return result;
}
let soundID;
if (mode === JitsiRecordingConstants.mode.FILE) {
soundID = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundID = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundID) {
dispatch(playSound(soundID));
}
break;
}
case OFF: {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {
duration
= (Date.now() / 1000) - oldSessionData.timestamp;
}
sendAnalytics(createRecordingEvent('stop', mode, duration));
if (disableRecordAudioNotification) {
return result;
}
if (mode === JitsiRecordingConstants.mode.FILE) {
soundOff = RECORDING_OFF_SOUND_ID;
soundOn = RECORDING_ON_SOUND_ID;
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
soundOn = LIVE_STREAMING_ON_SOUND_ID;
}
if (soundOff && soundOn) {
dispatch(stopSound(soundOn));
dispatch(playSound(soundOff));
}
break;
}
}
break;

View File

@@ -4,12 +4,14 @@ import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
SET_STREAM_KEY,
SET_WAITING_IN_RECORDING_NOTIFICATION_UID
} from './actionTypes';
const DEFAULT_STATE = {
pendingNotificationUids: {},
sessionDatas: []
sessionDatas: [],
waitingInQueueNotificationUids: {}
};
/**
@@ -56,6 +58,20 @@ ReducerRegistry.register(STORE_NAME,
streamKey: action.streamKey
};
case SET_WAITING_IN_RECORDING_NOTIFICATION_UID: {
const waitingInQueueNotificationUids = {
...state.waitingInQueueNotificationUids
};
waitingInQueueNotificationUids[action.streamType] = action.uid;
return {
...state,
waitingInQueueNotificationUids
};
}
default:
return state;
}
@@ -71,12 +87,12 @@ ReducerRegistry.register(STORE_NAME,
*/
function _updateSessionDatas(sessionDatas, newSessionData) {
const hasExistingSessionData = sessionDatas.find(
sessionData => sessionData.id === newSessionData.id);
sessionData => sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID);
let newSessionDatas;
if (hasExistingSessionData) {
newSessionDatas = sessionDatas.map(sessionData => {
if (sessionData.id === newSessionData.id) {
if (sessionData.id === newSessionData.id || sessionData.queueID === newSessionData.queueID) {
return {
...newSessionData
};

View File

@@ -9,6 +9,7 @@ import {
sendAnalytics
} from '../../../analytics';
import { openDialog, toggleDialog } from '../../../base/dialog';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import {
IconChat,
@@ -1233,6 +1234,8 @@ class Toolbox extends Component<Props, State> {
/ 2 // divide by the number of groups(left and right group)
);
const showOverflowMenu = this.state.windowWidth >= verySmallThreshold || isMobileBrowser();
if (this._shouldShowButton('chat')) {
buttonsLeft.push('chat');
}
@@ -1246,7 +1249,7 @@ class Toolbox extends Component<Props, State> {
if (this._shouldShowButton('closedcaptions')) {
buttonsLeft.push('closedcaptions');
}
if (overflowHasItems && this.state.windowWidth >= verySmallThreshold) {
if (overflowHasItems && showOverflowMenu) {
buttonsRight.push('overflowmenu');
}
if (this._shouldShowButton('invite')) {
@@ -1269,13 +1272,13 @@ class Toolbox extends Component<Props, State> {
movedButtons.push(...buttonsLeft.splice(
maxNumberOfButtonsPerGroup,
buttonsLeft.length - maxNumberOfButtonsPerGroup));
if (buttonsRight.indexOf('overflowmenu') === -1 && this.state.windowWidth >= verySmallThreshold) {
if (buttonsRight.indexOf('overflowmenu') === -1 && showOverflowMenu) {
buttonsRight.unshift('overflowmenu');
}
}
if (buttonsRight.length > maxNumberOfButtonsPerGroup) {
if (buttonsRight.indexOf('overflowmenu') === -1 && this.state.windowWidth >= verySmallThreshold) {
if (buttonsRight.indexOf('overflowmenu') === -1 && showOverflowMenu) {
buttonsRight.unshift('overflowmenu');
}
@@ -1346,7 +1349,7 @@ class Toolbox extends Component<Props, State> {
tooltip = { t('toolbar.invite') } /> }
{ buttonsRight.indexOf('security') !== -1
&& <SecurityDialogButton customClass = 'security-toolbar-button' /> }
{ buttonsRight.indexOf('overflowmenu') !== -1 && this.state.windowWidth >= verySmallThreshold
{ buttonsRight.indexOf('overflowmenu') !== -1
&& <OverflowMenuButton
isOpen = { _overflowMenuVisible }
onVisibilityChange = { this._onSetOverflowVisible }>

View File

@@ -21,11 +21,16 @@ export function getToolboxHeight() {
*
* @param {string} name - The name of the setting section as defined in
* interface_config.js.
* @returns {boolean} - True to indicate that the given toolbar button
* is enabled, false - otherwise.
* @returns {boolean|undefined} - True to indicate that the given toolbar button
* is enabled, false - otherwise. In cases where interfaceConfig is not available
* undefined is returned.
*/
export function isButtonEnabled(name: string) {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1;
if (typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1;
}
return undefined;
}

View File

@@ -7,6 +7,7 @@ import {
setMaxReceiverVideoQuality,
setPreferredVideoQuality
} from '../base/conference';
import { getParticipantCount } from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { shouldDisplayTileView } from '../video-layout';
@@ -46,21 +47,42 @@ StateListenerRegistry.register(
const { reducedUI } = state['features/base/responsive-ui'];
const _shouldDisplayTileView = shouldDisplayTileView(state);
const thumbnailSize = state['features/filmstrip']?.tileViewDimensions?.thumbnailSize;
const participantCount = getParticipantCount(state);
return {
displayTileView: _shouldDisplayTileView,
participantCount,
reducedUI,
thumbnailHeight: thumbnailSize?.height
};
},
/* listener */ ({ displayTileView, reducedUI, thumbnailHeight }, { dispatch, getState }) => {
const { maxReceiverVideoQuality } = getState()['features/base/conference'];
/* listener */ ({ displayTileView, participantCount, reducedUI, thumbnailHeight }, { dispatch, getState }) => {
const state = getState();
const { maxReceiverVideoQuality } = state['features/base/conference'];
const { maxFullResolutionParticipants = 2 } = state['features/base/config'];
let newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.HIGH;
if (reducedUI) {
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
} else if (displayTileView && !Number.isNaN(thumbnailHeight)) {
newMaxRecvVideoQuality = getNearestReceiverVideoQualityLevel(thumbnailHeight);
// Override HD level calculated for the thumbnail height when # of participants threshold is exceeded
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality && maxFullResolutionParticipants !== -1) {
const override
= participantCount > maxFullResolutionParticipants
&& newMaxRecvVideoQuality > VIDEO_QUALITY_LEVELS.STANDARD;
logger.info(`The nearest receiver video quality level for thumbnail height: ${thumbnailHeight}, `
+ `is: ${newMaxRecvVideoQuality}, `
+ `override: ${String(override)}, `
+ `max full res N: ${maxFullResolutionParticipants}`);
if (override) {
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.STANDARD;
}
}
}
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality) {

View File

@@ -159,7 +159,7 @@ class WelcomePage extends AbstractWelcomePage {
*/
render() {
const { _moderatedRoomServiceUrl, t } = this.props;
const { APP_NAME } = interfaceConfig;
const { APP_NAME, DEFAULT_WELCOME_PAGE_LOGO_URL } = interfaceConfig;
const showAdditionalContent = this._shouldShowAdditionalContent();
const showAdditionalToolbarContent = this._shouldShowAdditionalToolbarContent();
const showResponsiveText = this._shouldShowResponsiveText();
@@ -170,7 +170,7 @@ class WelcomePage extends AbstractWelcomePage {
? 'with-content' : 'without-content'}` }
id = 'welcome_page'>
<div className = 'welcome-watermark'>
<Watermarks />
<Watermarks defaultJitsiLogoURL = { DEFAULT_WELCOME_PAGE_LOGO_URL } />
</div>
<div className = 'header'>
<div className = 'welcome-page-settings'>

View File

@@ -19,7 +19,6 @@ local http_headers = {
-- TODO: Figure out a less arbitrary default cache size.
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
local cache = require"util.cache".new(cacheSize);
local Util = {}
Util.__index = Util
@@ -38,6 +37,8 @@ function Util.new(module)
self.asapKeyServer = module:get_option_string("asap_key_server");
self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
self.cache = require"util.cache".new(cacheSize);
--[[
Multidomain can be supported in some deployments. In these deployments
there is a virtual conference muc, which address contains the subdomain
@@ -108,7 +109,7 @@ end
-- @param keyId the key ID to request
-- @return the public key (the content of requested resource) or nil
function Util:get_public_key(keyId)
local content = cache:get(keyId);
local content = self.cache:get(keyId);
if content == nil then
-- If the key is not found in the cache.
module:log("debug", "Cache miss for key: "..keyId);
@@ -117,7 +118,10 @@ function Util:get_public_key(keyId)
local function cb(content_, code_, response_, request_)
content, code = content_, code_;
if code == 200 or code == 204 then
cache:set(keyId, content);
self.cache:set(keyId, content);
else
module:log("warn", "Error on public key request: Code %s, Content %s",
code_, content_);
end
done();
end
@@ -138,7 +142,10 @@ function Util:get_public_key(keyId)
-- TODO: This check is racey. Not likely to be a problem, but we should
-- still stick a mutex on content / code at some point.
if code == nil then
http.destroy_request(request);
-- no longer present in prosody 0.11, so check before calling
if http.destroy_request ~= nil then
http.destroy_request(request);
end
done();
end
end

View File

@@ -4,7 +4,7 @@
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="/base.html" -->
<link rel="stylesheet" href="../css/all.css">
<link rel="stylesheet" href="css/all.css">
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!JitsiMeetJS.app) {