Compare commits

..

7 Commits

Author SHA1 Message Date
paweldomas
c2191e3a28 callkit with base/session 2018-08-07 12:20:38 -05:00
paweldomas
72e3e8593d feat(base/session): store 'room' in the session
Stores name of the conference room in the session when it's being
created.
2018-08-07 12:20:38 -05:00
paweldomas
67a8b4915d feat(base/session): add SESSION_CONFIGURED event
The SESSION_CONFIGURED event is fired once the config has been set,
after either being loaded or restored from the storage.
2018-08-07 12:20:38 -05:00
paweldomas
468d4a7150 ref(mobile/external-api): use base/session 2018-08-07 12:20:38 -05:00
paweldomas
2a01e29fec feat: add features/base/session 2018-08-07 12:20:38 -05:00
paweldomas
90a64d30dc ref(base/config): keep 'locationURL' after SET_CONFIG
This is required for the session feature to be able to tell what's
the latest URL the app is working with.
2018-08-07 12:20:38 -05:00
paweldomas
31905d4f63 debug actions 2018-08-07 12:20:38 -05:00
266 changed files with 4187 additions and 13774 deletions

View File

@@ -2,7 +2,6 @@ BUILD_DIR = build
CLEANCSS = ./node_modules/.bin/cleancss
DEPLOY_DIR = libs
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
NODE_SASS = ./node_modules/.bin/node-sass
NPM = npm
OUTPUT_DIR = .
@@ -20,7 +19,7 @@ compile:
clean:
rm -fr $(BUILD_DIR)
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local
deploy-init:
rm -fr $(DEPLOY_DIR)
@@ -34,8 +33,6 @@ deploy-appbundle:
$(BUILD_DIR)/do_external_connect.min.map \
$(BUILD_DIR)/external_api.min.js \
$(BUILD_DIR)/external_api.min.map \
$(BUILD_DIR)/flacEncodeWorker.min.js \
$(BUILD_DIR)/flacEncodeWorker.min.map \
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
$(BUILD_DIR)/dial_in_info_bundle.min.js \
@@ -53,12 +50,6 @@ deploy-lib-jitsi-meet:
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
$(DEPLOY_DIR)
deploy-libflac:
cp \
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js \
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js.mem \
$(DEPLOY_DIR)
deploy-css:
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
@@ -67,7 +58,7 @@ deploy-css:
deploy-local:
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
dev: deploy-init deploy-css deploy-lib-jitsi-meet deploy-libflac
dev: deploy-init deploy-css deploy-lib-jitsi-meet
$(WEBPACK_DEV_SERVER)
source-package:

View File

@@ -41,15 +41,6 @@ android {
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.google.android.gms:play-services-auth:15.0.0'
implementation project(':sdk')
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
}
if (project.file('google-services.json').exists()) {
apply plugin: 'com.google.gms.google-services'
}

View File

@@ -5,7 +5,6 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".MainApplication"
android:theme="@style/AppTheme">
<activity
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"

View File

@@ -27,9 +27,11 @@ import org.jitsi.meet.sdk.invite.AddPeopleControllerListener;
import org.jitsi.meet.sdk.invite.InviteController;
import org.jitsi.meet.sdk.invite.InviteControllerListener;
import com.calendarevents.CalendarEventsPackage;
import com.facebook.react.bridge.UiThreadUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -227,4 +229,20 @@ public class MainActivity extends JitsiMeetActivity {
addPeopleController.endAddPeople();
}
}
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults) {
CalendarEventsPackage.onRequestPermissionsResult(
requestCode,
permissions,
grantResults);
super.onRequestPermissionsResult(
requestCode,
permissions,
grantResults);
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* 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;
import android.app.Application;
import com.squareup.leakcanary.LeakCanary;
/**
* Simple {@link Application} for hooking up LeakCanary:
* https://github.com/square/leakcanary
*/
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (!LeakCanary.isInAnalyzerProcess(this)) {
LeakCanary.install(this);
}
}
}

View File

@@ -8,7 +8,6 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.google.gms:google-services:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files.
@@ -17,7 +16,6 @@ buildscript {
allprojects {
repositories {
maven { url "https://maven.google.com" }
google()
jcenter()
maven { url "$rootDir/../node_modules/jsc-android/dist" }

View File

@@ -26,9 +26,6 @@ dependencies {
compile project(':react-native-background-timer')
compile project(':react-native-fast-image')
compile(project(":react-native-google-signin")) {
exclude group: 'com.google.android.gms'
}
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-linear-gradient')

View File

@@ -27,7 +27,6 @@ import android.view.KeyEvent;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;
import java.net.URL;
@@ -43,7 +42,7 @@ import java.net.URL;
* {@code JitsiMeetView} static methods.
*/
public class JitsiMeetActivity
extends AppCompatActivity implements JitsiMeetActivityInterface {
extends AppCompatActivity {
/**
* The request code identifying requests for the permission to draw on top
@@ -175,12 +174,7 @@ public class JitsiMeetActivity
if (Settings.canDrawOverlays(this)) {
initializeContentView();
}
return;
}
ReactActivityLifecycleCallbacks.onActivityResult(
this, requestCode, resultCode, data);
}
@Override
@@ -266,15 +260,6 @@ public class JitsiMeetActivity
ReactActivityLifecycleCallbacks.onNewIntent(intent);
}
// https://developer.android.com/reference/android/support/v4/app/ActivityCompat.OnRequestPermissionsResultCallback
@Override
public void onRequestPermissionsResult(
final int requestCode,
final String[] permissions,
final int[] grantResults) {
ReactActivityLifecycleCallbacks.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
protected void onResume() {
super.onResume();
@@ -298,14 +283,6 @@ public class JitsiMeetActivity
}
}
/**
* Implementation of the {@code PermissionAwareActivity} interface.
*/
@Override
public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
ReactActivityLifecycleCallbacks.requestPermissions(this, permissions, requestCode, listener);
}
/**
*
* @see JitsiMeetView#setDefaultURL(URL)

View File

@@ -1,15 +0,0 @@
package org.jitsi.meet.sdk;
import android.support.v4.app.ActivityCompat;
import com.facebook.react.modules.core.PermissionAwareActivity;
/**
* This interface serves as the umbrella interface that applications not using
* {@code JitsiMeetActivity} must implement in order to ensure full
* functionality.
*/
public interface JitsiMeetActivityInterface
extends ActivityCompat.OnRequestPermissionsResultCallback,
PermissionAwareActivity {
}

View File

@@ -16,16 +16,11 @@
package org.jitsi.meet.sdk;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import com.calendarevents.CalendarEventsPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Callback;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.PermissionListener;
/**
* Helper class to encapsulate the work which needs to be done on
@@ -33,37 +28,6 @@ import com.facebook.react.modules.core.PermissionListener;
* it.
*/
public class ReactActivityLifecycleCallbacks {
/**
* {@link Activity} lifecycle method which should be called from
* {@code Activity#onActivityResult} so we are notified about results of external intents
* started/finished.
*
* @param activity {@code Activity} activity from where the result comes from.
* @param requestCode {@code int} code of the request.
* @param resultCode {@code int} code of the result.
* @param data {@code Intent} the intent of the activity.
*/
public static void onActivityResult(
Activity activity,
int requestCode,
int resultCode,
Intent data) {
ReactInstanceManager reactInstanceManager
= ReactInstanceManagerHolder.getReactInstanceManager();
if (reactInstanceManager != null) {
reactInstanceManager.onActivityResult(activity, requestCode, resultCode, data);
}
}
/**
* Needed for making sure this class working with the "PermissionsAndroid"
* React Native module.
*/
private static PermissionListener permissionListener;
private static Callback permissionsCallback;
/**
* {@link Activity} lifecycle method which should be called from
* {@link Activity#onBackPressed} so we can do the required internal
@@ -143,11 +107,6 @@ public class ReactActivityLifecycleCallbacks {
if (reactInstanceManager != null) {
reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
}
if (permissionsCallback != null) {
permissionsCallback.invoke();
permissionsCallback = null;
}
}
/**
@@ -167,29 +126,4 @@ public class ReactActivityLifecycleCallbacks {
reactInstanceManager.onNewIntent(intent);
}
}
public static void onRequestPermissionsResult(
final int requestCode,
final String[] permissions,
final int[] grantResults) {
CalendarEventsPackage.onRequestPermissionsResult(
requestCode,
permissions,
grantResults);
permissionsCallback = new Callback() {
@Override
public void invoke(Object... args) {
if (permissionListener != null
&& permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
permissionListener = null;
}
}
};
}
@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

@@ -119,7 +119,6 @@ class ReactInstanceManagerHolder {
.setApplication(application)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.addPackage(new co.apptailor.googlesignin.RNGoogleSigninPackage())
.addPackage(new com.BV.LinearGradient.LinearGradientPackage())
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())

View File

@@ -5,8 +5,6 @@ include ':react-native-background-timer'
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
include ':react-native-google-signin'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android')
include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'

View File

@@ -11,7 +11,6 @@ import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents';
import UIEvents from './service/UI/UIEvents';
import UIUtil from './modules/UI/util/UIUtil';
import { createTaskQueue } from './modules/util/helpers';
import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import {
@@ -31,16 +30,14 @@ import EventEmitter from 'events';
import {
AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
authStatusChanged,
commonUserJoinedHandling,
commonUserLeftHandling,
conferenceFailed,
conferenceJoined,
conferenceLeft,
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
EMAIL_COMMAND,
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
@@ -77,10 +74,14 @@ import {
getAvatarURLByParticipantId,
getLocalParticipant,
getParticipantById,
hiddenParticipantJoined,
hiddenParticipantLeft,
localParticipantConnectionStatusChanged,
localParticipantRoleChanged,
MAX_DISPLAY_NAME_LENGTH,
participantConnectionStatusChanged,
participantJoined,
participantLeft,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
@@ -273,27 +274,6 @@ function redirectToStaticPage(pathname) {
windowLocation.pathname = newPathname;
}
/**
* A queue for the async replaceLocalTrack action so that multiple audio
* replacements cannot happen simultaneously. This solves the issue where
* replaceLocalTrack is called multiple times with an oldTrack of null, causing
* multiple local tracks of the same type to be used.
*
* @private
* @type {Object}
*/
const _replaceLocalAudioTrackQueue = createTaskQueue();
/**
* A task queue for replacement local video tracks. This separate queue exists
* so video replacement is not blocked by audio replacement tasks in the queue
* {@link _replaceLocalAudioTrackQueue}.
*
* @private
* @type {Object}
*/
const _replaceLocalVideoTrackQueue = createTaskQueue();
/**
*
*/
@@ -724,7 +704,7 @@ export default {
track.mute();
}
});
logger.log(`initialized with ${tracks.length} local tracks`);
logger.log('initialized with %s local tracks', tracks.length);
this._localTracksInitialized = true;
con.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
@@ -876,6 +856,9 @@ export default {
return;
}
// FIXME it is possible to queue this task twice, but it's not causing
// any issues. Specifically this can happen when the previous
// get user media call is blocked on "ask user for permissions" dialog.
if (!this.localVideo && !mute) {
const maybeShowErrorDialog = error => {
showUI && APP.UI.showCameraErrorNotification(error);
@@ -1278,23 +1261,16 @@ export default {
* @returns {Promise}
*/
useVideoStream(newStream) {
return new Promise((resolve, reject) => {
_replaceLocalVideoTrackQueue.enqueue(onFinish => {
APP.store.dispatch(
replaceLocalTrack(this.localVideo, newStream, room))
.then(() => {
this.localVideo = newStream;
this._setSharingScreen(newStream);
if (newStream) {
APP.UI.addLocalStream(newStream);
}
this.setVideoMuteStatus(this.isLocalVideoMuted());
})
.then(resolve)
.catch(reject)
.then(onFinish);
return APP.store.dispatch(
replaceLocalTrack(this.localVideo, newStream, room))
.then(() => {
this.localVideo = newStream;
this._setSharingScreen(newStream);
if (newStream) {
APP.UI.addLocalStream(newStream);
}
this.setVideoMuteStatus(this.isLocalVideoMuted());
});
});
},
/**
@@ -1324,22 +1300,15 @@ export default {
* @returns {Promise}
*/
useAudioStream(newStream) {
return new Promise((resolve, reject) => {
_replaceLocalAudioTrackQueue.enqueue(onFinish => {
APP.store.dispatch(
replaceLocalTrack(this.localAudio, newStream, room))
.then(() => {
this.localAudio = newStream;
if (newStream) {
APP.UI.addLocalStream(newStream);
}
this.setAudioMuteStatus(this.isLocalAudioMuted());
})
.then(resolve)
.catch(reject)
.then(onFinish);
return APP.store.dispatch(
replaceLocalTrack(this.localAudio, newStream, room))
.then(() => {
this.localAudio = newStream;
if (newStream) {
APP.UI.addLocalStream(newStream);
}
this.setAudioMuteStatus(this.isLocalAudioMuted());
});
});
},
/**
@@ -1692,16 +1661,24 @@ export default {
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user));
room.on(JitsiConferenceEvents.USER_JOINED, (id, user) => {
// The logic shared between RN and web.
commonUserJoinedHandling(APP.store, room, user);
const displayName = user.getDisplayName();
if (user.isHidden()) {
APP.store.dispatch(hiddenParticipantJoined(id, displayName));
return;
}
const displayName = user.getDisplayName();
APP.store.dispatch(participantJoined({
botType: user.getBotType(),
conference: room,
id,
name: displayName,
presence: user.getStatus(),
role: user.getRole()
}));
logger.log(`USER ${id} connnected:`, user);
logger.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id, {
displayName,
formattedDisplayName: appendSuffix(
@@ -1714,14 +1691,14 @@ export default {
});
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
// The logic shared between RN and web.
commonUserLeftHandling(APP.store, room, user);
if (user.isHidden()) {
APP.store.dispatch(hiddenParticipantLeft(id));
return;
}
logger.log(`USER ${id} LEFT:`, user);
APP.store.dispatch(participantLeft(id, room));
logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
APP.UI.messageHandler.participantNotification(
user.getDisplayName(),
@@ -2398,24 +2375,11 @@ export default {
createLocalTracksF,
newDevices.videoinput,
newDevices.audioinput)
.then(tracks => {
// If audio or video muted before, or we unplugged current
// device and selected new one, then mute new track.
const muteSyncPromises = tracks.map(track => {
if ((track.isVideoTrack() && videoWasMuted)
|| (track.isAudioTrack() && audioWasMuted)) {
return track.mute();
}
return Promise.resolve();
});
return Promise.all(muteSyncPromises)
.then(() => Promise.all(
this._setLocalAudioVideoStreams(tracks)));
})
.then(tracks =>
Promise.all(this._setLocalAudioVideoStreams(tracks)))
.then(() => {
// Log and sync known mute state.
// If audio was muted before, or we unplugged current device
// and selected new one, then mute new audio track.
if (audioWasMuted) {
sendAnalytics(createTrackMutedEvent(
'audio',
@@ -2424,6 +2388,8 @@ export default {
muteLocalAudio(true);
}
// If video was muted before, or we unplugged current device
// and selected new one, then mute new video track.
if (!this.isSharingScreen && videoWasMuted) {
sendAnalytics(createTrackMutedEvent(
'video',

View File

@@ -256,10 +256,6 @@ var config = {
// maintenance at 01:00 AM GMT,
// noticeMessage: '',
// Enables calendar integration, depends on googleApiApplicationClientID
// and microsoftApiApplicationClientID
// enableCalendarIntegration: false,
// Stats
//
@@ -351,36 +347,6 @@ var config = {
// userRegion: "asia"
}
// Local Recording
//
// localRecording: {
// Enables local recording.
// Additionally, 'localrecording' (all lowercase) needs to be added to
// TOOLBAR_BUTTONS in interface_config.js for the Local Recording
// button to show up on the toolbar.
//
// enabled: true,
//
// The recording format, can be one of 'ogg', 'flac' or 'wav'.
// format: 'flac'
//
// }
// Options related to end-to-end (participant to participant) ping.
// e2eping: {
// // The interval in milliseconds at which pings will be sent.
// // Defaults to 10000, set to <= 0 to disable.
// pingInterval: 10000,
//
// // The interval in milliseconds at which analytics events
// // with the measured RTT will be sent. Defaults to 60000, set
// // to <= 0 to disable.
// analyticsInterval: 60000,
// }
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
@@ -400,10 +366,8 @@ var config = {
externalConnectUrl
firefox_fake_device
googleApiApplicationClientID
googleApiIOSClientID
iAmRecorder
iAmSipGateway
microsoftApiApplicationClientID
peopleSearchQueryTypes
peopleSearchUrl
requireDisplayName
@@ -432,7 +396,6 @@ var config = {
nick
startBitrate
*/
};
/* eslint-enable no-unused-vars, no-var */

View File

@@ -10,31 +10,3 @@
-ms-transform: translateX(0) translateY(100%) translateY(16px) !important;
-webkit-transform: translateX(0) translateY(100%) translateY(16px) !important;
}
/**
* Welcome page tab color adjustments.
*/
.welcome {
/**
* The text color of the selected tab and hovered tabs.
*/
li.bcVmZW,
li.bcVmZW:hover,
li.kheoEp:hover {
color: #172B4D;
}
/**
* The color of the inactive tab text.
*/
li.kheoEp {
color: #FFFFFF;
}
/**
* The color of the underline of a selected tab.
*/
li>span.kByArU {
background-color: #172B4D;
}
}

View File

@@ -13,55 +13,23 @@
float: left;
}
.navigate-section-list-tile {
background-color: #1754A9;
height: 90px;
width: 260px;
border-radius: 4px;
box-sizing: border-box;
display: inline-flex;
margin-bottom: 8px;
background-color: #1754A9;
margin-right: 8px;
min-height: 100px;
padding: 16px;
width: 100%;
&.with-click-handler {
cursor: pointer;
}
&.with-click-handler:hover {
background-color: #1a5dbb;
}
i {
cursor: inherit;
}
.element-after {
display: flex;
align-items: center;
justify-content: center;
}
.join-button {
display: none;
}
&:hover .join-button {
display: block
}
display: inline-block;
box-sizing: border-box;
cursor: pointer;
}
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
line-height: 24px;
}
.navigate-section-list-tile-info {
flex: 1;
word-break: break-word;
}
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
line-height: 24px;
}
.navigate-section-section-header {
@extend %navigate-section-list-text;
@@ -72,8 +40,4 @@
position: relative;
margin-top: 36px;
margin-bottom: 36px;
width: 100%;
}
.navigate-section-list-empty {
text-align: center;
}

View File

@@ -2,42 +2,6 @@
vertical-align: top;
}
.recording-dialog {
.authorization-panel {
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
margin-bottom: 10px;
padding-bottom: 10px;
.dropbox-sign-in {
align-items: center;
border: 1px solid #4285f4;
background-color: white;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
padding: 10px;
font-size: 18px;
font-weight: 600;
margin: 10px 0px;
color: #4285f4;
.dropbox-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding-right: 5px;
height: 18px;
}
}
.logged-in-pannel {
padding: 10px;
}
}
}
.live-stream-dialog {
/**
* Set font-size to be consistent with Atlaskit FieldText.
@@ -70,6 +34,39 @@
color: $errorColor;
}
/**
* The Google sign in button must follow Google's design guidelines.
* See: https://developers.google.com/identity/branding-guidelines
*/
.google-sign-in {
background-color: #4285f4;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
font-family: Roboto, arial, sans-serif;
font-size: 14px;
padding: 1px;
.google-cta {
color: white;
display: inline-block;
/**
* Hack the line height for vertical centering of text.
*/
line-height: 32px;
margin: 0 15px;
}
.google-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding: 8px;
height: 18px;
width: 18px;
}
}
.google-panel {
align-items: center;
border-bottom: 2px solid rgba(0, 0, 0, 0.3);

View File

@@ -2,19 +2,12 @@
bottom: 10%;
font-size: 16px;
font-weight: 1000;
left: 50%;
max-width: 50vw;
opacity: 0.80;
pointer-events: none;
position: absolute;
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
0px 1px 1px rgba(0,0,0,0.3),
1px 0px 1px rgba(0,0,0,0.3),
0px 0px 1px rgba(0,0,0,0.3);
transform: translateX(-50%);
z-index: $filmstripVideosZ + 1;
span {
background: black;
}
width: 100%;
z-index: $zindex2;
}

View File

@@ -45,7 +45,6 @@ body.welcome-page {
font-size: 1rem;
font-weight: 400;
line-height: 24px;
margin-bottom: 20px;
}
#enter_room {
@@ -63,30 +62,12 @@ body.welcome-page {
width: 100%;
}
}
.tab-container {
font-size: 16px;
position: relative;
text-align: left;
width: 650px;
}
}
.welcome-page-button {
font-size: 16px;
}
.welcome-page-settings {
color: $welcomePageDescriptionColor;
position: absolute;
right: 10px;
z-index: $zindex2;
* {
cursor: pointer;
}
}
.welcome-watermark {
position: absolute;
width: 100%;

View File

@@ -14,9 +14,14 @@
* Focused video thumbnail.
*/
&.videoContainerFocused {
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected;
0 0 3px $videoThumbnailSelected !important;
}
.remotevideomenu > .icon-menu {
@@ -26,7 +31,7 @@
/**
* Hovered video thumbnail.
*/
&:hover:not(.videoContainerFocused):not(.active-speaker) {
&:hover {
cursor: hand;
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,

View File

@@ -1,113 +0,0 @@
/**
* CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
*/
.tile-view {
/**
* Add a border around the active speaker to make the thumbnail easier to
* see.
*/
.active-speaker {
box-shadow: 0 0 5px 3px $videoThumbnailSelected
}
#filmstripRemoteVideos {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
.filmstrip__videos .videocontainer {
&:not(.active-speaker),
&:hover:not(.active-speaker) {
border: none;
box-shadow: none;
}
}
#remoteVideos {
/**
* Height is modified with an inline style in horizontal filmstrip mode
* so !important is used to override that.
*/
height: 100% !important;
width: 100%;
}
.filmstrip {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: $filmstripVideosZ
}
/**
* Regardless of the user setting, do not let the filmstrip be in a hidden
* state.
*/
.filmstrip__videos.hidden {
display: block;
}
#filmstripRemoteVideos {
box-sizing: border-box;
/**
* Allow vertical scrolling of the thumbnails.
*/
overflow-x: hidden;
overflow-y: auto;
}
/**
* The size of the thumbnails should be set with javascript, based on
* desired column count and window width. The rows are created using flex
* and allowing the thumbnails to wrap.
*/
#filmstripRemoteVideosContainer {
align-content: center;
align-items: center;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
height: 100vh;
justify-content: center;
padding: 100px 0;
.videocontainer {
border: 0;
box-sizing: border-box;
display: block;
margin: 5px;
}
video {
object-fit: contain;
}
}
.has-overflow#filmstripRemoteVideosContainer {
align-content: baseline;
}
.has-overflow .videocontainer {
align-self: baseline;
}
/**
* Firefox flex acts a little differently. To make sure the bottom row of
* thumbnails is not overlapped by the horizontal toolbar, margin is added
* to the local thumbnail to keep it from the bottom of the screen. It is
* assumed the local thumbnail will always be on the bottom row.
*/
.has-overflow #localVideoContainer {
margin-bottom: 100px !important;
}
}

View File

@@ -1,47 +0,0 @@
/**
* Various overrides outside of the filmstrip to style the app to support a
* tiled thumbnail experience.
*/
.tile-view {
/**
* Let the avatar grow with the tile.
*/
.userAvatar {
max-height: initial;
max-width: initial;
}
/**
* Hide various features that should not be displayed while in tile view.
*/
#dominantSpeaker,
#filmstripLocalVideoThumbnail,
#largeVideoElementsContainer,
#sharedVideo,
.filmstrip__toolbar {
display: none;
}
#localConnectionMessage,
#remoteConnectionMessage,
.watermark {
z-index: $filmstripVideosZ + 1;
}
/**
* The follow styling uses !important to override inline styles set with
* javascript.
*
* TODO: These overrides should be more easy to remove and should be removed
* when the components are in react so their rendering done declaratively,
* making conditional styling easier to apply.
*/
#largeVideoElementsContainer,
#remoteConnectionMessage,
#remotePresenceMessage {
display: none !important;
}
#largeVideoContainer {
background-color: $defaultBackground !important;
}
}

View File

@@ -45,7 +45,6 @@
@import 'modals/settings/settings';
@import 'modals/speaker_stats/speaker_stats';
@import 'modals/video-quality/video-quality';
@import 'modals/local-recording/local-recording';
@import 'videolayout_default';
@import 'notice';
@import 'popup_menu';
@@ -73,8 +72,6 @@
@import 'filmstrip/filmstrip_toolbar';
@import 'filmstrip/horizontal_filmstrip';
@import 'filmstrip/small_video';
@import 'filmstrip/tile_view';
@import 'filmstrip/tile_view_overrides';
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'unsupported-browser/main';
@@ -82,7 +79,4 @@
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import 'navigate_section_list';
@import 'third-party-branding/google';
@import 'third-party-branding/microsoft';
/* Modules END */

View File

@@ -1,92 +0,0 @@
.localrec-participant-stats {
list-style: none;
padding: 0;
width: 100%;
font-weight: 500;
.localrec-participant-stats-item__status-dot {
position: relative;
display: block;
width: 9px;
height: 9px;
border-radius: 50%;
margin: 0 auto;
&.status-on {
background: green;
}
&.status-off {
background: gray;
}
&.status-unknown {
background: darkgoldenrod;
}
&.status-error {
background: darkred;
}
}
.localrec-participant-stats-item__status,
.localrec-participant-stats-item__name,
.localrec-participant-stats-item__sessionid {
display: inline-block;
margin: 5px 0;
vertical-align: middle;
}
.localrec-participant-stats-item__status {
width: 5%;
}
.localrec-participant-stats-item__name {
width: 40%;
}
.localrec-participant-stats-item__sessionid {
width: 55%;
}
.localrec-participant-stats-item__name,
.localrec-participant-stats-item__sessionid {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.localrec-control-info-label {
font-weight: bold;
}
.localrec-control-info-label:after {
content: ' ';
}
.localrec-control-action-link {
display: inline-block;
line-height: 1.5em;
a {
cursor: pointer;
vertical-align: middle;
}
}
.localrec-control-action-link:before {
color: $linkFontColor;
content: '\2022';
font-size: 1.5em;
padding: 0 10px;
vertical-align: middle;
}
.localrec-control-action-link:first-child:before {
content: '';
padding: 0;
}
.localrec-control-action-links {
font-weight: bold;
margin-top: 10px;
white-space: nowrap;
}

View File

@@ -10,7 +10,6 @@
margin-bottom: 4px;
}
.calendar-tab,
.device-selection {
margin-top: 20px;
}
@@ -23,7 +22,6 @@
padding: 20px 0px 4px 0px;
}
.calendar-tab,
.more-tab,
.profile-edit {
display: flex;
@@ -42,20 +40,4 @@
.language-settings {
max-width: 50%;
}
.calendar-tab {
align-items: center;
flex-direction: column;
font-size: 14px;
min-height: 100px;
text-align: center;
}
.calendar-tab-sign-in {
margin-top: 20px;
}
.sign-out-cta {
margin-bottom: 20px;
}
}

View File

@@ -168,10 +168,6 @@
background: #FF5630;
}
.circular-label.local-rec {
background: #FF5630;
}
.circular-label.stream {
background: #0065FF;
}

View File

@@ -1,32 +0,0 @@
/**
* The Google sign in button must follow Google's design guidelines.
* See: https://developers.google.com/identity/branding-guidelines
*/
.google-sign-in {
background-color: #4285f4;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
font-family: Roboto, arial, sans-serif;
font-size: 14px;
padding: 1px;
.google-cta {
color: white;
display: inline-block;
/**
* Hack the line height for vertical centering of text.
*/
line-height: 32px;
margin: 0 15px;
}
.google-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding: 8px;
height: 18px;
width: 18px;
}
}

View File

@@ -1,28 +0,0 @@
/**
* The Microsoft sign in button must follow Microsoft's brand guidelines.
* See: https://docs.microsoft.com/en-us/azure/active-directory/
* develop/active-directory-branding-guidelines
*/
.microsoft-sign-in {
align-items: center;
background: #FFFFFF;
border: 1px solid #8C8C8C;
box-sizing: border-box;
cursor: pointer;
display: inline-flex;
font-family: Segoe UI, Roboto, arial, sans-serif;
height: 41px;
padding: 12px;
.microsoft-cta {
display: inline-block;
color: #5E5E5E;
font-size: 15px;
line-height: 41px;
}
.microsoft-logo {
display: inline-block;
margin-right: 12px;
}
}

View File

@@ -135,8 +135,7 @@ server {
location / {
ssi on;
}
# BOSH, Bidirectional-streams Over Synchronous HTTP
# https://en.wikipedia.org/wiki/BOSH_(protocol)
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;

View File

@@ -1,22 +0,0 @@
# Setting up Google Authentication
- Create a Firebase project here: https://firebase.google.com/. You'll need a
signed Android build for that, that can be a debug auto-signed build too, just
retrieve the signing hash.
- Place the generated ```google-services.json``` file in ```android/app```
for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for
iOS (you can stop at that step, no need for the driver and the code changes they
suggest in the wizard).
- You may want to exclude these files in YOUR GIT config (do not exclude them in
the ```.gitignore``` of the application itself!).
- Your WEB and iOS client IDs are auto generated during the Firebase project
creation. Find them in the Google Developer console:
https://console.developers.google.com/
- Make sure your config reflects these IDs so then the Redux state of the
feature ```features/base/config``` contains variables
```googleApiApplicationClientID``` and ```googleApiIOSClientID``` with the
respective values.
- Add your iOS client ID as an application URL schema into
```ios/app/src/Info.plist``` (replacing placeholder).
- Enable YouTube API access on the developer console (see above) for live
streaming.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="324px" height="63.8px" viewBox="0 0 324 63.8" style="enable-background:new 0 0 324 63.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0061FF;}
.st1{display:none;}
.st2{display:inline;}
.st3{fill:none;}
</style>
<path class="st0" d="M37.6,12L18.8,24l18.8,12L18.8,48L0,35.9l18.8-12L0,12L18.8,0L37.6,12z M18.7,51.8l18.8-12l18.8,12l-18.8,12
L18.7,51.8z M37.6,35.9l18.8-12L37.6,12L56.3,0l18.8,12L56.3,24l18.8,12L56.3,48L37.6,35.9z"/>
<path d="M89.8,12H105c9.7,0,17.7,5.6,17.7,18.4v2.7c0,12.9-7.5,18.7-17.4,18.7H89.8V12z M98.3,19.2v25.3h6.5c5.5,0,9.2-3.6,9.2-11.6
v-2.1c0-8-3.9-11.6-9.5-11.6H98.3z M127.2,19.6h6.8l1.1,7.5c1.3-5.1,4.6-7.8,10.6-7.8h2.1v8.6h-3.5c-6.9,0-8.6,2.4-8.6,9.2v14.8
h-8.4V19.6H127.2z M149.5,36.4v-0.9c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.5,16.3-16.3,16.3
C155.4,52.6,149.5,47,149.5,36.4z M173.5,36.3v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1
C170.5,45.3,173.5,42.1,173.5,36.3z M186.5,19.6h7l0.8,6.1c1.7-4.1,5.3-6.9,10.6-6.9c8.2,0,13.6,5.9,13.6,16.8v0.9
c0,10.6-6,16.2-13.6,16.2c-5.1,0-8.6-2.3-10.3-6V63h-8.2L186.5,19.6L186.5,19.6z M210,36.3v-0.7c0-6.4-3.3-9.6-7.7-9.6
c-4.7,0-7.8,3.6-7.8,9.6v0.6c0,5.7,3,9.3,7.7,9.3C207,45.4,210,42.3,210,36.3z M230.9,45.9l-0.7,5.9H223v-43h8.2v16.5
c1.8-4.2,5.4-6.5,10.5-6.5c7.7,0.1,13.4,5.4,13.4,16.1v1c0,10.7-5.4,16.8-13.6,16.8C236.1,52.6,232.6,50.1,230.9,45.9z M246.5,35.9
v-0.8c0-5.9-3.2-9.2-7.7-9.2c-4.6,0-7.8,3.7-7.8,9.3v0.7c0,6,3.1,9.5,7.7,9.5C243.6,45.4,246.5,42.3,246.5,35.9z M258.7,36.4v-0.9
c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.6,16.3-16.3,16.3C264.6,52.6,258.7,47,258.7,36.4z M282.8,36.3
v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1C279.8,45.3,282.8,42.1,282.8,36.3z M302.3,35.1L291,19.6
h9.7l6.5,9.7l6.6-9.7h9.6L311.9,35L324,51.8h-9.5l-7.4-10.7l-7.2,10.7H290L302.3,35.1z"/>
<g id="Editble" class="st1">
<g class="st2">
<rect x="-105" y="5" class="st3" width="506" height="71.8"/>
<path d="M0.2,13.6h16.3c10.4,0,19,6.1,19,19.8v2.9c0,13.8-8,20-18.7,20H0.2V13.6z M9.4,21.3v27.2h7c5.9,0,9.9-3.9,9.9-12.5v-2.2
c0-8.6-4.1-12.5-10.2-12.5H9.4z M40.4,21.8h7.3l1.1,8c1.4-5.5,4.9-8.3,11.3-8.3h2.2v9.2h-3.7c-7.4,0-9.2,2.6-9.2,9.9v15.8h-9
C40.4,56.4,40.4,21.8,40.4,21.8z M64.3,39.8v-1c0-11.6,7.4-17.9,17.5-17.9c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7,17.5-17.5,17.5
C70.6,57.3,64.3,51.2,64.3,39.8z M90.1,39.7v-0.8c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7
C86.9,49.4,90.1,46,90.1,39.7z M104,21.8h7.6l0.9,6.6c1.9-4.4,5.7-7.4,11.4-7.4c8.8,0,14.6,6.4,14.6,18v1
c0,11.4-6.4,17.3-14.6,17.3c-5.5,0-9.2-2.5-11-6.5v17.5H104V21.8z M129.3,39.8V39c0-6.9-3.5-10.3-8.3-10.3c-5,0-8.4,3.8-8.4,10.3
v0.7c0,6.1,3.2,10,8.2,10C126,49.5,129.3,46.1,129.3,39.8z M151.7,50.1l-0.7,6.3h-7.8V10.2h8.8V28c1.9-4.5,5.8-7,11.2-7
c8.2,0.1,14.3,5.8,14.3,17.3v1c0,11.5-5.8,18-14.6,18C157.3,57.3,153.5,54.5,151.7,50.1z M168.5,39.3v-0.8c0-6.4-3.5-9.8-8.3-9.8
c-5,0-8.4,4-8.4,10v0.7c0,6.5,3.3,10.2,8.3,10.2C165.3,49.5,168.5,46.1,168.5,39.3z M181.6,39.8v-1c0-11.6,7.4-17.9,17.5-17.9
c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7.1,17.5-17.5,17.5C187.9,57.3,181.6,51.2,181.6,39.8z M207.4,39.7v-0.8
c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7C204.2,49.4,207.4,46,207.4,39.7z M228.3,38.4
l-12.1-16.7h10.4l7,10.4l7.1-10.4H251l-12.3,16.6l13,18h-10.2l-8-11.5l-7.7,11.5h-10.6L228.3,38.4z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>

Before

Width:  |  Height:  |  Size: 343 B

View File

@@ -48,11 +48,10 @@ var interfaceConfig = {
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview'
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
// Determines how the video would fit the screen. 'both' would fit the whole
// screen, 'height' would fit the original video height to the height of the
@@ -173,12 +172,6 @@ var interfaceConfig = {
*/
RECENT_LIST_ENABLED: true
/**
* How many columns the tile view can expand to. The respected range is
* between 1 and 5.
*/
// TILE_VIEW_MAX_COLUMNS: 5,
/**
* Specify custom URL for downloading android mobile app.
*/

View File

@@ -35,8 +35,8 @@ target 'JitsiMeet' do
pod 'react-native-locale-detector',
:path => '../node_modules/react-native-locale-detector'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNGoogleSignin',
:path => '../node_modules/react-native-google-signin'
pod 'ReactNativePermissions',
:path => '../node_modules/react-native-permissions'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'react-native-calendar-events',

View File

@@ -7,31 +7,11 @@ PODS:
- DoubleConversion
- glog
- glog (0.3.4)
- GoogleSignIn (4.2.0):
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
- "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)"
- GTMOAuth2 (~> 1.0)
- GTMSessionFetcher/Core (~> 1.1)
- GoogleToolboxForMac/DebugUtils (2.1.4):
- GoogleToolboxForMac/Defines (= 2.1.4)
- GoogleToolboxForMac/Defines (2.1.4)
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.1.4)":
- GoogleToolboxForMac/DebugUtils (= 2.1.4)
- GoogleToolboxForMac/Defines (= 2.1.4)
- "GoogleToolboxForMac/NSString+URLArguments (= 2.1.4)"
- "GoogleToolboxForMac/NSString+URLArguments (2.1.4)"
- GTMOAuth2 (1.1.6):
- GTMSessionFetcher (~> 1.1)
- GTMSessionFetcher (1.2.0):
- GTMSessionFetcher/Full (= 1.2.0)
- GTMSessionFetcher/Core (1.2.0)
- GTMSessionFetcher/Full (1.2.0):
- GTMSessionFetcher/Core (= 1.2.0)
- React (0.55.4):
- React/Core (= 0.55.4)
- react-native-background-timer (2.0.0):
- React
- react-native-calendar-events (1.6.2):
- react-native-calendar-events (1.6.0):
- React
- react-native-fast-image (4.0.14):
- FLAnimatedImage
@@ -42,7 +22,7 @@ PODS:
- React
- react-native-locale-detector (1.0.0):
- React
- react-native-webrtc (1.63.0):
- react-native-webrtc (1.58.2):
- React
- React/Core (0.55.4):
- yoga (= 0.55.4.React)
@@ -83,8 +63,7 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- RNGoogleSignin (1.0.0-rc3):
- GoogleSignIn
- ReactNativePermissions (1.1.1):
- React
- RNSound (0.10.9):
- React/Core
@@ -119,7 +98,7 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- RNGoogleSignin (from `../node_modules/react-native-google-signin`)
- ReactNativePermissions (from `../node_modules/react-native-permissions`)
- RNSound (from `../node_modules/react-native-sound`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -128,10 +107,6 @@ SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- FLAnimatedImage
- GoogleSignIn
- GoogleToolboxForMac
- GTMOAuth2
- GTMSessionFetcher
- SDWebImage
EXTERNAL SOURCES:
@@ -155,8 +130,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-locale-detector"
react-native-webrtc:
:path: "../node_modules/react-native-webrtc"
RNGoogleSignin:
:path: "../node_modules/react-native-google-signin"
ReactNativePermissions:
:path: "../node_modules/react-native-permissions"
RNSound:
:path: "../node_modules/react-native-sound"
RNVectorIcons:
@@ -170,10 +145,6 @@ SPEC CHECKSUMS:
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Folly: 211775e49d8da0ca658aebc8eab89d642935755c
glog: 1de0bb937dccdc981596d3b5825ebfb765017ded
GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532
GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f
GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2
GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e
React: aa2040dbb6f317b95314968021bd2888816e03d5
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
@@ -181,12 +152,12 @@ SPEC CHECKSUMS:
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
RNGoogleSignin: 44debd8c359a662c0e2d585952e88b985bf78008
ReactNativePermissions: 9f2d9c45c98800795e6c2ed330e25d11a66a8169
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
PODFILE CHECKSUM: da74c08f6eb674668c49d8d799f8d9e2476a9fc5
PODFILE CHECKSUM: 1d5c8382f73d9540fac68d93b32e1d3b58d069ee
COCOAPODS: 1.5.3

View File

@@ -32,16 +32,6 @@
<string>org.jitsi.meet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.googleusercontent.apps</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.YOUR_ID_HERE</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>

View File

@@ -376,8 +376,6 @@
);
inputPaths = (
"${SRCROOT}/../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh",
"${PODS_ROOT}/GTMOAuth2/Source/Touch/GTMOAuth2ViewTouch.xib",
"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
@@ -392,8 +390,6 @@
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMOAuth2ViewTouch.nib",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",

View File

@@ -1,18 +1,11 @@
{
"en": "Inglês",
"az": "Azerbaijanês",
"bg": "Búlgaro",
"cs": "Checo",
"de": "Alemão",
"el": "Grego",
"eo": "Esperanto",
"es": "Espanhol",
"fr": "Francês",
"hy": "Armênio",
"it": "Italiano",
"ja": "Japonês",
"ko": "Coreano",
"nb": "Bokmal norueguês",
"oc": "Occitano",
"pl": "Polonês",
"ptBR": "Português (Brasil)",
@@ -21,6 +14,7 @@
"sl": "Esloveno",
"sv": "Sueco",
"tr": "Turco",
"vi": "Vietnamita",
"zhCN": "Chinês (China)"
"zhCN": "Chinês (China)",
"nb": "Bokmal norueguês",
"eo": "Esperanto"
}

View File

@@ -15,6 +15,6 @@
"sv": "Шведский",
"tr": "Турецкий",
"zhCN": "Китайский (Китай)",
"nb": "Норвежский букмол",
"eo": "Эсперанто"
"nb": "",
"eo": ""
}

View File

@@ -1,20 +0,0 @@
{
"en": "English",
"bg": "Bulgarian",
"de": "German",
"es": "Spanish",
"fr": "French",
"hy": "Armenian",
"it": "Italian",
"oc": "Occitan",
"pl": "Polish",
"ptBR": "Portuguese (Brazil)",
"ru": "Russian",
"sk": "Slovak",
"sl": "Slovenian",
"sv": "Swedish",
"tr": "Turkish",
"zhCN": "中文 简体 (中国)",
"nb": "Norwegian Bokmal",
"eo": "Esperanto"
}

View File

@@ -35,39 +35,31 @@
"raiseHand": "Erga ou baixe sua mão",
"pushToTalk": "Pressione para falar",
"toggleScreensharing": "Trocar entre câmera e compartilhamento de tela",
"toggleFilmstrip": "Mostrar ou ocultar miniaturas de vídeo",
"toggleShortcuts": "Mostrar ou ocultar atalhos de teclado",
"toggleFilmstrip": "Mostrar ou ocultar a barra lateral",
"toggleShortcuts": "Mostrar ou ocultar este menu de ajuda",
"focusLocal": "Focar no seu vídeo",
"focusRemote": "Focar no vídeo de outro participante",
"toggleChat": "Abrir ou fechar o painel de bate-papo",
"mute": "Deixar mudo ou não o microfone",
"fullScreen": "Entrar ou sair da tela cheia",
"videoMute": "Iniciar ou parar sua câmera",
"showSpeakerStats": "Exibir estatísticas do alto falante",
"localRecording": "Mostrar ou ocultar controles de gravação local"
"showSpeakerStats": "Exibir estatísticas do alto falante"
},
"welcomepage": {
"accessibilityLabel": {
"join": "Toque para entrar",
"roomname": "Digite o nome da sala"
},
"appDescription": "Vá em frente, converse por vídeo com toda a equipe. De fato, convide todos que você conhece. __app__ é uma solução de videoconferência totalmente criptografada e 100% de código aberto que você pode usar todos os dias, a cada dia, gratuitamente — sem necessidade de conta.",
"audioVideoSwitch": {
"audio": "Voz",
"video": "Vídeo"
},
"calendar": "Calendário",
"connectCalendarText": "Conecte seu calendário para ver todas as suas reuniões no __app__. Além disso, adicione reuniões no __app__ ao seu calendário e inicie-as com um clique.",
"connectCalendarButton": "Conectar seu calendário",
"go": "IR",
"join": "Entrar",
"privacy": "Política de Privacidade",
"recentList": "Hitstórico",
"roomname": "Digite o nome da sala",
"roomnameHint": "Digite o nome ou a URL da sala que você deseja entrar. Você pode digitar um nome, e apenas deixe para as pessoas que você quer se reunir digitem o mesmo nome.",
"sendFeedback": "Enviar comentários",
"terms": "Termos",
"title": "Vídeo conferência mais segura, mais flexível e completamente livre."
"title": "Vídeo conferência mais segura, mais flexível e completamente livre "
},
"startupoverlay": {
"policyText": " ",
@@ -79,41 +71,8 @@
"rejoinKeyTitle": "Reconectar"
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "Alternar para apenas áudio",
"audioRoute": "",
"callQuality": "Gerenciar qualidade da chamada",
"chat": "Alternar para janela de chat",
"cc": "Alternar legendas",
"document": "Alternar para documento compartilhado",
"feedback": "Deixar feedback",
"fullScreen": "Alternar para tela cheia",
"hangup": "Sair da chamada",
"invite": "Convidar pessoas",
"localRecording": "Alternar controles de gravação local",
"lockRoom": "Alternar trava da sala",
"moreActions": "Alternar mais menu de ações",
"moreActionsMenu": "Menu de mais ações",
"mute": "Alternar mudo do áudio",
"pip": "Alternar modo Picture-in-Picture",
"profile": "Editar seu perfil",
"raiseHand": "Alternar levantar a mão",
"recording": "Alternar gravação",
"Settings": "Alternar configurações",
"sharedvideo": "Alternar compartilhamento de vídeo do Youtube",
"shareRoom": "Convidar alguém",
"shareYourScreen": "Alternar compartilhamento de tela",
"shortcuts": "Alternar atalhos",
"speakerStats": "Alternar estatísticas do apresentador",
"toggleCamera": "",
"tileView": "",
"videomute": "Alternar mudo do vídeo"
},
"addPeople": "Adicionar pessoas à sua chamada",
"audioonly": "Ativar / desativar modo somente áudio (economiza banda)",
"audioOnlyOn": "Ativar modo somente áudio (economiza banda)",
"audioOnlyOff": "Desativar modo somente áudio",
"audioRoute": "Selecionar o dispositivo de som",
"callQuality": "Gerenciar qualidade da chamada",
"enterFullScreen": "Ver em tela cheia",
"exitFullScreen": "Sair da tela cheia",
@@ -127,8 +86,8 @@
"etherpad": "Abrir ou fechar o documento compartilhado",
"documentOpen": "Abrir documento compartilhado",
"documentClose": "Fechar documento compartilhado",
"shareRoom": "Compartilhar sala",
"sharedvideo": "Compartilhar um vídeo do YouTube",
"sharescreen": "Compartilhamento de tela",
"stopSharedVideo": "Parar vídeo do YouTube",
"fullscreen": "Entrar ou sair da tela cheia",
"sip": "Chamar número SIP",
@@ -137,20 +96,16 @@
"login": "Iniciar sessão",
"logout": "Encerrar sessão",
"sharedVideoMutedPopup": "Seu vídeo compartilhado foi silenciado para que você possa conversar com os outros membros.",
"toggleCamera": "Alternar câmera",
"micMutedPopup": "Seu microfone foi silenciado para que você aproveite plenamente seu vídeo compartilhado.",
"talkWhileMutedPopup": "Tentando falar? Você está em mudo.",
"unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.",
"cameraDisabled": "A câmera não está disponível",
"micDisabled": "O microfone não está disponível",
"filmstrip": "Mostrar / ocultar vídeos",
"pip": "Entrar em modo Quadro-a-Quadro",
"profile": "Editar seu perfil",
"raiseHand": "Erguer / Baixar sua mão",
"raiseHand": "Erguer o baixar sua mão",
"shortcuts": "Ver atalhos",
"speakerStats": "Estatísticas do Apresentador",
"tileViewToggle": "Alternar visualização em blocos",
"invite": "Convidar pessoas"
"speakerStats": "Estatísticas do Apresentador"
},
"chat": {
"nickname": {
@@ -160,13 +115,6 @@
"messagebox": "Digite um texto..."
},
"settings": {
"calendar": {
"about": "A integração do calendário __appName__ é usada para acessar com segurança o seu calendário para que ele possa ler os próximos eventos.",
"disconnect": "Desconectar",
"microsoftSignIn": "Entrar com Microsoft",
"signedIn": "Atualmente acessando eventos do calendário para __email__. Clique no botão Desconectar abaixo para parar de acessar os eventos da agenda.",
"title": "Calendário"
},
"title": "Configurações",
"update": "Atualizar",
"name": "Nome",
@@ -176,15 +124,11 @@
"selectMic": "Microfone",
"selectAudioOutput": "Saída de áudio",
"followMe": "Todos me seguem",
"language": "Idioma",
"loggedIn": "Conectado como __name__",
"noDevice": "Nenhum",
"cameraAndMic": "Câmera e microfone",
"moderator": "Moderador",
"more": "Mais",
"moderator": "MODERADOR",
"password": "DEFINIR SENHA",
"audioVideo": "ÁUDIO E VÍDEO",
"devices": "Dispositivos"
"audioVideo": "ÁUDIO E VÍDEO"
},
"profile": {
"title": "Perfil",
@@ -243,6 +187,7 @@
"focus": "Foco da conferência",
"focusFail": "__component__ não disponĩvel - tente em __ms__ seg.",
"grantedTo": "Direitos de moderador concedido para __to__!",
"grantedToUnknown": "Direitos de moderador concedido para $t(notify.somebody)!",
"muted": "Você iniciou uma conversa em mudo.",
"mutedTitle": "Você está mudo!",
"raisedHand": "Gostaria de falar.",
@@ -250,11 +195,7 @@
"suboptimalExperienceDescription": "Eer ... temos medo de que sua experiência com o __appName__ não seja tão boa aqui. Estamos procurando maneiras de melhorar isso, mas até lá tente usar um dos <a href='static/recommendedBrowsers.html' target='_blank'> navegadores totalmente compatíveis</a>."
},
"dialog": {
"accessibilityLabel": {
"liveStreaming": "Transmissão ao vivo"
},
"allow": "Permitir",
"confirm": "Confirmar",
"kickMessage": "Ouch! Você foi expulso da reunião!",
"popupErrorTitle": "Popup bloqueado",
"popupError": "Seu navegador está bloqueando janelas popup deste site. Habilite os popups nas configurações de segurança no seu navegador e tente novamente.",
@@ -278,7 +219,6 @@
"rejoinNow": "Reconectar agora",
"maxUsersLimitReachedTitle": "Limite máximo de membros alcançado",
"maxUsersLimitReached": "O limite para o máximo do número de membros foi alcançado. A conferência está cheia. Contate o dono da reunião ou tente de novo depois!",
"lockRoom": "Travar sala",
"lockTitle": "Bloqueio falhou",
"lockMessage": "Falha ao travar a conferência.",
"warning": "Atenção",
@@ -326,7 +266,6 @@
"reservationError": "Erro de sistema de reserva",
"reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
"password": "Insira a senha",
"unlockRoom": "Destravar sala",
"userPassword": "senha do usuário",
"token": "token",
"tokenAuthFailedTitle": "Falha de autenticação",
@@ -339,11 +278,11 @@
"sorryFeedback": "Sentimos muito pelos transtornos. Poderia nos das mais detalhes?",
"liveStreaming": "Transmissão ao Vivo",
"streamKey": "Chave para transmissão ao vivo",
"startLiveStreaming": "Iniciar transmissão ao vivo",
"startLiveStreaming": "Ir ao vivo agora",
"startRecording": "Parar gravação",
"stopStreamingWarning": "Tem certeza que deseja parar a transmissão ao vivo?",
"stopRecordingWarning": "Tem certeza que deseja parar a gravação?",
"stopLiveStreaming": "Parar transmissão ao vivo",
"stopLiveStreaming": "Parar a transmissão ao vivo",
"stopRecording": "Parar a gravação",
"doNotShowMessageAgain": "Não mostre esta mensagem novamente",
"permissionDenied": "Permissão Negada",
@@ -373,10 +312,6 @@
"muteParticipantTitle": "Silenciar esse membro?",
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
"muteParticipantButton": "Mudo",
"liveStreamingDisabledTooltip": "Iniciar transmissão ao vivo desativada.",
"liveStreamingDisabledForGuestTooltip": "Visitantes não podem iniciar transmissão ao vivo.",
"recordingDisabledTooltip": "Iniciar gravação desativada.",
"recordingDisabledForGuestTooltip": "Visitantes não podem iniciar gravações.",
"remoteControlTitle": "Conexão de área de trabalho remota",
"remoteControlRequestMessage": "Deseja permitir que __user__ controle remotamente sua área de trabalho?",
"remoteControlShareScreenWarning": "Note que se você pressionar \"Permitir\" você vai compartilhar sua tela!",
@@ -387,11 +322,8 @@
"remoteControlStopMessage": "A sessão de controle remoto terminou!",
"close": "Fechar",
"shareYourScreen": "Compartilhar sua tela",
"shareYourScreenDisabled": "Compartilhamento de tela desativada.",
"shareYourScreenDisabledForGuest": "Visitantes não podem compartilhar tela.",
"yourEntireScreen": "Toda sua tela",
"applicationWindow": "Janela de aplicativo",
"transcribing": "Transcrevendo"
"applicationWindow": "Janela de aplicativo"
},
"email": {
"sharedKey": [
@@ -421,22 +353,6 @@
],
"and": "e"
},
"share": {
"mainText": [
"Clique no seguinte link para entrar na reunião:",
"__roomUrl__"
],
"dialInfoText": [
"",
"",
"=====",
"",
"Quer apenas ligar no seu telefone?",
"",
"Clique neste link para ver a discagem nos números de telefone para esta reunião",
"__dialInfoPageUrl__"
]
},
"connection": {
"ERROR": "Erro",
"CONNECTING": "Conectando",
@@ -450,37 +366,18 @@
"ATTACHED": "Anexado"
},
"recording": {
"beta": "BETA",
"busy": "Estamos trabalhando para liberar recursos de gravação. Tente novamente em alguns minutos.",
"busyTitle": "Todas as gravações estão atualmente ocupadas",
"buttonTooltip": "Iniciar / parar gravação",
"error": "A gravação falhou. Tente novamente.",
"failedToStart": "Falha ao iniciar a gravação",
"live": "AOVIVO",
"off": "Gravação parada",
"on": "Gravando",
"pending": "Preparando para gravar a reunião...",
"rec": "GRAVAR",
"authDropboxText": "Enviar sua gravação para o Dropbox.",
"authDropboxCompletedText": "Seu arquivo de gravação aparecerá no seu Dropbox logo após terminar a gravação.",
"pending": "Aguardando um participante para iniciar a gravação...",
"serviceName": "Serviço de gravação",
"signOut": "Sair",
"signIn": "entrar",
"loggedIn": "Conectado como __userName__",
"availableSpace": "Espaço disponível: __spaceLeft__ MB (aproximadamente __duration__ minutos de gravação)",
"startRecordingBody": "Tem certeza que deseja iniciar a gravação?",
"unavailable": "Oops! O __serviceName__ está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"unavailableTitle": "Gravação indisponível"
},
"transcribing": {
"pending": "Preparando a transcrição da reunião...",
"off": "Transcrição parada",
"error": "Transcrição falhou. Tente novamente.",
"failedToStart": "Transcrição falhou ao iniciar",
"tr": "TR",
"labelToolTip": "A reunião esta sendo transcrita",
"ccButtonTooltip": "Iniciar / Parar de mostrar as legendas"
},
"liveStreaming": {
"busy": "Estamos trabalhando para liberar os recursos de transmissão. Tente novamente em alguns minutos.",
"busyTitle": "Todas as transmissões estão atualmente ocupadas",
@@ -491,7 +388,6 @@
"enterStreamKey": "Insira sua chave de transmissão ao vivo do YouTube aqui.",
"error": "Falha na transmissão ao vivo. Tente de novo.",
"errorAPI": "Ocorreu um erro ao acessar suas transmissões do YouTube. Por favor tente logar novamente.",
"errorLiveStreamNotEnabled": "Transmissão ao vivo não está ativada em __email__. Ative a transmissão ao vivo ou registre numa conta com transmissão ao vivo ativada.",
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
"off": "Transmissão ao vivo encerrada",
"on": "Transmissão ao Vivo",
@@ -528,10 +424,9 @@
"noPermission": "Permissão não concedida",
"previewUnavailable": "Visualização indisponível",
"selectADevice": "Selecione um dispositivo",
"testAudio": "Tocar um som de teste"
"testAudio": "Testar o som"
},
"videoStatus": {
"audioOnly": "AUD",
"callQuality": "Qualidade da Chamada",
"hd": "HD",
"hdTooltip": "Ver vídeo em alta definição",
@@ -560,7 +455,6 @@
"countryNotSupported": "Ainda não suportamos este destino.",
"countryReminder": "Ligando fora dos EUA? Por favor, certifique-se de começar com o código do país!",
"disabled": "Você não pode convidar pessoas.",
"footerText": "Discagem está desativada.",
"invite": "Convidar",
"loading": "Procurando por pessoas e números de telefone",
"loadingNumber": "Validando o número de telefone",
@@ -597,7 +491,6 @@
"veryGood": "Muito boa"
},
"info": {
"accessibilityLabel": "Mostrar info",
"addPassword": "Adicionar uma senha",
"cancelPassword": "Cancelar senha",
"conferenceURL": "Link:",
@@ -619,7 +512,7 @@
"numbers": "Números de discagem",
"password": "Senha:",
"title": "Compartilhar",
"tooltip": "Compartilhar link e discagem para esta reunião"
"tooltip": "Obtenha informações de acesso sobre a reunião"
},
"settingsView": {
"alertOk": "OK",
@@ -635,17 +528,17 @@
"startWithVideoMuted": "Iniciar sem vídeo"
},
"calendarSync": {
"addMeetingURL": "Adicionar um link da reunião",
"today": "Hoje",
"later": "Depois",
"next": "Recebendo",
"nextMeeting": "próxima reunião",
"noEvents": "Não há eventos próximos agendados.",
"ongoingMeeting": "reunião em progresso",
"now": "Agora",
"permissionButton": "Abrir configurações",
"permissionMessage": "Permissão do calendário é requerida para ver suas reuniões na aplicação.",
"refresh": "Atualizar calendário"
"permissionMessage": "Permissão do calendário é requerida para listar suas reuniões na aplicação."
},
"recentList": {
"joinPastMeeting": "Entrar em uma reunião passada"
"today": "Hoje",
"yesterday": "Ontem",
"earlier": "Mais cedo"
},
"sectionList": {
"pullToRefresh": "Puxe para atualizar"
@@ -658,60 +551,5 @@
"appNotInstalled": "Você precisa do aplicativo móvel __app__ para participar da reunião no seu telefone.",
"downloadApp": "Baixe o Aplicativo",
"openApp": "Continue na aplicação"
},
"presenceStatus": {
"invited": "Convidar",
"ringing": "Toque...",
"calling": "Chamando...",
"initializingCall": "Iniciando Chamada...",
"connected": "Conectado",
"connecting": "Conectando...",
"connecting2": "Conectando*...",
"disconnected": "Desconectado",
"busy": "Ocupado",
"rejected": "Rejeitado",
"ignored": "Ignorado",
"expired": "Expirado"
},
"dateUtils": {
"today": "Hoje",
"yesterday": "Ontem",
"earlier": "Mais cedo"
},
"incomingCall": {
"answer": "Responder",
"audioCallTitle": "Chamada chegando",
"decline": "Dispensar",
"productLabel": "do Jitsi Meet",
"videoCallTitle": "Chamada de vídeo chegando"
},
"localRecording": {
"localRecording": "Gravação local",
"dialogTitle": "Controles da Gravação Local",
"start": "Iniciar gravação",
"stop": "Parar a Gravação",
"moderator": "Moderador",
"me": "Eu",
"duration": "Duração",
"durationNA": "N/A",
"encoding": "Codificando",
"participantStats": "Estatísticas dos Participantes",
"participant": "Participante",
"sessionToken": "Token de Sessão",
"clientState": {
"on": "On",
"off": "Off",
"unknown": "Desconhecido"
},
"messages": {
"engaged": "Gravação local iniciada.",
"finished": "Sessão de gravação __token__ terminada. Por favor, envie o arquivo gravado para o moderador.",
"finishedModerator": "Sessão de gravação __token__ terminada. A gravação da faixa local foi salva. Por favor, peça aos outros participantes para enviar suas gravações.",
"notModerator": "Você não é o moderador. Você não pode iniciar ou parar a gravação local."
},
"yes": "Sim",
"no": "Não",
"label": "LOR",
"labelToolTip": "Gravação local está envolvida"
}
}

View File

@@ -1,148 +1,183 @@
{
"contactlist_plural": "Участников: __count__",
"contactlist": "Участники (__pcount__)",
"addParticipants": "Поделиться ссылкой",
"roomLocked": "Вызывающие должны ввести пароль",
"roomUnlocked": "Любой, владеющий ссылкой, может присоединиться",
"passwordSetRemotely": "установлен другим участником",
"connectionsettings": "Настройки подключения",
"poweredby": "работает на",
"inviteUrlDefaultMsg": "Подготавливаем вашу встречу...",
"me": "я",
"speaker": "Колонка",
"raisedHand": "Хочет говорить",
"defaultNickname": "напр. Яна Цветкова",
"defaultLink": "напр. __url__",
"audioDevices": {
"bluetooth": "Bluetooth",
"headphones": "Наушники",
"phone": "Телефон",
"speaker": "Колонка"
"feedback": {
"average": "",
"bad": "",
"good": "",
"rateExperience": "Пожалуйста, оцените ваш опыт встречи.",
"veryBad": "",
"veryGood": ""
},
"inviteUrlDefaultMsg": "Ваша конференция создается в данный момент...",
"me": "Я",
"speaker": "Говорящий",
"raisedHand": "Хочет говорить",
"defaultNickname": "напр. Яна Цветочкина",
"defaultLink": "напр. __url__",
"callingName": "__name__",
"audioOnly": {
"audioOnly": "Только звук",
"featureToggleDisabled": "Переключение функции __feature__ недоступно в режиме \"только звук\""
"audioOnly": "",
"featureToggleDisabled": ""
},
"userMedia": {
"react-nativeGrantPermissions": "Выберите <b><i>Разрешить</i></b>, когда браузер спросит о разрешениях.",
"chromeGrantPermissions": "Выберите <b><i>Разрешить</i></b>, когда браузер спросит о разрешениях.",
"androidGrantPermissions": "Выберите <b><i>Разрешить</i></b>, когда браузер спросит о разрешениях.",
"electronGrantPermissions": "Пожалуйста, дайте разрешение на доступ к камере и микрофону",
"firefoxGrantPermissions": "Выберите <b><i>Поделиться выбранным устройством</i></b>, когда браузер спросит о разрешениях.",
"operaGrantPermissions": "Выберите <b><i>Разрешить</i></b>, когда браузер спросит о разрешениях.",
"iexplorerGrantPermissions": "Выберите <b><i>OK</i></b>, когда браузер спросит о разрешениях.",
"safariGrantPermissions": "Выберите <b><i>OK</i></b>, когда браузер спросит о разрешениях.",
"nwjsGrantPermissions": "Пожалуйста, дайте разрешение на доступ к камере и микрофону",
"edgeGrantPermissions": "Выберите <b><i>Да</i></b>, когда браузер спросит о разрешениях."
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "",
"operaGrantPermissions": "",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "",
"nwjsGrantPermissions": "Пожалуйста дайте разрешение на доступ к камере и микрофону",
"edgeGrantPermissions": ""
},
"keyboardShortcuts": {
"keyboardShortcuts": "Комбинации клавиш",
"raiseHand": "Поднять или опустить руку",
"pushToTalk": "Нажмите, чтобы говорить",
"toggleScreensharing": "Переключиться между камерой и показом экрана",
"toggleScreensharing": "Переключиться между камерой и совместным использованием экрана",
"toggleFilmstrip": "Показать или скрыть видео",
"toggleShortcuts": "Показать или скрыть это справочное меню",
"focusLocal": "Фокус на ваше видео",
"focusRemote": "Фокус на видео другого участника",
"toggleChat": "Чат (открыть/закрыть)",
"mute": "Микрофон (вкл./выкл.)",
"fullScreen": "Полноэкранный режим (вкл./выкл.)",
"videoMute": "Камера (вкл./выкл.)",
"showSpeakerStats": "Показать статистику выступающего"
"focusRemote": "Фокус на видео другого абонента",
"toggleChat": "Открыть или закрыть чат",
"mute": "Заглушить или включить микрофон",
"fullScreen": "Войти или выйти из полноэкранного режима",
"videoMute": "Включить или выключить вашу камеру",
"showSpeakerStats": ""
},
"welcomepage": {
"appDescription": "Попробуйте видеочат со всей командой. Приглашайте знакомых! __app__ — полностью зашифрованное решение для видеоконференций с открытым исходным кодом. Пользуйтесь каждый день, бесплатно и без регистрации.",
"audioVideoSwitch": {
"audio": "Календарь",
"video": "Видео"
"disable": "Не показывать эту страницу снова",
"feature1": {
"content": "Нет нужды что-либо скачивать. __app__ работает прямо из вашего браузера. Просто отправьте URL ссылку на вашу конференцию другим, чтобы начать общение.",
"title": "Простой в использовании"
},
"calendar": "Календарь",
"go": "ОК",
"join": "ПРИСОЕДИНИТЬСЯ",
"privacy": "Приватность",
"roomname": "Укажите название комнаты",
"roomnameHint": "Укажите название комнаты или ее адрес. Можете сами создать название и передать его будущим участникам встречи, чтобы они использовали именно его.",
"sendFeedback": "Обратная связь",
"terms": "Условия",
"title": "Видеоконференции. Безопасно. Гибко. Полностью бесплатно"
"feature2": {
"content": "Многопользовательским видеоконференциям достаточно скорости передачи данных в 128 Кбит/с. Демонстрация экрана или аудиоконференции требуют и того меньше.",
"title": "Низкие требования к ширине канала"
},
"feature3": {
"content": "__app__ лицензирован под Apache License. Вы можете свободно скачивать, использовать, изменять это ПО в соответствии с условиями лицензии.",
"title": "Исходный код открыт"
},
"feature4": {
"content": "Нет никаких искусственных ограничений по количеству пользовательниц или участников конференций. Вас отграничивают только мощность сервера и качество соединения.",
"title": "Количество пользовательниц не ограничено"
},
"feature5": {
"content": "С лёгкостью можно пользоваться экраном совместно. __app__ идеально для онлайн презентаций, лекций и сеансов техподдержки.",
"title": "Общий доступ к экрану"
},
"feature6": {
"content": "Нужно больше приватности? __app__ конференц-комнаты могут быть защищены паролем, чтобы исключить незваных гостей или заминки.",
"title": "Защищённые комнаты"
},
"feature7": {
"content": "__app__ включает Etherpad, текстовый редактор для совместной работы над текстом в реальном времени, который замечательно подходит, чтобы вести протоколы или совместно писать статьи.",
"title": "Поделиться заметками"
},
"feature8": {
"content": "Узнайте больше о пользователях с помощью интеграции с Piwik, Google Analytics и другими системами мониторига и сбора статистики.",
"title": "Статистика использования"
},
"go": "Вперед!",
"join": "",
"privacy": "",
"roomname": "Введите название комнаты",
"roomnamePlaceHolder": "",
"sendFeedback": "",
"terms": ""
},
"\u0005welcomepage": {},
"startupoverlay": {
"policyText": " ",
"title": "__app__ требуется доступ к микрофону и камере."
"policyText": "",
"title": "__app__ нуждается в использовании вашего микрофона и камеры."
},
"suspendedoverlay": {
"title": "Видеосвязь прервана. Причина: этот компьютер перешел в режим сна.",
"text": "Для восстановления связи нажмите кнопку <i>Подключиться снова</i>.",
"rejoinKeyTitle": одключиться снова"
"title": "",
"text": "",
"rejoinKeyTitle": рисоединиться повторно"
},
"toolbar": {
"addPeople": "Добавить людей к вашему сеансу связи",
"audioonly": "Режим \"только звук\" (вкл./выкл.), экономит трафик",
"callQuality": "Качество связи",
"enterFullScreen": "Полный экран",
"exitFullScreen": "Полный экран",
"feedback": "Оставить отзыв",
"moreActions": "Больше",
"mute": "Звук (вкл./выкл.)",
"videomute": "Камера",
"addPeople": "",
"audioonly": "",
"mute": "Вкл. / Выкл. звук",
"videomute": "Вкл / Выкл камеру",
"authenticate": "Аутентифицировать",
"lock": "Блокировка комнаты",
"chat": "Чат",
"etherpad": "Общий документ",
"documentOpen": "Открыть общий документ",
"documentClose": "Закрыть общий документ",
"sharedvideo": "Видео YouTube",
"sharescreen": "Показ экрана",
"stopSharedVideo": "Остановить видео на YouTube",
"fullscreen": "Полноэкранный режим (вкл./выкл.)",
"sip": "Набрать номер SIP",
"lock": "Заблокировать / разблокировать комнату",
"invite": "Поделиться ссылкой",
"chat": "Открыть / Закрыть чат",
"etherpad": "Открыть / Закрыть общий документ",
"sharedvideo": "Поделиться YouTube видео",
"sharescreen": "Начать / Завершить совместное использование экрана",
"fullscreen": "Вкл / Выкл полноэкранный режим",
"sip": "Набрать SIP номер",
"Settings": "Настройки",
"hangup": "Выход",
"hangup": "Покинуть",
"login": "Войти",
"logout": "Завершить сеанс",
"sharedVideoMutedPopup": "Звук видео, которым вы делитесь, отключен, чтобы вы могли общаться с другими участниками.",
"micMutedPopup": "Ваш микрофон отключен, чтобы вы могли слышать звук вашего видео.",
"talkWhileMutedPopup": "Пытаетесь говорить? У вас отключен звук.",
"dialpad": "Открыть / Закрыть клавиатуру для набора номера",
"sharedVideoMutedPopup": "У видео, которым Вы поделились, отключён звук, чтобы вы могли говорить с остальными.",
"micMutedPopup": "Ваш микрофон отключён, чтобы вы могли сосредоточиться на видео, которым поделились.",
"talkWhileMutedPopup": "Пытаетесь говорить? Вы приглушены.",
"unableToUnmutePopup": "Вы не можете включить звук, потому что включено видео.",
"cameraDisabled": "Камера недоступна",
"micDisabled": "Микрофон недоступен",
"filmstrip": "Видео (показать/скрыть)",
"profile": "Редактировать профиль",
"raiseHand": "Хочу говорить",
"shortcuts": "Комбинации клавиш",
"speakerStats": "Статистика"
"filmstrip": "Показать / Скрыть видео",
"profile": "Редактировать ваш профиль",
"raiseHand": "Поднять / Опустить вашу руку"
},
"unsupportedBrowser": {
"appInstalled": "",
"appNotInstalled": "",
"downloadApp": "",
"joinConversation": "",
"startConference": ""
},
"bottomtoolbar": {
"chat": "Открыть / Закрыть чат",
"filmstrip": "Показать / Скрыть видео",
"contactlist": "Просмотреть и пригласить участников"
},
"\u0005toolbar": {},
"chat": {
"nickname": {
"title": "Введите имя в поле ниже",
"popover": "Выберите имя"
},
"messagebox": "Пишите..."
"messagebox": "Введите текст.."
},
"settings": {
"title": "Настройки",
"update": "Обновить",
"name": "Имя",
"startAudioMuted": "Все начинают с выключенным звуком",
"startVideoMuted": "Все начинают в скрытом режиме",
"startAudioMuted": "Каждый начинает глушиться",
"startVideoMuted": "Все начинают скрываться",
"selectCamera": "Камера",
"selectMic": "Микрофон",
"selectAudioOutput": "Звуковой выход",
"followMe": "Все следуют за мной",
"noDevice": "Все как я",
"followMe": "Каждый следует за мной",
"noDevice": "Нет",
"cameraAndMic": "Камера и микрофон",
"moderator": "МОДЕРАТОР",
"password": "УСТАНОВИТЬ ПАРОЛЬ",
"audioVideo": "ЗВУК И ВИДЕО"
"audioVideo": "АУДИО И ВИДЕО"
},
"profile": {
"title": "Профиль",
"setDisplayNameLabel": "Отображаемое имя",
"setEmailLabel": "E-mail для gravatar",
"setEmailInput": "Введите e-mail"
"setDisplayNameLabel": "Установить ваше отображаемое имя",
"setEmailLabel": "Установить электронную почту для gravatar",
"setEmailInput": "Введите электронную почту"
},
"videothumbnail": {
"moderator": "Модератор",
"videomute": "Участник отключил камеру",
"mute": "Участник отключил звук",
"kick": "Выкинуть",
"editnickname": "Нажми, чтобы<br/>поменять имя экрана",
"moderator": "Хозяйка конференции.",
"videomute": "Участник<br/>остановил камеру",
"mute": "Без звука",
"kick": "Прогнать",
"muted": "Звук выключен",
"domute": "Выключить звук",
"flip": "Отразить",
@@ -150,221 +185,208 @@
},
"connectionindicator": {
"header": "Данные соединения",
"bitrate": "Битрейт:",
"packetloss": "Потери пакетов:",
"bitrate": "Битрейт",
"packetloss": "Потеря пакетов:",
"resolution": "Разрешение:",
"framerate": "Частота кадров:",
"less": "Меньше",
"more": "Больше",
"framerate": "",
"less": "Свернуть",
"more": "Показать больше",
"address": "Адрес:",
"remoteport": "Удаленные порты:",
"remoteport_plural_2": "Удаленные порты:",
"remoteport_plural_5": "Удаленные порты:",
"localport": "Локальные порты:",
"remoteport": "Удалённый порт:",
"remoteport_plural_2": "",
"remoteport_plural_5": "",
"localport": "Локальный порт:",
"localport_plural_2": "Локальные порты:",
"localport_plural_5": "Локальные порты:",
"localaddress": "Локальные адреса:",
"localport_plural_5": "",
"localaddress": "Локальный адрес:",
"localaddress_plural_2": "Локальные адреса:",
"localaddress_plural_5": "Локальные адреса:",
"remoteaddress": "Удаленные адреса:",
"remoteaddress_plural_2": "Удаленные адреса:",
"remoteaddress_plural_5": "Удаленные адреса:",
"transport": "Методы отправки:",
"transport_plural_2": "Методы отправки:",
"transport_plural_5": "Методы отправки:",
"bandwidth": "Средняя скорость:",
"na": "Во время беседы вы можете следить за данными о соединении",
"turn": " (повернуть)",
"quality": {
"good": "Хорошо",
"inactive": "не активно",
"lost": "потеряно",
"nonoptimal": "не оптимально",
"poor": "плохо"
},
"status": "Связь:"
"localaddress_plural_5": "",
"remoteaddress": "Удалённый адрес:",
"remoteaddress_plural_2": "",
"remoteaddress_plural_5": "",
"transport": "Метод отправки:",
"bandwidth": "Средняя скорость соединения:",
"na": "Вернитесь сюда за информацией о соединении, когда конференция начнётся",
"turn": ""
},
"notify": {
"disconnected": "соединение разорвано",
"moderator": "Получены права модератора!",
"connectedOneMember": "__name__ подключен",
"connectedTwoMembers": "__first__ и __second__ подключены",
"connectedThreePlusMembers": "__name__ и другие (__count__) подключены",
"moderator": "Получены права для модерации!",
"connected": "подключено",
"somebody": "Кто-то",
"me": "Я",
"focus": "Фокус встречи",
"focusFail": "__component__ недоступен, повторите через __ms__ с",
"grantedTo": "__to__ получил права модератора!",
"grantedToUnknown": "К сожалению, что-то пошло не так.",
"muted": "Вы начали разговор без звука.",
"focus": "Фокусировка конференции",
"focusFail": "__component__ недоступен - повторите через __ms__ секунд",
"grantedTo": "Теперь модерирует __to__!",
"grantedToUnknown": "",
"muted": "Вы начали конференцию без звука.",
"mutedTitle": "Вы без звука!",
"raisedHand": "Хочет говорить.",
"suboptimalExperienceTitle": "К сожалению, этот браузер может не подойти для работы с __appName__. Мы работаем над проблемой, а пока попробуйте один из <a href='static/recommendedBrowsers.html 'target='_blank'>полностью поддерживаемых браузеров</a>.",
"suboptimalExperienceDescription": "К сожалению, этот браузер не очень подходит для работы с __appName__. Мы работаем над проблемой, а пока попробуйте один из <a href='static/recommendedBrowsers.html 'target='_blank'>полностью поддерживаемых браузеров</a>."
"raisedHand": "Хочу высказаться."
},
"dialog": {
"allow": "Разрешить",
"kickMessage": "Вас выкинули из комнаты!",
"popupErrorTitle": "Заблокировано всплывающее окно",
"popupError": "Ваш браузер блокирует всплывающие окна этого сайта. Пожалуйста, разрешите всплывающие окна в настройках безопасности браузера и попробуйте снова.",
"add": "Добавить",
"allow": "",
"kickMessage": "Фигасе! Вас прогнали со встречи!",
"popupError": "Ваш браузер блокирует всплывающие окна на этом сайте. Пожалуйста разрешите всплывающие окна в настройках безопасности и попробуйте снова.",
"passwordErrorTitle": "Ошибка пароля",
"passwordError": "Эта встреча защищена паролем. Только организатор встречи может устанавливать пароль.",
"passwordError2": "Эта встреча не защищена паролем. Только организатор встречи может устанавливать пароль.",
"connectError": "Ошибка. Невозможно установить связь для вашей встречи.",
"connectErrorWithMsg": "Ошибка. Невозможно установить связь для вашей встречи: __msg__",
"incorrectPassword": "Ошибка имени пользователя или пароля",
"connecting": "Подключение",
"passwordError": "Этот разговор сейчас защищён паролем. Только хозяйка конференции может устанавливать пароль.",
"passwordError2": "Эта конференция защищена паролем. Только хозяйка конференции может устанавливать пароль.",
"connectError": "Ёпрст! Что-то пошло не так и мы не можем связаться с конференцией.",
"connectErrorWithMsg": "Ёпрст! Что-то пошло не так и мы не можем связаться с конференцией: __msg__",
"incorrectPassword": "Неверный пароль",
"connecting": "Идёт подключение",
"copy": "Копировать",
"contactSupport": "Связь с поддержкой",
"error": "Ошибка",
"detectext": "Ошибка связи с расширением для показа экрана.",
"failedpermissions": "Ошибка доступа к локальному микрофону и/или камере.",
"conferenceReloadTitle": "К сожалению, что-то пошло не так.",
"conferenceReloadMsg": "Мы стараемся это исправить. Восстановление связи через __seconds__ с.",
"conferenceDisconnectTitle": "Вы отключены.",
"conferenceDisconnectMsg": "Следует проверить интернет-соединение. Попытка восстановления связи через __seconds__ с.",
"dismiss": "Отклонить",
"rejoinNow": "Подключиться снова",
"maxUsersLimitReachedTitle": "Достигнут лимит числа участников",
"maxUsersLimitReached": "Достигнуто максимальное число участников. Больше нельзя. Пожалуйста, свяжитесь с организатором встречи или попробуйте позже!",
"createPassword": "Создать пароль",
"detectext": "Ошибка при попытке определить расширение для совместного использования экрана.",
"failtoinstall": "Невозможно установить расширение для совместного использования рабочего стола",
"failedpermissions": "Невозможно получить права на использование локального микрофона и/или камеры.",
"conferenceReloadTitle": "",
"conferenceReloadMsg": "",
"conferenceDisconnectTitle": "",
"conferenceDisconnectMsg": "",
"rejoinNow": "",
"maxUsersLimitReached": "Достигнут максимум количества участников конференции. Конференция заполнена. Пожалуйста попробуйте позже!",
"lockTitle": "Блокировка не удалась",
"lockMessage": "Не удалось запереть конференцию",
"warning": "Внимание",
"passwordNotSupportedTitle": "Пароль не поддержвается",
"passwordNotSupported": "Установка пароля не поддерживается.",
"passwordNotSupported": "Пароли для комнат сейчас не поддерживаются.",
"internalErrorTitle": "Внутренняя ошибка",
"internalError": "Что-то пошло не так. Ошибка: __error__",
"unableToSwitch": "Невозможно сменить трансляцию видео.",
"SLDFailure": "Что-то пошло не так, невозможно выключить звук! (ошибка SLD)",
"SRDFailure": "Что-то пошло не так, невозможно выключить видео! (ошибка SRD)",
"oops": "Ой!",
"currentPassword": "Текущий пароль",
"internalError": "Ой! Что-то пошло не так. Возникла следующая ошибка: [setRemoteDescription]",
"unableToSwitch": "Невозможно сменить видео трансляцию.",
"SLDFailure": "Ёпрст! Что-то пошло не так и мы не можем отключить звук! (ошибка SLD)",
"SRDFailure": "Ёпрст! Что-то пошло не так и мы не можем остановить видео! (ошибка SRD)",
"oops": "Ёпрст!",
"currentPassword": "Текущим паролем является",
"passwordLabel": "Пароль",
"defaultError": "Произошла ошибка",
"defaultError": "Какая-то ошибка",
"passwordRequired": "Требуется пароль",
"Ok": "Ok",
"done": "Готово",
"Remove": "Удалить",
"removePassword": "Удалить пароль",
"shareVideoTitle": "Поделиться видео",
"shareVideoLinkError": "Пожалуйста, укажите корректную ссылку Youtube.",
"removeSharedVideoTitle": "Убрать видео",
"removeSharedVideoMsg": "Уверены, что хотите убрать видео, которым поделились?",
"alreadySharedVideoMsg": "Другой участник уже показывает свое видео. Нельзя показывать два видео одновременно.",
"alreadySharedVideoTitle": "Допускается показ только одного видео",
"WaitingForHost": "Ждем организатора...",
"WaitForHostMsg": "Встреча <b>__room__ </b> еще не началась. Если вы организатор встречи, пожалуйста, представьтесь. Если нет, подождите организатора.",
"IamHost": "Я организатор",
"Cancel": "Отмена",
"Submit": "ОК",
"shareVideoLinkError": "Пожалуйста введите корректную youtube ссылку.",
"removeSharedVideoTitle": "Удалить общее видео",
"removeSharedVideoMsg": "Вы уверрены, что хотите удалить ваше расшаренное видео?",
"alreadySharedVideoMsg": "Другая участница сейчас делится видео. В этой конференции можно делиться только одним видео одновременно.",
"WaitingForHost": "Ожидание хоста...",
"WaitForHostMsg": "Конференция <b>__room__ </b> ещё не началась. Если вы её хост - аутентифицируйтесь. Или сидите ждите хоста.",
"IamHost": "Я хост",
"Cancel": "Отменить",
"Submit": "Принять",
"retry": "Повторить",
"logoutTitle": "Завершить сеанс",
"logoutQuestion": "Уверены, что хотите выйти и остановить встречу?",
"sessTerminated": "Связь прервана",
"logoutQuestion": "Вы уверены, что хотите выйти и остановить конференцию?",
"sessTerminated": "Сеанс закрыт",
"hungUp": "Вы повесили трубку",
"joinAgain": "Войти снова",
"joinAgain": "Войдите заново",
"Share": "Поделиться",
"Save": "Сохранить",
"recording": "Запись",
"recordingToken": "Введите токен для записи",
"passwordCheck": "Вы уверены, что хотите удалить ваш пароль?",
"passwordMsg": "Введите пароль для вашей комнаты",
"shareLink": "Поделитесь ссылкой на звонок",
"settings1": "Настройка Вашей конференции",
"settings2": "Участница подключилась без звука",
"settings3": "Нужны имена<br/><br/>Установите пароль, чтобы запереть Вашу комнату:",
"yourPassword": "Введите новый пароль",
"Back": "Назад",
"serviceUnavailable": "Служба недоступна",
"gracefulShutdown": "Технические работы. Пожалуйста, попробуйте позже.",
"gracefulShutdown": "Сервис закрыт на переучёт. Пожалуйста попробуйте позже.",
"Yes": "Да",
"reservationError": "Ошибка системы резервирования",
"reservationError": "Ошибка системы резервации",
"reservationErrorMsg": "Код ошибки: __code__, сообщение: __msg__",
"password": "Введите пароль",
"userPassword": "пароль пользователя",
"token": "токен",
"tokenAuthFailedTitle": "Ошибка аутентификации",
"tokenAuthFailed": "Извините, вам не разрешено присоединиться к этому сеансу связи.",
"displayNameRequired": "Требуется имя",
"enterDisplayName": "Пожалуйста, введите имя",
"feedbackHelp": "Ваш отзыв поможет нам улучшать параметры видеосвязи.",
"feedbackQuestion": "Расскажите, как вам понравилась связь!",
"thankYou": "Спасибо, что используете __appName__!",
"sorryFeedback": "Жаль. Может, расскажете подробнее?",
"tokenAuthFailed": "Извините, вам не разрешено присоединиться к этому звонку.",
"displayNameRequired": "Требуется отображаемое имя",
"enterDisplayName": "Пожалуйста, введите Ваше имя экрана",
"extensionRequired": "Требуется расширение:",
"firefoxExtensionPrompt": "Нужно установить расширение Firefox, чтобы совместно пользоваться экраном. Попробуйте позже, скачав его <a href='__url__'>отсюда</a>!",
"feedbackHelp": "Ваша поддержка поможет нам улучшить опыт видео.",
"feedbackQuestion": "Расскажите нам о вашем звонке!",
"thankYou": "Спасибо за использование __appName__!",
"sorryFeedback": "Мы удручены услышанным. Может расскажете поподробнее?",
"liveStreaming": "Трансляция",
"streamKey": "Ключ трансляции",
"streamKey": "Имя/ключ трансляции",
"startLiveStreaming": "Начать трансляцию",
"startRecording": "Начать запись",
"stopStreamingWarning": "Уверены, что хотите остановить трансляцию?",
"stopRecordingWarning": "Уверены, что хотите остановить запись?",
"stopStreamingWarning": "Вы уверены, что хотите остановить трансляцию?",
"stopRecordingWarning": "Вы уверены, что хотите остановить запись?",
"stopLiveStreaming": "Остановить трансляцию",
"stopRecording": "Остановить запись",
"doNotShowMessageAgain": "Больше не показывать это сообщение",
"permissionDenied": "Доступ запрещен",
"screenSharingFailedToInstall": "Ошибка установки расширения для показа экрана.",
"screenSharingFailedToInstallTitle": "Расширение для показа экрана не установлено",
"screenSharingFirefoxPermissionDeniedError": "Что-то пошло не так, когда мы пытались поделиться вашим экраном. Пожалуйста, убедитесь, что вы дали нам разрешение на это.",
"screenSharingFirefoxPermissionDeniedTitle": "Ошибка показа экрана!",
"screenSharingPermissionDeniedError": "Ошибка доступа к вашему расширению для показа экрана. Пожалуйста, перезапустите браузер и попробуйте снова.",
"cameraUnsupportedResolutionError": "Ваша камера не поддерживает необходимое разрешение видео.",
"cameraUnknownError": "Неизвестная ошибка использования камеры.",
"cameraPermissionDeniedError": "Нет доступа к камере. Вы можете участвовать во встрече, но другие не будут вас видеть. Используйте значок камеры в адресной строке браузера, чтобы устранить проблему.",
"cameraNotFoundError": "Камера не обнаружена.",
"cameraConstraintFailedError": "Камера не отвечает определенным требованиям.",
"micUnknownError": "Неизвестная ошибка использования микрофона.",
"micPermissionDeniedError": "Нет доступа к микрофону. Вы можете участвовать во встрече, но другие не будут вас слышать. Используйте значок камеры в адресной строке браузера, чтобы устранить проблему.",
"micNotFoundError": "Микрофон не обнаружен.",
"micConstraintFailedError": "Ваш микрофон не отвечает определенным требованиям.",
"micNotSendingDataTitle": "Нет доступа к микрофону",
"micNotSendingData": "Ошибка доступа к микрофону. Пожалуйста, выберите другое устройство из меню настроек или попробуйте перезапустить приложение.",
"cameraNotSendingDataTitle": "Нет доступа к камере",
"cameraNotSendingData": "Ошибка доступа к камере. Пожалуйста, проверьте, не использует ли камеру какая-нибудь другая программа. Вы можете также выбрать другое устройство из меню настроек или попробовать перезапустить приложение.",
"doNotShowWarningAgain": "Больше не показывать это предупреждение",
"doNotShowMessageAgain": "Не показывать больше это сообщение",
"permissionDenied": "Доступ запрещён",
"screenSharingPermissionDeniedError": "У Вас нет прав совместно использовать Ваш экран",
"micErrorPresent": "Произошла ошибка при подключении к Вашему микрофону",
"cameraErrorPresent": "Произошла ошибка при подключении к Вашей камере",
"cameraUnsupportedResolutionError": "Ваша камера не поддерживает необходимое разрешение.",
"cameraUnknownError": "Не могу использовать камеру по неизвестной причине.",
"cameraPermissionDeniedError": "У вас нет прав на использование камеры. Вы можете участвовать в конференции, но другие не будут Вас видеть. Используйте значок с камерой в строке адреса, чтобы устранить проблему.",
"cameraNotFoundError": "Камера не была найдена.",
"cameraConstraintFailedError": "",
"micUnknownError": "Не могу пользоваться микрофоном по непонятным причинам.",
"micPermissionDeniedError": "Вы не дали прав на использование микрофона. Вы все-равно можете присоединиться к конференции, но никто не будет Вас слышать. Используйте иконку с камерой в адресной строке браузера, чтобы исправить это.",
"micNotFoundError": "Микрофон не был найден.",
"micConstraintFailedError": "",
"micNotSendingData": "Мы не можем получить доступ к вашему микрофону. Пожалуйста, выберите другое устройство из меню настроек или попробуйте перезапустить приложение.",
"cameraNotSendingData": "Мы не можем получить доступ к вашей камере. Пожалуйста, проверьте, используется ли это устройство другим приложением, выберите другое устройство из меню настроек или же перезапустите приложение.",
"goToStore": "Перейти к интернет-магазину",
"externalInstallationTitle": "Требуется расширение",
"externalInstallationMsg": "Вам необходимо установить наше дополнение для совместного использования рабочего стола.",
"inlineInstallationMsg": "Вам необходимо установить наше дополнение для совместного использования рабочего стола.",
"inlineInstallExtension": "Установить",
"muteParticipantTitle": "Выключить звук этому участнику?",
"muteParticipantBody": "Вы не можете включить им звук, но они могут сделать это сами в любое время.",
"inlineInstallExtension": "",
"muteParticipantTitle": "Приглушить этого участника?",
"muteParticipantBody": "Вы не сможете перестать глушить их, но они могут сделать это сами в любое время.",
"muteParticipantButton": "Выключить звук",
"remoteControlTitle": "Удаленное управление рабочим столом",
"remoteControlRequestMessage": "Разрешить __user__ удаленное управление вашим рабочим столом?",
"remoteControlShareScreenWarning": "Если нажмете \"Разрешить\", то поделитесь своим экраном!",
"remoteControlDeniedMessage": "__user__ отклонил ваш запрос на удаленное управление!",
"remoteControlAllowedMessage": "__user__ принял ваш запрос на удаленное управление!",
"remoteControlErrorMessage": "Произошла ошибка при попытке запросить разрешения удаленного управления от __user__.",
"startRemoteControlErrorMessage": "Ошибка начала сессии удаленного управления!",
"remoteControlStopMessage": "Сессия удаленного управления завершена!",
"close": "Закрыть",
"shareYourScreen": "Показать экран",
"yourEntireScreen": "Весь экран",
"applicationWindow": "Окно приложения"
"remoteControlTitle": "",
"remoteControlRequestMessage": "",
"remoteControlShareScreenWarning": "",
"remoteControlDeniedMessage": "__user__ отклонил ваш запрос на дистанционное управление!",
"remoteControlAllowedMessage": "__user__ принял ваш запрос на дистанционное управление!",
"remoteControlErrorMessage": "Произошла ошибка при попытке запросить разрешения удалённого управления от __user__!",
"startRemoteControlErrorMessage": "",
"remoteControlStopMessage": "Сессия дистанционного управления завершена!",
"close": "",
"shareYourScreen": "",
"yourEntireScreen": "",
"applicationWindow": ""
},
"\u0005dialog": {},
"email": {
"sharedKey": [
"Эта встреча защищена паролем. Пожалуйста, используйте для входа:",
"Эта конференция защищена паролем. Пожалуйста, используйте это пин для входа:",
"",
"",
"__sharedKey__",
"",
""
],
"subject": "Приглашение в __appName__ (__conferenceName__)",
"subject": "Приглашение для __appName__ (__conferenceName__)",
"body": [
"Здравствуйте! Приглашаю вас на текущую встречу __appName__.",
"Привет! я бы хотел пригласить тебя на __appName__ конференцию, которую мы как раз начали.",
"",
"",
"Для подключения, пожалуйста, используйте ссылку:",
"Пожелуста, следуй по ссылке, чтобы подключиться к конференции.",
"",
"",
"__roomUrl__",
"",
"",
"__sharedKeyText__",
"Имейте в виду, что __appName__ в настоящее время поддерживается только в __supportedBrowsers__. Вам понадобится один из этих браузеров.",
"Имей в виду, что __appName__ сейчас поддерживается только __supportedBrowsers__, так что полюзуйся одним из этих браузеров.",
"",
"",
"Присоединяйтесь!"
"Услышимся через секунду!"
],
"and": "и"
},
"connection": {
"ERROR": "Ошибка",
"CONNECTING": "Подключение",
"CONNECTING": "Идёт подключение",
"RECONNECTING": "Проблема с сетью. Переподключение...",
"CONNFAIL": "Сбой подключения",
"AUTHENTICATING": "Аутентификация",
@@ -375,190 +397,89 @@
"ATTACHED": "Прикреплено"
},
"recording": {
"busy": "Мы стараемся обеспечить больше ресурсов для записи. Пожалуйста, попробуйте через несколько минут.",
"busyTitle": "Все записывающие устройства заняты",
"buttonTooltip": "Запись (старт/стоп)",
"error": "Ошибка записи. Пожалуйста, попробуйте позже.",
"failedToStart": "Ошибка начала записи",
"off": "Запись остановлена",
"pending": "Записываем ожидаем подключение участницы...",
"on": "Запись",
"pending": "Запись приостановлена: ожидаем подключение участника...",
"serviceName": "Служба записи",
"unavailable": "Служба __serviceName__ сейчас недоступна. Мы работаем над исправлением этой ошибки. Пожалуйста, попробуйте позже.",
"unavailableTitle": "Запись невозможна"
"off": "Запись остановлена",
"failedToStart": "Ошибка при начале записи",
"buttonTooltip": "Начать / Остановить запись",
"error": "Ошибка записи. Попробуйте позже.",
"unavailable": "Сервис записи сейчас недоступен. Попробуйте позже."
},
"liveStreaming": {
"busy": "Освобождаем новые ресурсы для трансляции. Пожалуйста, попробуйте снова через несколько минут.",
"busyTitle": "Все ресурсы для трансляции уже задействованы",
"buttonTooltip": "Трансляция (старт/стоп)",
"changeSignIn": "Переключить аккаунты.",
"choose": "Выбрать трансляцию",
"chooseCTA": "Выберите трансляцию. Вы вошли в систему как __email__. ",
"enterStreamKey": "Введите ваш ключ трансляции YouTube.",
"error": "Ошибка трансляции. Пожалуйста, попробуйте снова.",
"errorAPI": "Произошла ошибка при доступе к вашим трансляциям на YouTube. Повторите попытку входа в систему.",
"failedToStart": "Ошибка трансляции видео",
"off": "Трансляция остановлена",
"pending": "Начинаю трансляцию...",
"on": "Трансляция",
"pending": "Начинаем трансляцию...",
"serviceName": "Служба трансляции",
"signIn": "Войти через Google",
"signInCTA": "Войдите или введите свой ключ трансляции YouTube.",
"start": "Начать трансляцию",
"streamIdHelp": "Что это?",
"unavailableTitle": "Трансляция недоступна"
},
"videoSIPGW": {
"busy": "Мы работаем над высвобождением ресурсов. Пожалуйста, попробуйте через несколько минут.",
"busyTitle": "Служба сейчас занята",
"errorInvite": "Встреча еще не началась. Пожалуйста, попробуйте позже.",
"errorInviteTitle": "Ошибка приглашения в комнату",
"errorAlreadyInvited": "__displayName__ уже приглашен",
"errorInviteFailedTitle": "Ошибка приглашения __displayName__",
"errorInviteFailed": "Мы работаем над решением проблемы. Пожалуйста, попробуйте позже.",
"pending": "__displayName__ был приглашен",
"serviceName": "Служба комнат",
"unavailableTitle": "Служба недоступна"
"off": "Трансляция остановлена",
"unavailable": "Служба трансляций сейчас недоступна. Попробуйте позже.",
"failedToStart": "Трансляция видео не может быть начата",
"buttonTooltip": "Начать / Остановить прямую трансляцию",
"streamIdRequired": "Пожалуйста введите идентификатор трансляции, чтобы запустить её.",
"streamIdHelp": "Где я могу найти это?",
"error": "Не удалось начать трансляцию. Попробуйте снова.",
"busy": "Все рекордеры сейчас заняты. Попробуйте позже."
},
"speakerStats": {
"hours": "__count__ч",
"minutes": "__count__м",
"hours": "",
"minutes": "",
"name": "Имя",
"seconds": "__count__с",
"speakerStats": "Статистика выступлений",
"speakerTime": "Время выступлений"
"seconds": "",
"speakerStats": "",
"speakerTime": ""
},
"deviceSelection": {
"deviceSettings": "Настройки устройства",
"noPermission": "Нет доступа",
"previewUnavailable": "Предпросмотр недоступен",
"selectADevice": "Выбор устройства",
"testAudio": "Тест звука"
"deviceSettings": "",
"noPermission": "",
"previewUnavailable": "",
"selectADevice": "",
"testAudio": ""
},
"invite": {
"addPassword": "",
"callNumber": "",
"enterID": "",
"howToDialIn": "",
"hidePassword": "",
"inviteTo": "",
"invitedYouTo": "",
"locked": "",
"showPassword": "",
"unlocked": ""
},
"videoStatus": {
"callQuality": "Качество связи",
"hd": "HD",
"hdTooltip": "Видео высокого качества",
"highDefinition": "Высокое качество",
"labelTooltipAudioOnly": "Включен режим \"только звук\"",
"labelTooiltipNoVideo": "Нет видео",
"labelTooltipVideo": "Текущее качество видео",
"ld": "LD",
"ldTooltip": "Видео низкого качества",
"lowDefinition": "Низкое качество",
"onlyAudioAvailable": "Только звук",
"onlyAudioSupported": "В этом браузере разрешен только звук.",
"p2pEnabled": "Включен режим \"точка-к-точке\"",
"p2pVideoQualityDescription": "В режиме \"точка-к-точке\" качество входящего сигнала может быть установлено в позицию \"высокое\" или \"только звук\". Остальные варианты в этом режиме не работают.",
"recHighDefinitionOnly": "Предпочтительно высокое качество.",
"sd": "SD",
"sdTooltip": "Видео стандартного качества",
"standardDefinition": "Стандартное качество (SD)",
"qualityButtonTip": "Изменить качество входящего видео"
"callQuality": "",
"changeVideoTip": "",
"hd": "",
"highDefinition": "",
"ld": "",
"lowDefinition": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "",
"sd": "",
"standardDefinition": "",
"qualityButtonTip": ""
},
"dialOut": {
"statusMessage": "сейчас __status__"
"dial": "Дозвон",
"dialOut": "",
"statusMessage": "",
"enterPhone": "",
"phoneNotAllowed": ""
},
"addPeople": {
"add": "Пригласить",
"countryNotSupported": "Эта страна пока не поддерживается.",
"countryReminder": "Вызов не в США? Пожалуйста, убедитесь, что указали код страны!",
"disabled": "",
"invite": "Пригласить",
"loading": "Поиск людей и номеров телефонов",
"loadingNumber": "Поиск людей для приглашения",
"loadingPeople": "Поиск людей для приглашения",
"noResults": "Поиск не дал результата",
"noValidNumbers": "Пожалуйста, введите номер телефона",
"notAvailable": "Поиск не дал результата",
"searchNumbers": "Добавить номера телефонов",
"searchPeople": "Поиск не дал результата",
"searchPeopleAndNumbers": "Поиск людей или добавление их телефонов",
"telephone": "Номер: __number__",
"title": "Пригласить людей на эту встречу",
"failedToAdd": "Ошибка добавления участников"
"add": "Добавить",
"noResults": "",
"searchPlaceholder": "",
"title": "",
"failedToAdd": ""
},
"inlineDialogFailure": {
"msg": "Небольшая заминка.",
"retry": "Попробовать снова",
"support": "Поддержка",
"supportMsg": "Если это продолжится, свяжитесь с"
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
},
"deviceError": {
"cameraError": "Ошибка доступа к камере",
"microphoneError": "Ошибка доступа к микрофону",
"cameraPermission": "Ошибка доступа к микрофону",
"microphonePermission": "Нет разрешения на доступ к микрофону"
},
"feedback": {
"average": "Средне",
"bad": "Плохо",
"good": "Хорошо",
"detailsLabel": "Расскажите подробнее.",
"rateExperience": "Оценка качества связи",
"veryBad": "Очень плохо",
"veryGood": "Очень хорошо"
},
"info": {
"addPassword": "Добавить пароль",
"cancelPassword": "Убрать пароль",
"conferenceURL": "Ссылка:",
"country": "Страна",
"dialANumber": "Чтобы присоединиться к встрече, наберите один из этих номеров, а затем введите PIN-код: __conferenceID __ #",
"dialInNumber": "Номер:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "К сожалению, набор номера в настоящее время не поддерживается.",
"genericError": "Что-то пошло не так.",
"inviteLiveStream": "Трансляция этой встречи: __url__",
"invitePhone": "Чтобы присоединиться по телефону, наберите __number__ и введите PIN-код: __conferenceID __#",
"invitePhoneAlternatives": "Посмотреть другие номера телефонов: __url__",
"inviteURL": "Присоединиться к видеоконференции: __url__",
"liveStreamURL": "Трансляция:",
"moreNumbers": "Больше номеров",
"noNumbers": "Нет номеров для набора.",
"noPassword": "нет",
"noRoom": "Для набора номера не было указано ни одной комнаты.",
"numbers": "Номера для набора",
"password": "Пароль:",
"title": "Поделиться",
"tooltip": "Получить информацию об этой встрече"
},
"settingsView": {
"alertOk": "OK",
"alertTitle": "Внимание",
"alertURLText": "Ошибка адреса сервера",
"conferenceSection": "Номера для набора",
"displayName": "Отображаемое имя",
"email": "Email",
"header": "Настройки",
"profileSection": "Профиль",
"serverURL": "Адрес сервера",
"startWithAudioMuted": "Начать с отключенным звуком",
"startWithVideoMuted": "Начать с отключенным видео"
},
"calendarSync": {
"later": "Позже",
"next": "Предстоящие",
"nextMeeting": "следующая встреча",
"now": "Сейчас",
"permissionButton": "Открыть настройки",
"permissionMessage": "Для показа ваших встреч в приложении нужен доступ к календарю."
},
"recentList": {
"today": "Сегодня",
"yesterday": "Вчера",
"earlier": "Ранее"
},
"sectionList": {
"pullToRefresh": "Потяните для обновления"
},
"deepLinking": {
"title": "Запуск вашей встречи в __app __...",
"description": "Ничего не случилось? Мы попытались запустить вашу встречу в настольном приложении __app__. Повторите попытку или запустите ее в веб-приложении __app__.",
"tryAgainButton": "Повторите в настольном приложении",
"launchWebButton": "Запустить в браузере",
"appNotInstalled": "Чтобы присоединиться к этой встрече на телефоне, нужно мобильное приложение __app__.",
"downloadApp": "Скачать приложение",
"openApp": "Перейти к приложению"
"cameraPermission": "",
"microphonePermission": ""
}
}

View File

@@ -1,551 +0,0 @@
{
"contactlist_plural": "__count__ 位成員",
"passwordSetRemotely": "由另一位成員設置",
"poweredby": "技術支援",
"inviteUrlDefaultMsg": "您的會議正在建立起來………",
"me": "我",
"speaker": "",
"raisedHand": "請求發言",
"defaultNickname": "例如 阿美 志明",
"defaultLink": "例如 __url__",
"audioDevices": {
"bluetooth": "藍牙",
"headphones": "耳機",
"phone": "電話",
"speaker": "發言者"
},
"audioOnly": {
"audioOnly": "僅用音訊",
"featureToggleDisabled": "在僅用音訊模式下,開關 __feature__ 功能是停用的"
},
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"electronGrantPermissions": "",
"firefoxGrantPermissions": "當瀏覽器要求權限允許時,請選擇<b><i>分享設備</i></b> ",
"operaGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>允許</i></b>",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>OK</i></b>",
"nwjsGrantPermissions": "請允許權限使用您的攝影裝置和麥克風",
"edgeGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>是的</i></b>"
},
"keyboardShortcuts": {
"keyboardShortcuts": "快捷鍵",
"raiseHand": "舉手發言或不作發言",
"pushToTalk": "按鍵通話",
"toggleScreensharing": "在攝影鏡頭和螢幕分享之間進行切換",
"toggleFilmstrip": "顯示或隱藏視訊",
"toggleShortcuts": "顯示或隱藏說明選單",
"focusLocal": "聚焦於自己的視訊",
"focusRemote": "聚焦於另一位通話者的視訊",
"toggleChat": "開啟或關閉聊天",
"mute": "靜音或解除靜音",
"fullScreen": "進入或退出全螢幕",
"videoMute": "啟動或停止自己的攝影裝置",
"showSpeakerStats": "顯示發言者數據"
},
"welcomepage": {
"appDescription": "快來使用吧,團隊全部成員使用視訊通話,可以邀請任何您所認識的人。 __app__ 是一套完全加密、100% 開放源碼的視訊會議解決方案。無需註冊帳號,無時無刻不分日夜均可免費使用。",
"audioVideoSwitch": {
"audio": "語音",
"video": "視訊"
},
"calendar": "日曆",
"go": "開始",
"join": "加入",
"privacy": "隱私",
"roomname": "輸入會議室名稱",
"roomnameHint": "請輸入您想加入的會議室 URL 網址或名稱。您可以用個名稱來建立會議室,只要其他人輸入相同的名稱就能加入會議室喔。",
"sendFeedback": "發送回報",
"terms": "條款",
"title": "更加安全、更具彈性、又完全免費的視訊會議系統"
},
"startupoverlay": {
"policyText": " ",
"title": "__app__ 需要使用您的麥克風和攝影裝置。"
},
"suspendedoverlay": {
"title": "由於電腦進入休眠,您的視訊通話已經中斷。",
"text": "按下 <i>重新加入</i> 按鈕重新連接。",
"rejoinKeyTitle": "重新加入"
},
"toolbar": {
"addPeople": "新增人員到您的通話中",
"audioonly": "啟用/停用 僅用音訊模式(節省頻寬)",
"callQuality": "管理通話品質",
"enterFullScreen": "觀看全螢幕",
"exitFullScreen": "跳出全螢幕",
"feedback": "留言回報",
"moreActions": "更多動作",
"mute": "靜音 / 解除靜音",
"videomute": "啟動/停止 攝影裝置",
"authenticate": "驗證",
"lock": "鎖定/解鎖 會議室",
"chat": "開啟/關閉 聊天",
"etherpad": "開啟/關閉 分享文件檔案",
"documentOpen": "開啟分享的文件檔案",
"documentClose": "關閉分享的文件檔案",
"sharedvideo": "分享 YouTube 視訊",
"sharescreen": "螢幕分享",
"stopSharedVideo": "停止 YouTube 視訊",
"fullscreen": "觀看/跳出 全螢幕",
"sip": "播打 SIP 號碼",
"Settings": "",
"hangup": "留言",
"login": "登入",
"logout": "",
"sharedVideoMutedPopup": "您分享的視訊已經靜音,現在可以和其他成員交談了。",
"micMutedPopup": "您的麥克風已經處於靜音,可以觀看分享視訊了。",
"talkWhileMutedPopup": "您要發言嗎? 目前您處於靜音。",
"unableToUnmutePopup": "當分享視訊正在使用時,您不能解除靜音。",
"cameraDisabled": "攝影裝置無法使用",
"micDisabled": "麥克風無法使用",
"filmstrip": "顯示/隱藏 視訊",
"profile": "編輯您的簡介",
"raiseHand": "舉手/取消 請求發言",
"shortcuts": "查看快捷鍵",
"speakerStats": "發言者數據"
},
"chat": {
"nickname": {
"title": "請在下面欄位輸入暱稱",
"popover": "選擇暱稱"
},
"messagebox": "請輸入文字..."
},
"settings": {
"title": "",
"update": "更新",
"name": "",
"startAudioMuted": "全部人啟動時處於靜音",
"startVideoMuted": "全部人啟動時隱藏視訊畫面",
"selectCamera": "攝影裝置",
"selectMic": "麥克風",
"selectAudioOutput": "音訊輸出",
"followMe": "全部人跟隨仿照我",
"noDevice": "",
"cameraAndMic": "攝影裝置和麥克風",
"moderator": "主持人",
"password": "設定密碼",
"audioVideo": "音訊和視訊"
},
"profile": {
"title": "",
"setDisplayNameLabel": "設定您的顯示名稱",
"setEmailLabel": "設置您的大頭人像電子信箱",
"setEmailInput": "輸入您的電子信箱"
},
"videothumbnail": {
"moderator": "主持人",
"videomute": "成員已經停用攝影裝置",
"mute": "成員處於靜音",
"kick": "踢出",
"muted": "處於靜音",
"domute": "",
"flip": "翻轉",
"remoteControl": "遠端控制"
},
"connectionindicator": {
"header": "連接資料",
"bitrate": "比特率:",
"packetloss": "丟包:",
"resolution": "解析度:",
"framerate": "影格率:",
"less": "顯示較少",
"more": "顯示更多",
"address": "地址:",
"remoteport": "遠端端口:",
"localport": "本地端口:",
"localaddress": "本地地址:",
"remoteaddress": "遠端地址:",
"transport": "傳輸:",
"bandwidth": "估計頻寬:",
"na": "一旦會議啟動,即可回到此處查看連接資訊",
"turn": " (轉)",
"quality": {
"good": "",
"inactive": "未啟用",
"lost": "漏失",
"nonoptimal": "不甚理想",
"poor": "不好"
},
"status": "連接:"
},
"notify": {
"disconnected": "已經中斷連接",
"moderator": "主持人權限已經取得!",
"connectedOneMember": "__name__ 已經連接",
"connectedTwoMembers": "__first__ 與 __second__ 已經連接",
"connectedThreePlusMembers": "__name__ 與 __count__ 位其他成員已經連接",
"somebody": "某人",
"me": "自己",
"focus": "會議焦點",
"focusFail": "__component__ 無法使用 - 請在 __ms__ 秒後重試",
"grantedTo": "主持人權限已授予 __to__!",
"grantedToUnknown": "主持人權限已經授予 $t(somebody) ",
"muted": "您已經啟動通話,並處於靜音狀態。",
"mutedTitle": "您目前處於靜音!",
"raisedHand": "請求發言。",
"suboptimalExperienceTitle": "瀏覽器警告",
"suboptimalExperienceDescription": "呃……恐怕您對 __appName__ 的體驗不是很好,我們正在嘗試找方法改進對此瀏覽器的支援。現下敬請選用 <a href='static/recommendedBrowsers.html' target='_blank'>全力支援的瀏覽器</a> 來進行。"
},
"dialog": {
"allow": "允許",
"kickMessage": "您已經被踢出會議!",
"popupErrorTitle": "彈出視窗遭到阻攔",
"popupError": "您的瀏覽器在此網站上阻攔彈出視窗。請在瀏覽器的安全設置中開啟它並再試一次。",
"passwordErrorTitle": "密碼錯誤",
"passwordError": "此會議目前已受密碼保護。只有會議的擁有者可以設定密碼。",
"passwordError2": "此會議目前未受密碼保護。只有會議的擁有者可以設定密碼。",
"connectError": "喔哦!發生錯誤,無法連接至會議。",
"connectErrorWithMsg": "喔哦!發生錯誤,無法連接至會議: __msg__",
"incorrectPassword": "錯誤的用戶名稱或密碼",
"connecting": "",
"copy": "複製",
"contactSupport": "聯絡支援",
"error": "",
"detectext": "嘗試偵測桌面分享擴充應用程式時發生錯誤。",
"failedpermissions": "未能取得使用本地麥克風或攝影裝置的權限。",
"conferenceReloadTitle": "不好意思,出錯了。",
"conferenceReloadMsg": "我們正試著修復狀況。重新連接於 __seconds__ 秒內……",
"conferenceDisconnectTitle": "您已經被中斷連接。",
"conferenceDisconnectMsg": "請檢查一下網路連接。將在 __seconds__ 秒後重新連接…",
"dismiss": "解除",
"rejoinNow": "立即重新加入",
"maxUsersLimitReachedTitle": "成員人數已經達到上限",
"maxUsersLimitReached": "由於會議已達到人數上限,額滿不能加入。請聯絡會議發起人,或是稍後再次嘗試!",
"lockTitle": "鎖定失敗",
"lockMessage": "鎖定會議失敗。",
"warning": "",
"passwordNotSupportedTitle": "不支援密碼",
"passwordNotSupported": "不支援設置會議密碼。",
"internalErrorTitle": "內部錯誤",
"internalError": "喔哦!出現了點問題。發生錯誤: __error__",
"unableToSwitch": "無法切換視訊串流。",
"SLDFailure": "喔哦!發生錯誤,無法靜音! (SLD故障)",
"SRDFailure": "喔哦!發生錯誤,無法停止視訊! (SRD故障)",
"oops": "喔哦!",
"currentPassword": "目前的密碼是",
"passwordLabel": "密碼",
"defaultError": "存在某種錯誤",
"passwordRequired": "需要密碼",
"Ok": "Ok",
"done": "完成",
"Remove": "移除",
"removePassword": "移除密碼",
"shareVideoTitle": "分享視訊",
"shareVideoLinkError": "請提供正確的 YouTube 連結。",
"removeSharedVideoTitle": "移除分享視訊",
"removeSharedVideoMsg": "您確定要移除自己的分享視訊嗎?",
"alreadySharedVideoMsg": "另一位成員正準備分享視訊。會議一次僅允許一位視訊分享。",
"alreadySharedVideoTitle": "一次只能允許一位視訊分享",
"WaitingForHost": "等侯主辦人………",
"WaitForHostMsg": "會議 <b>__room__ </b> 尚未啟動。如果您是主辦人,請授權開始,否則請等候主辦人。",
"IamHost": "我是主辦人",
"Cancel": "取消",
"Submit": "提交",
"retry": "重試",
"logoutTitle": "登出",
"logoutQuestion": "您確定要登出並停止會議嗎?",
"sessTerminated": "通話已經終止",
"hungUp": "我方掛斷",
"joinAgain": "再次加入",
"Share": "",
"Save": "儲存",
"recording": "",
"recordingToken": "輸入錄製標記",
"Back": "返回",
"serviceUnavailable": "服務無法使用",
"gracefulShutdown": "本伺服器閉關維護中,請稍後再試。",
"Yes": "是的",
"reservationError": "預約系統錯誤",
"reservationErrorMsg": "錯誤碼: __code__, 訊息: __msg__",
"password": "輸入密碼",
"userPassword": "用戶密碼",
"token": "標記",
"tokenAuthFailedTitle": "",
"tokenAuthFailed": "對不起,您未被允許加入此會議。",
"displayNameRequired": "顯示名稱是必須的",
"enterDisplayName": "請輸入您的顯示名稱",
"feedbackHelp": "您的回報將幫助提昇視訊體驗。",
"feedbackQuestion": "請告訴我們本次通話體驗!",
"thankYou": "感謝您使用 __appName__",
"sorryFeedback": "很抱歉聽到這些,能告訴我們更多詳情嗎?",
"liveStreaming": "",
"streamKey": "直播串流密鑰",
"startLiveStreaming": "立即開始直播",
"startRecording": "啟動錄製作業",
"stopStreamingWarning": "確定要停止直播串流嗎?",
"stopRecordingWarning": "確定要停止錄製作業嗎?",
"stopLiveStreaming": "停止直播串流",
"stopRecording": "停止錄製作業",
"doNotShowMessageAgain": "不再顯示此訊息",
"permissionDenied": "權限受到禁止",
"screenSharingFailedToInstall": "喔哦!螢幕分享擴充程式安裝失敗。",
"screenSharingFailedToInstallTitle": "螢幕分享擴充安裝失敗",
"screenSharingFirefoxPermissionDeniedError": "嘗試進行螢幕分享時遇到問題。請確認您有賦予相對的權限允許。",
"screenSharingFirefoxPermissionDeniedTitle": "喔哦!我們無法啟動螢幕分享!",
"screenSharingPermissionDeniedError": "喔哦!您的視訊分享擴充權限發生一點問題。請重新載入再試一次。",
"cameraUnsupportedResolutionError": "您的攝影裝置不支援所需的視訊解析度。",
"cameraUnknownError": "由於不明原因,無法使用攝影裝置。",
"cameraPermissionDeniedError": "您未取得權限使用您的攝影裝置。您仍然可參加會議,但是其他人無法看到。可以利用位址欄中的攝影裝置按鈕來修復啟動。",
"cameraNotFoundError": "未發現攝影裝置。",
"cameraConstraintFailedError": "您的攝影裝置不符合要求。",
"micUnknownError": "不明原因造成麥克風無法使用。",
"micPermissionDeniedError": "您未取得權限使用麥克風。您仍然可參加會議,但是其他人無法聽到。可以利用位址欄中的攝影裝置按鈕來修復啟動。",
"micNotFoundError": "未發現麥克風。",
"micConstraintFailedError": "您的麥克風不符合要求。",
"micNotSendingDataTitle": "無法取用麥克風",
"micNotSendingData": "我們無法取用您的麥克風。請從設罝選單裡選擇其他設備或者重新裝載。",
"cameraNotSendingDataTitle": "無法取用攝影裝置",
"cameraNotSendingData": "我們無法取用您的攝影裝置。請檢查是否有其他程序正在使用這個設備,否則請從設置選單裡選擇其他設備或者重新裝載。",
"goToStore": "前往應用商店",
"externalInstallationTitle": "需要擴充應用程式",
"externalInstallationMsg": "",
"inlineInstallationMsg": "您需要安裝桌面分享擴充應用程式。",
"inlineInstallExtension": "立即安裝",
"muteParticipantTitle": "靜音這位成員?",
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
"muteParticipantButton": "靜音",
"remoteControlTitle": "遠端桌面控制",
"remoteControlRequestMessage": "您要允許 __user__ 遠端控制您的桌面嗎?",
"remoteControlShareScreenWarning": "注意:如果按下 \"允許\" 您將分享自己的螢幕!",
"remoteControlDeniedMessage": "__user__ 拒絕您進行遠端控制的要求!",
"remoteControlAllowedMessage": "__user__ 接受您進行遠端控制的要求!",
"remoteControlErrorMessage": "在嘗試向 __user__ 請求遠端控制權限時發生錯誤!",
"startRemoteControlErrorMessage": "嘗試啟動遠端控制階段時發生錯誤!",
"remoteControlStopMessage": "遠端控制階段結束!",
"close": "關閉",
"shareYourScreen": "分享自己的螢幕",
"yourEntireScreen": "自己的全螢幕",
"applicationWindow": "應用程式視窗"
},
"email": {
"sharedKey": [
"該會議受密碼保護,請在加入會議時使用下列密碼:",
"",
"",
"__sharedKey__",
"",
""
],
"subject": "邀請發至__appName__ (__conferenceName__)",
"body": [
"嗨, 我想邀請您加入剛剛建立的 __appName__ 會議。",
"",
"",
"請點按下面的連結來加入會議。",
"",
"",
"__roomUrl__",
"",
"",
"__sharedKeyText__",
" 請注意 __appName__ 目前僅支援下列瀏覽器__supportedBrowsers__, 敬請選用其一。",
"",
"",
"即刻加入與您交談喔!"
],
"and": "與"
},
"connection": {
"ERROR": "錯誤",
"CONNECTING": "連接中",
"RECONNECTING": "網絡錯誤發生。重新連接中………",
"CONNFAIL": "連接失敗",
"AUTHENTICATING": "驗證中",
"AUTHFAIL": "驗證失敗",
"CONNECTED": "已經連接",
"DISCONNECTED": "已經中斷連接",
"DISCONNECTING": "中斷連接中",
"ATTACHED": "已經附加"
},
"recording": {
"busy": "我們正在釋放錄製資源。請過幾分鐘後再試。",
"busyTitle": "全部錄製設備正在忙碌",
"buttonTooltip": "啟動/停止 錄製作業",
"error": "錄製作業失敗。請再次重試。",
"failedToStart": "錄製啟動失敗",
"off": "錄製作業已經停止",
"on": "錄製作業中",
"pending": "錄製作業正在等待成員加入……",
"serviceName": "錄製作業服務",
"unavailable": "喔哦__serviceName__ 目前無法使用。我們正在解決此問題,請稍後再試。",
"unavailableTitle": "錄製作業無法使用"
},
"liveStreaming": {
"busy": "我們正在釋放串流資源。請過幾分鐘後再試。",
"busyTitle": "全部串流設備正在忙碌",
"buttonTooltip": "啟動/停止 直播串流",
"changeSignIn": "切換帳號。",
"choose": "選擇直播串流",
"chooseCTA": "請選擇直播串流選項。您目前是以 __email__ 身份登入。",
"enterStreamKey": "在此輸入您的 YouTube 直播串流密鑰。",
"error": "直播串流失敗。請重試。",
"errorAPI": "取用您的 YouTube 播出時發生錯誤。請重新登入。",
"failedToStart": "直播串流啟動失敗",
"off": "直播串流已經停止",
"on": "直播串流中",
"pending": "啟動直播串流………",
"serviceName": "直播串流服務",
"signIn": "使用 Google 帳戶登入",
"signInCTA": "輸入 YouTube 直播串流密鑰,或登入 YouTube 帳號。",
"start": "啟動直播串流",
"streamIdHelp": "這是什麼?",
"unavailableTitle": "直播串流無法使用"
},
"videoSIPGW": {
"busy": "我們正在清理釋放資源。請過幾分鐘後再試。",
"busyTitle": "會議室服務正處於忙碌中",
"errorInvite": "會議尚未開始,請稍後再來。",
"errorInviteTitle": "錯誤邀請會議室",
"errorAlreadyInvited": "__displayName__ 已受邀請",
"errorInviteFailedTitle": "邀請 __displayName__ 失敗",
"errorInviteFailed": "我們正在解決問題。請稍後再試。",
"pending": "__displayName__ 已經邀請",
"serviceName": "會議室服務",
"unavailableTitle": "會議室服務無法使用"
},
"speakerStats": {
"hours": "__count__h",
"minutes": "__count__m",
"name": "名稱",
"seconds": "__count__s",
"speakerStats": "發言者數據",
"speakerTime": "發言者時間"
},
"deviceSelection": {
"deviceSettings": "設備設置",
"noPermission": "未取得權限",
"previewUnavailable": "預覽無法使用",
"selectADevice": "選擇設備",
"testAudio": "測試聲音"
},
"videoStatus": {
"callQuality": "通話品質",
"hd": "HD 高清",
"hdTooltip": "觀看高清視訊 HD",
"highDefinition": "高清品質 HD",
"labelTooltipAudioOnly": "僅有音訊模式已經啟用",
"labelTooiltipNoVideo": "沒有視訊",
"labelTooltipVideo": "目前視訊品質",
"ld": "LD 低清",
"ldTooltip": "觀看低清視訊 LD",
"lowDefinition": "低清品質 LD",
"onlyAudioAvailable": "僅有音訊可以使用",
"onlyAudioSupported": "在此瀏覽器我們僅支援音訊功能。",
"p2pEnabled": "點對點功能已經啟用",
"p2pVideoQualityDescription": "在點對點模式下,通話品質只能使有高清和僅用音訊兩個選項。其他選項只有在點對點模式退出後才可使用。",
"recHighDefinitionOnly": "將會偏好使用高清模式 HD。",
"sd": "SD 標清",
"sdTooltip": "觀看標清視訊 SD",
"standardDefinition": "標清品質 SD",
"qualityButtonTip": "修改接收視訊品質"
},
"dialOut": {
"statusMessage": "現在狀態為 __status__"
},
"addPeople": {
"add": "",
"countryNotSupported": "此目標區域尚未支援。",
"countryReminder": "嘗試在美國外地通話?請確認開頭使用的國家代碼!",
"disabled": "",
"invite": "邀請",
"loading": "尋找聯絡人及電話號碼",
"loadingNumber": "驗證電話號碼",
"loadingPeople": "正在尋搜人員進行邀請",
"noResults": "沒有符合要求的搜尋結果",
"noValidNumbers": "請輸入一組電話號碼",
"notAvailable": "您不可以邀請人員。",
"searchNumbers": "新增電話號碼",
"searchPeople": "尋找人員",
"searchPeopleAndNumbers": "尋找人員或新增電話號碼",
"telephone": "電話: __number__",
"title": "邀請人員參加會議",
"failedToAdd": "無法增加成員"
},
"inlineDialogFailure": {
"msg": "好像有點卡卡不順。",
"retry": "重試",
"support": "支援",
"supportMsg": "如果狀況一直發生,請聯絡"
},
"deviceError": {
"cameraError": "無法取用您的攝影裝置",
"microphoneError": "無法取用您的麥克風",
"cameraPermission": "無法獲得攝影裝置取用權限",
"microphonePermission": "無法獲得麥克風取用權限"
},
"feedback": {
"average": "普通中等",
"bad": "很差",
"good": "很好",
"detailsLabel": "告訴我們本次會議使用上更多結果。",
"rateExperience": "請您評價這次會議的體驗成效",
"veryBad": "極差",
"veryGood": "極好"
},
"info": {
"addPassword": "新增密碼",
"cancelPassword": "取消密碼",
"conferenceURL": "連結:",
"country": "國家",
"dialANumber": "要加入您的會議,請撥打其中之一的電話號碼並輸入 PIN__conferenceID__#",
"dialInNumber": "播入:",
"dialInConferenceID": "PIN 號碼:",
"dialInNotSupported": "抱歉,目前不支援電話播入。",
"genericError": "糟糕!出錯了。",
"inviteLiveStream": "要觀看這場會議的直播串流,點按此連結: __url__",
"invitePhone": "要想使用電話加入會議,請撥打 __number__ 並輸入 PIN __conferenceID__#",
"invitePhoneAlternatives": "查看更多電話號碼,點按此連結: __url__",
"inviteURL": "加入視訊會議,請點按此連結: __url__",
"liveStreamURL": "直播串流:",
"moreNumbers": "更多成員",
"noNumbers": "無播入號碼。",
"noPassword": "無",
"noRoom": "沒有會議室是指定要播打進入。",
"numbers": "播入號碼",
"password": "密碼:",
"title": "分享",
"tooltip": "取得關於會議的連接使用資訊"
},
"settingsView": {
"alertOk": "確認",
"alertTitle": "警告",
"alertURLText": "所輸入的伺服器 URL 是無效的",
"conferenceSection": "會議",
"displayName": "顯示名稱",
"email": "電子郵件",
"header": "設置",
"profileSection": "簡介",
"serverURL": "伺服器 URL",
"startWithAudioMuted": "啟動並音訊靜音",
"startWithVideoMuted": "啟動並視訊靜音"
},
"calendarSync": {
"later": "稍後",
"next": "即將推出",
"nextMeeting": "下次會議",
"now": "現在",
"permissionButton": "開啟設定",
"permissionMessage": "日曆允許權限是必須的,以列入您的會議於應用程式中。"
},
"recentList": {
"today": "今日",
"yesterday": "昨天",
"earlier": "稍早"
},
"sectionList": {
"pullToRefresh": "下滑以重新整理"
},
"deepLinking": {
"title": "發起您的會議於 __app__...",
"description": "沒有發生作用嗎?我們嘗試發起您的會議於 __app__ desktop 桌面應用程式。請再試一次,或是發起會議於 __app__ 網路應用程式。",
"tryAgainButton": "在桌面上再試一次",
"launchWebButton": "在網路上發起",
"appNotInstalled": "在您的手機上需要 __app__ 行動應用程式去加入這場會議。",
"downloadApp": "下載應用 APP",
"openApp": "繼續前往此應用程式"
}
}

View File

@@ -43,8 +43,7 @@
"mute": "Mute or unmute your microphone",
"fullScreen": "View or exit full screen",
"videoMute": "Start or stop your camera",
"showSpeakerStats": "Show speaker stats",
"localRecording": "Show or hide local recording controls"
"showSpeakerStats": "Show speaker stats"
},
"welcomepage":{
"accessibilityLabel": {
@@ -57,13 +56,10 @@
"video": "Video"
},
"calendar": "Calendar",
"connectCalendarText": "Connect your calendar to view all your meetings in __app__. Plus, add __app__ meetings to your calendar and start them with one click.",
"connectCalendarButton": "Connect your calendar",
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
"recentList": "Recent",
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
"recentList": "History",
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
@@ -91,7 +87,6 @@
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
"invite": "Invite people",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle room lock",
"moreActions": "Toggle more actions menu",
"moreActionsMenu": "More actions menu",
@@ -107,7 +102,6 @@
"shortcuts": "Toggle shortcuts",
"speakerStats": "Toggle speaker statistics",
"toggleCamera": "Toggle camera",
"tileView": "Toggle tile view",
"videomute": "Toggle mute video"
},
"addPeople": "Add people to your call",
@@ -150,7 +144,6 @@
"raiseHand": "Raise / Lower your hand",
"shortcuts": "View shortcuts",
"speakerStats": "Speaker stats",
"tileViewToggle": "Toggle tile view",
"invite": "Invite people"
},
"chat":{
@@ -160,14 +153,8 @@
},
"messagebox": "Enter text..."
},
"settings": {
"calendar": {
"about": "The __appName__ calendar integration is used to securely access your calendar so it can read upcoming events.",
"disconnect": "Disconnect",
"microsoftSignIn": "Sign in with Microsoft",
"signedIn": "Currently accessing calendar events for __email__. Click the Disconnect button below to stop accessing calendar events.",
"title": "Calendar"
},
"settings":
{
"title": "Settings",
"update": "Update",
"name": "Name",
@@ -211,7 +198,6 @@
"packetloss": "Packet loss:",
"resolution": "Resolution:",
"framerate": "Frame rate:",
"e2e_rtt": "E2E RTT:",
"less": "Show less",
"more": "Show more",
"address": "Address:",
@@ -262,7 +248,6 @@
"allow": "Allow",
"confirm": "Confirm",
"kickMessage": "Ouch! You have been kicked out of the meet!",
"kickTitle": "Kicked from meeting",
"popupErrorTitle": "Pop-up blocked",
"popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
"passwordErrorTitle": "Password Error",
@@ -428,11 +413,6 @@
],
"and": "and"
},
"share":
{
"mainText": "Click the following link to join the meeting:\n__roomUrl__",
"dialInfoText": "\n\n=====\n\nJust want to dial in on your phone?\n\n__defaultDialInNumber__Click this link to see the dial in phone numbers for this meeting\n__dialInfoPageUrl__"
},
"connection":
{
"ERROR": "Error",
@@ -465,13 +445,7 @@
"on": "Recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"authDropboxText": "Upload your recording to Dropbox.",
"authDropboxCompletedText": "Your recording file will appear in your Dropbox shortly after the recording has finished.",
"serviceName": "Recording service",
"signOut": "Sign Out",
"signIn": "sign in",
"loggedIn": "Logged in as __userName__",
"availableSpace": "Available space: __spaceLeft__ MB (approximately __duration__ minutes of recording)",
"startRecordingBody": "Are you sure you would like to start recording?",
"unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
@@ -484,9 +458,7 @@
"failedToStart": "Transcribing failed to start",
"tr": "TR",
"labelToolTip": "The meeting is being transcribed",
"ccButtonTooltip": "Start / Stop showing subtitles",
"start": "Start showing subtitles",
"stop": "Stop showing subtitles"
"ccButtonTooltip": "Start / Stop showing subtitles"
},
"liveStreaming":
{
@@ -505,9 +477,7 @@
"on": "Live Streaming",
"pending": "Starting Live Stream...",
"serviceName": "Live Streaming service",
"signedInAs": "You are currently signed in as:",
"signIn": "Sign in with Google",
"signOut": "Sign out",
"signInCTA": "Sign in or enter your live stream key from YouTube.",
"start": "Start a live stream",
"streamIdHelp": "What's this?",
@@ -647,21 +617,16 @@
"startWithVideoMuted": "Start with video muted"
},
"calendarSync": {
"addMeetingURL": "Add a meeting link",
"confirmAddLink": "Do you want to add a Jitsi link to this event?",
"confirmAddLinkTitle": "Calendar",
"join": "Join",
"joinTooltip": "Join the meeting",
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"noEvents": "There are no upcoming events scheduled.",
"now": "Now",
"ongoingMeeting": "ongoing meeting",
"permissionButton": "Open settings",
"permissionMessage": "The Calendar permission is required to see your meetings in the app.",
"refresh": "Refresh calendar",
"today": "Today"
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
},
"recentList": {
"joinPastMeeting": "Join a past meeting"
"joinPastMeeting": "Join A Past Meeting"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
@@ -700,34 +665,5 @@
"decline": "Dismiss",
"productLabel": "from Jitsi Meet",
"videoCallTitle": "Incoming video call"
},
"localRecording": {
"localRecording": "Local Recording",
"dialogTitle": "Local Recording Controls",
"start": "Start Recording",
"stop": "Stop Recording",
"moderator": "Moderator",
"me": "Me",
"duration": "Duration",
"durationNA": "N/A",
"encoding": "Encoding",
"participantStats": "Participant Stats",
"participant": "Participant",
"sessionToken": "Session Token",
"clientState": {
"on": "On",
"off": "Off",
"unknown": "Unknown"
},
"messages": {
"engaged": "Local recording engaged.",
"finished": "Recording session __token__ finished. Please send the recorded file to the moderator.",
"finishedModerator": "Recording session __token__ finished. The recording of the local track has been saved. Please ask the other participants to submit their recordings.",
"notModerator": "You are not the moderator. You cannot start or stop local recording."
},
"yes": "Yes",
"no": "No",
"label": "LOR",
"labelToolTip": "Local recording is engaged"
}
}

View File

@@ -21,7 +21,6 @@ import {
getPinnedParticipant,
pinParticipant
} from '../react/features/base/participants';
import { setTileView } from '../react/features/video-layout';
import UIEvents from '../service/UI/UIEvents';
import VideoLayout from './UI/videolayout/VideoLayout';
@@ -118,31 +117,6 @@ class State {
}
}
/**
* A getter for this object instance to know the state of tile view.
*
* @returns {boolean} True if tile view is enabled.
*/
get tileViewEnabled() {
return this._tileViewEnabled;
}
/**
* A setter for {@link tileViewEnabled}. Fires a property change event for
* other participants to follow.
*
* @param {boolean} b - Whether or not tile view is enabled.
* @returns {void}
*/
set tileViewEnabled(b) {
const oldValue = this._tileViewEnabled;
if (oldValue !== b) {
this._tileViewEnabled = b;
this._firePropertyChange('tileViewEnabled', oldValue, b);
}
}
/**
* Invokes {_propertyChangeCallback} to notify it that {property} had its
* value changed from {oldValue} to {newValue}.
@@ -215,10 +189,6 @@ class FollowMe {
this._sharedDocumentToggled
.bind(this, this._UI.getSharedDocumentManager().isVisible());
}
this._tileViewToggled.bind(
this,
APP.store.getState()['features/video-layout'].tileViewEnabled);
}
/**
@@ -244,10 +214,6 @@ class FollowMe {
this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
this.sharedDocEventHandler);
this.tileViewEventHandler = this._tileViewToggled.bind(this);
this._UI.addListener(UIEvents.TOGGLED_TILE_VIEW,
this.tileViewEventHandler);
}
/**
@@ -261,8 +227,6 @@ class FollowMe {
this.sharedDocEventHandler);
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
this.pinnedEndpointEventHandler);
this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
this.tileViewEventHandler);
}
/**
@@ -302,18 +266,6 @@ class FollowMe {
this._local.sharedDocumentVisible = sharedDocumentVisible;
}
/**
* Notifies this instance that the tile view mode has been enabled or
* disabled.
*
* @param {boolean} enabled - True if tile view has been enabled, false
* if has been disabled.
* @returns {void}
*/
_tileViewToggled(enabled) {
this._local.tileViewEnabled = enabled;
}
/**
* Changes the nextOnStage property value.
*
@@ -364,8 +316,7 @@ class FollowMe {
attributes: {
filmstripVisible: local.filmstripVisible,
nextOnStage: local.nextOnStage,
sharedDocumentVisible: local.sharedDocumentVisible,
tileViewEnabled: local.tileViewEnabled
sharedDocumentVisible: local.sharedDocumentVisible
}
});
}
@@ -404,7 +355,6 @@ class FollowMe {
this._onFilmstripVisible(attributes.filmstripVisible);
this._onNextOnStage(attributes.nextOnStage);
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
this._onTileViewEnabled(attributes.tileViewEnabled);
}
/**
@@ -484,21 +434,6 @@ class FollowMe {
}
}
/**
* Process a tile view enabled / disabled event received from FOLLOW-ME.
*
* @param {boolean} enabled - Whether or not tile view should be shown.
* @private
* @returns {void}
*/
_onTileViewEnabled(enabled) {
if (typeof enabled === 'undefined') {
return;
}
APP.store.dispatch(setTileView(enabled === 'true'));
}
/**
* Pins / unpins the video thumbnail given by clickId.
*

View File

@@ -157,7 +157,7 @@ UI.notifyKicked = function() {
messageHandler.showError({
hideErrorSupportLink: true,
descriptionKey: 'dialog.kickMessage',
titleKey: 'dialog.kickTitle'
titleKey: 'dialog.sessTerminated'
});
};

View File

@@ -1,5 +1,4 @@
/* global $ */
import SmallVideo from '../videolayout/SmallVideo';
const logger = require('jitsi-meet-logger').getLogger(__filename);

View File

@@ -1,13 +1,6 @@
/* global $, APP, interfaceConfig */
import { setFilmstripVisible } from '../../../react/features/filmstrip';
import {
LAYOUTS,
getCurrentLayout,
getMaxColumnCount,
getTileViewGridDimensions,
shouldDisplayTileView
} from '../../../react/features/video-layout';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
@@ -240,10 +233,6 @@ const Filmstrip = {
* @returns {*|{localVideo, remoteVideo}}
*/
calculateThumbnailSize() {
if (shouldDisplayTileView(APP.store.getState())) {
return this._calculateThumbnailSizeForTileView();
}
const availableSizes = this.calculateAvailableSize();
const width = availableSizes.availableWidth;
const height = availableSizes.availableHeight;
@@ -258,10 +247,11 @@ const Filmstrip = {
* @returns {{availableWidth: number, availableHeight: number}}
*/
calculateAvailableSize() {
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const isHorizontalFilmstripView
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
const thumbs = this.getThumbs(true);
const numvids = thumbs.remoteThumbs.length;
const localVideoContainer = $('#localVideoContainer');
/**
* If the videoAreaAvailableWidth is set we use this one to calculate
@@ -278,15 +268,10 @@ const Filmstrip = {
- UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
- 5;
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let availableWidth = videoAreaAvailableWidth;
const thumbs = this.getThumbs(true);
// If local thumb is not hidden
if (thumbs.localThumb) {
const localVideoContainer = $('#localVideoContainer');
availableWidth = Math.floor(
videoAreaAvailableWidth - (
UIUtil.parseCssInt(
@@ -304,12 +289,10 @@ const Filmstrip = {
);
}
// If the number of videos is 0 or undefined or we're not in horizontal
// If the number of videos is 0 or undefined or we're in vertical
// filmstrip mode we don't need to calculate further any adjustments
// to width based on the number of videos present.
const numvids = thumbs.remoteThumbs.length;
if (numvids && isHorizontalFilmstripView) {
if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
availableWidth = Math.floor(
@@ -339,10 +322,8 @@ const Filmstrip = {
availableHeight
= Math.min(maxHeight, window.innerHeight - 18);
return {
availableHeight,
availableWidth
};
return { availableWidth,
availableHeight };
},
/**
@@ -453,51 +434,6 @@ const Filmstrip = {
};
},
/**
* Calculates the size for thumbnails when in tile view layout.
*
* @returns {{localVideo, remoteVideo}}
*/
_calculateThumbnailSizeForTileView() {
const tileAspectRatio = 16 / 9;
// The distance from the top and bottom of the screen, as set by CSS, to
// avoid overlapping UI elements.
const topBottomPadding = 200;
// Minimum space to keep between the sides of the tiles and the sides
// of the window.
const sideMargins = 30 * 2;
const state = APP.store.getState();
const viewWidth = document.body.clientWidth - sideMargins;
const viewHeight = document.body.clientHeight - topBottomPadding;
const {
columns,
visibleRows
} = getTileViewGridDimensions(state, getMaxColumnCount());
const initialWidth = viewWidth / columns;
const aspectRatioHeight = initialWidth / tileAspectRatio;
const heightOfEach = Math.min(
aspectRatioHeight,
viewHeight / visibleRows);
const widthOfEach = tileAspectRatio * heightOfEach;
return {
localVideo: {
thumbWidth: widthOfEach,
thumbHeight: heightOfEach
},
remoteVideo: {
thumbWidth: widthOfEach,
thumbHeight: heightOfEach
}
};
},
/**
* Resizes thumbnails
* @param local
@@ -507,28 +443,6 @@ const Filmstrip = {
*/
// eslint-disable-next-line max-params
resizeThumbnails(local, remote, forceUpdate = false) {
const state = APP.store.getState();
if (shouldDisplayTileView(state)) {
// The size of the side margins for each tile as set in CSS.
const sideMargins = 10 * 2;
const {
columns,
rows
} = getTileViewGridDimensions(state, getMaxColumnCount());
const hasOverflow = rows > columns;
// Width is set so that the flex layout can automatically wrap
// tiles onto new rows.
this.filmstripRemoteVideos.css({
width: (local.thumbWidth * columns) + (columns * sideMargins)
});
this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
} else {
this.filmstripRemoteVideos.css('width', '');
}
const thumbs = this.getThumbs(!forceUpdate);
if (thumbs.localThumb) {
@@ -552,15 +466,13 @@ const Filmstrip = {
});
}
const currentLayout = getCurrentLayout(APP.store.getState());
// Let CSS take care of height in vertical filmstrip mode.
if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
if (interfaceConfig.VERTICAL_FILMSTRIP) {
$('#filmstripLocalVideo').css({
// adds 4 px because of small video 2px border
width: `${local.thumbWidth + 4}px`
});
} else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
} else {
this.filmstrip.css({
// adds 4 px because of small video 2px border
height: `${remote.thumbHeight + 4}px`

View File

@@ -11,7 +11,6 @@ import {
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { updateSettings } from '../../../react/features/base/settings';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -27,7 +26,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
this.$container = $(this.container);
this.updateDOMLocation();
$('#filmstripLocalVideoThumbnail').append(this.container);
this.localVideoId = null;
this.bindHoverHandler();
@@ -110,7 +109,16 @@ LocalVideo.prototype.changeVideo = function(stream) {
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
const localVideoContainer = document.getElementById('localVideoWrapper');
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = { this.localVideoId }
videoTrack = {{ jitsiTrack: stream }} />
</Provider>,
localVideoContainer
);
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
@@ -120,14 +128,12 @@ LocalVideo.prototype.changeVideo = function(stream) {
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
if (this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
@@ -229,29 +235,6 @@ LocalVideo.prototype._enableDisableContextMenu = function(enable) {
}
};
/**
* Places the {@code LocalVideo} in the DOM based on the current video layout.
*
* @returns {void}
*/
LocalVideo.prototype.updateDOMLocation = function() {
if (!this.container) {
return;
}
if (this.container.parentElement) {
this.container.parentElement.removeChild(this.container);
}
const appendTarget = shouldDisplayTileView(APP.store.getState())
? document.getElementById('localVideoTileViewContainer')
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
};
/**
* Callback invoked when the thumbnail is clicked. Will directly call
* VideoLayout to handle thumbnail click if certain elements have not been
@@ -286,28 +269,4 @@ LocalVideo.prototype._onContainerClick = function(event) {
}
};
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
LocalVideo.prototype._updateVideoElement = function() {
const localVideoContainer = document.getElementById('localVideoWrapper');
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = {{ jitsiTrack: this.videoStream }} />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
const video = this.container.querySelector('video');
video && video.play();
};
export default LocalVideo;

View File

@@ -20,11 +20,6 @@ import {
REMOTE_CONTROL_MENU_STATES,
RemoteVideoMenuTriggerButton
} from '../../../react/features/remote-video-menu';
import {
LAYOUTS,
getCurrentLayout,
shouldDisplayTileView
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -168,17 +163,8 @@ RemoteVideo.prototype._generatePopupContent = function() {
const onVolumeChange = this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;
const currentLayout = getCurrentLayout(APP.store.getState());
let remoteMenuPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
remoteMenuPosition = 'left top';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
remoteMenuPosition = 'left bottom';
} else {
remoteMenuPosition = 'top center';
}
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
? 'left bottom' : 'top center';
ReactDOM.render(
<Provider store = { APP.store }>
@@ -188,7 +174,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
initialVolumeValue = { initialVolumeValue }
isAudioMuted = { this.isAudioMuted }
isModerator = { isModerator }
menuPosition = { remoteMenuPosition }
menuPosition = { menuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }

View File

@@ -27,12 +27,6 @@ import {
RaisedHandIndicator,
VideoMutedIndicator
} from '../../../react/features/filmstrip';
import {
LAYOUTS,
getCurrentLayout,
setTileView,
shouldDisplayTileView
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -334,21 +328,7 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
SmallVideo.prototype.updateStatusBar = function() {
const statusBarContainer
= this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
}
const currentLayout = getCurrentLayout(APP.store.getState());
let tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
tooltipPosition = 'left';
} else {
tooltipPosition = 'top';
}
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
ReactDOM.render(
<I18nextProvider i18n = { i18next }>
@@ -567,8 +547,7 @@ SmallVideo.prototype.isVideoPlayable = function() {
*/
SmallVideo.prototype.selectDisplayMode = function() {
// Display name is always and only displayed when user is on the stage
if (this.isCurrentlyOnLargeVideo()
&& !shouldDisplayTileView(APP.store.getState())) {
if (this.isCurrentlyOnLargeVideo()) {
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (this.isVideoPlayable()
@@ -706,10 +685,7 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
};
/**
@@ -789,18 +765,6 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
}
};
/**
* Helper function for re-rendering multiple react components of the small
* video.
*
* @returns {void}
*/
SmallVideo.prototype.rerender = function() {
this.updateIndicators();
this.updateStatusBar();
this.updateView();
};
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
@@ -820,19 +784,7 @@ SmallVideo.prototype.updateIndicators = function() {
const iconSize = UIUtil.getIndicatorFontSize();
const showConnectionIndicator = this.videoIsHovered
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const currentLayout = getCurrentLayout(APP.store.getState());
let statsPopoverPosition, tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'left';
} else {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'top';
}
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
ReactDOM.render(
<I18nextProvider i18n = { i18next }>
@@ -847,7 +799,7 @@ SmallVideo.prototype.updateIndicators = function() {
enableStatsDisplay
= { !interfaceConfig.filmStripOnly }
statsPopoverPosition
= { statsPopoverPosition }
= { this.statsPopoverLocation }
userID = { this.id } />
: null }
{ this._showRaisedHand

View File

@@ -216,6 +216,8 @@ export class VideoContainer extends LargeContainer {
this.emitter = emitter;
this.resizeContainer = resizeContainer;
this.isVisible = false;
/**
* Whether the background should fit the height of the container
* (portrait) or fit the width of the container (landscape).
@@ -601,11 +603,17 @@ export class VideoContainer extends LargeContainer {
* TODO: refactor this since Temasys is no longer supported.
*/
show() {
// its already visible
if (this.isVisible) {
return Promise.resolve();
}
return new Promise(resolve => {
this.$wrapperParent.css('visibility', 'visible').fadeTo(
FADE_DURATION_MS,
1,
() => {
this.isVisible = true;
resolve();
}
);
@@ -620,9 +628,15 @@ export class VideoContainer extends LargeContainer {
// hide its avatar
this.showAvatar(false);
// its already hidden
if (!this.isVisible) {
return Promise.resolve();
}
return new Promise(resolve => {
this.$wrapperParent.fadeTo(FADE_DURATION_MS, 0, () => {
this.$wrapperParent.css('visibility', 'hidden');
this.isVisible = false;
resolve();
});
});

View File

@@ -1,10 +1,6 @@
/* global APP, $, interfaceConfig */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import {
getNearestReceiverVideoQualityLevel,
setMaxReceiverVideoQuality
} from '../../../react/features/base/conference';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
@@ -13,9 +9,6 @@ import {
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import {
shouldDisplayTileView
} from '../../../react/features/video-layout';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
@@ -601,19 +594,12 @@ const VideoLayout = {
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (shouldDisplayTileView(APP.store.getState())) {
const height
= (localVideo && localVideo.thumbHeight)
|| (remoteVideo && remoteVideo.thumbnHeight)
|| 0;
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
}
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
return { localVideo,
remoteVideo };
},
/**
@@ -1156,22 +1142,6 @@ const VideoLayout = {
);
},
/**
* Helper method to invoke when the video layout has changed and elements
* have to be re-arranged and resized.
*
* @returns {void}
*/
refreshLayout() {
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
VideoLayout.resizeVideoArea();
localVideoThumbnail && localVideoThumbnail.rerender();
Object.values(remoteVideos).forEach(
remoteVideo => remoteVideo.rerender()
);
},
/**
* Triggers an update of large video if the passed in participant is
* currently displayed on large video.

View File

@@ -1,63 +0,0 @@
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Manages a queue of functions where the current function in progress will
* automatically execute the next queued function.
*/
export class TaskQueue {
/**
* Creates a new instance of {@link TaskQueue} and sets initial instance
* variable values.
*/
constructor() {
this._queue = [];
this._currentTask = null;
this._onTaskComplete = this._onTaskComplete.bind(this);
}
/**
* Adds a new function to the queue. It will be immediately invoked if no
* other functions are queued.
*
* @param {Function} taskFunction - The function to be queued for execution.
* @private
* @returns {void}
*/
enqueue(taskFunction) {
this._queue.push(taskFunction);
this._executeNext();
}
/**
* If no queued task is currently executing, invokes the first task in the
* queue if any.
*
* @private
* @returns {void}
*/
_executeNext() {
if (this._currentTask) {
logger.warn('Task queued while a task is in progress.');
return;
}
this._currentTask = this._queue.shift() || null;
if (this._currentTask) {
this._currentTask(this._onTaskComplete);
}
}
/**
* Prepares to invoke the next function in the queue.
*
* @private
* @returns {void}
*/
_onTaskComplete() {
this._currentTask = null;
this._executeNext();
}
}

View File

@@ -1,5 +1,3 @@
import { TaskQueue } from './TaskQueue';
/**
* Create deferred object.
*
@@ -15,12 +13,3 @@ export function createDeferred() {
return deferred;
}
/**
* Returns an instance of {@link TaskQueue}.
*
* @returns {Object}
*/
export function createTaskQueue() {
return new TaskQueue();
}

1514
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,28 +15,27 @@
"author": "",
"readmeFilename": "README.md",
"dependencies": {
"@atlaskit/avatar": "14.0.10",
"@atlaskit/button": "9.0.8",
"@atlaskit/checkbox": "4.0.6",
"@atlaskit/avatar": "8.0.5",
"@atlaskit/button": "5.4.2",
"@atlaskit/checkbox": "2.0.2",
"@atlaskit/dropdown-menu": "3.10.2",
"@atlaskit/field-text": "7.0.10",
"@atlaskit/field-text-area": "4.0.9",
"@atlaskit/droplist": "4.11.1",
"@atlaskit/field-text": "4.0.1",
"@atlaskit/field-text-area": "1.2.0",
"@atlaskit/flag": "6.1.0",
"@atlaskit/icon": "13.8.1",
"@atlaskit/icon": "10.0.0",
"@atlaskit/inline-dialog": "5.3.0",
"@atlaskit/inline-message": "7.0.4",
"@atlaskit/layer-manager": "5.0.12",
"@atlaskit/lozenge": "6.2.0",
"@atlaskit/modal-dialog": "6.0.12",
"@atlaskit/multi-select": "11.0.6",
"@atlaskit/spinner": "9.0.8",
"@atlaskit/inline-message": "4.0.0",
"@atlaskit/layer-manager": "2.8.0",
"@atlaskit/lozenge": "3.4.2",
"@atlaskit/modal-dialog": "3.4.0",
"@atlaskit/multi-select": "7.1.3",
"@atlaskit/spinner": "4.0.0",
"@atlaskit/tabs": "4.0.1",
"@atlaskit/theme": "6.0.2",
"@atlaskit/tooltip": "12.0.14",
"@microsoft/microsoft-graph-client": "1.1.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "9.1.1",
"@webcomponents/url": "0.7.1",
"autosize": "1.18.13",
"dropbox": "4.0.9",
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",
"i18next-xhr-backend": "1.4.2",
@@ -47,10 +46,8 @@
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jsc-android": "224109.1.0",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#3f46c64f4f373d3b573fcc55b59568dbe9b9d51f",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"lodash": "4.17.4",
"moment": "2.19.4",
"moment-duration-format": "2.2.2",
@@ -61,18 +58,18 @@
"react-i18next": "4.8.0",
"react-native": "0.55.4",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
"react-native-callstats": "3.53.4",
"react-native-fast-image": "github:jitsi/react-native-fast-image#1f8c93a5584869848d75cc9b946beb9688efe285",
"react-native-google-signin": "1.0.0-rc3",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
"react-native-callstats": "3.52.0",
"react-native-fast-image": "4.0.14",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-linear-gradient": "2.4.0",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"react-native-permissions": "1.1.1",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#be3de15bb988cfabbb62cd4b3b06f4c920ee5ba0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"react-redux": "5.0.7",
"redux": "4.0.0",
"redux-thunk": "2.2.0",
@@ -90,7 +87,7 @@
"clean-css": "3.4.25",
"css-loader": "0.28.7",
"eslint": "4.12.1",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#v0.1.0",
"eslint-plugin-flowtype": "2.39.1",
"eslint-plugin-import": "2.8.0",
"eslint-plugin-jsdoc": "3.2.0",

View File

@@ -113,95 +113,6 @@ export function createConnectionEvent(action, attributes = {}) {
};
}
/**
* Creates an event which indicates an action occurred in the calendar
* integration UI.
*
* @param {string} eventName - The name of the calendar UI event.
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createCalendarClickedEvent(eventName, attributes = {}) {
return {
action: 'clicked',
actionSubject: eventName,
attributes,
source: 'calendar',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that the calendar container is shown and
* selected.
*
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createCalendarSelectedEvent(attributes = {}) {
return {
action: 'selected',
actionSubject: 'calendar.selected',
attributes,
source: 'calendar',
type: TYPE_UI
};
}
/**
* Creates an event indicating that a calendar has been connected.
*
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createCalendarConnectedEvent(attributes = {}) {
return {
action: 'calendar.connected',
actionSubject: 'calendar.connected',
attributes
};
}
/**
* Creates an event which indicates an action occurred in the recent list
* integration UI.
*
* @param {string} eventName - The name of the recent list UI event.
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRecentClickedEvent(eventName, attributes = {}) {
return {
action: 'clicked',
actionSubject: eventName,
attributes,
source: 'recent.list',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that the recent list container is shown and
* selected.
*
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRecentSelectedEvent(attributes = {}) {
return {
action: 'selected',
actionSubject: 'recent.list.selected',
attributes,
source: 'recent.list',
type: TYPE_UI
};
}
/**
* Creates an event for an action on the deep linking page.
*

View File

@@ -124,8 +124,6 @@ function _appNavigateToOptionalLocation(
// FIXME Turn location's host, hostname, and port properties into
// setters in order to reduce the risks of inconsistent state.
location.hostname = defaultLocation.hostname;
location.pathname
= defaultLocation.pathname + location.pathname.substr(1);
location.port = defaultLocation.port;
location.protocol = defaultLocation.protocol;
} else {

View File

@@ -5,7 +5,6 @@ import { Linking } from 'react-native';
import '../../analytics';
import '../../authentication';
import { DialogContainer } from '../../base/dialog';
import '../../base/jwt';
import { Platform } from '../../base/react';
import {
@@ -181,17 +180,6 @@ export class App extends AbstractApp {
_onLinkingURL({ url }) {
super._openURL(url);
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer() {
return (
<DialogContainer />
);
}
}
/**

View File

@@ -3,7 +3,6 @@
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
import { DialogContainer } from '../../base/dialog';
import '../../base/responsive-ui';
import '../../chat';
import '../../room-lock';
@@ -40,17 +39,4 @@ export class App extends AbstractApp {
</AtlasKitThemeProvider>
);
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer() {
return (
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
);
}
}

View File

@@ -127,7 +127,6 @@ export default class BaseApp extends Component<*, State> {
{ this._createMainElement(component) }
<SoundCollection />
{ this._createExtraElement() }
{ this._renderDialogContainer() }
</Fragment>
</Provider>
</I18nextProvider>
@@ -236,11 +235,4 @@ export default class BaseApp extends Component<*, State> {
this.setState({ route }, resolve);
});
}
/**
* Renders the platform specific dialog container.
*
* @returns {React$Element}
*/
_renderDialogContainer: () => React$Element<*>
}

View File

@@ -13,11 +13,12 @@ import {
MAX_DISPLAY_NAME_LENGTH,
dominantSpeakerChanged,
participantConnectionStatusChanged,
participantJoined,
participantLeft,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
} from '../participants';
import { endpointMessageReceived } from '../../subtitles';
import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
import { getJitsiMeetGlobalNS } from '../util';
@@ -51,8 +52,6 @@ import {
} from './constants';
import {
_addLocalTracksToConference,
commonUserJoinedHandling,
commonUserLeftHandling,
getCurrentConference,
sendLocalParticipant
} from './functions';
@@ -138,20 +137,24 @@ function _addConferenceListeners(conference, dispatch) {
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
id => dispatch(dominantSpeakerChanged(id, conference)));
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args) => dispatch(endpointMessageReceived(...args)));
conference.on(
JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
(...args) => dispatch(participantConnectionStatusChanged(...args)));
conference.on(
JitsiConferenceEvents.USER_JOINED,
(id, user) => commonUserJoinedHandling({ dispatch }, conference, user));
(id, user) => !user.isHidden() && dispatch(participantJoined({
botType: user.getBotType(),
conference,
id,
name: user.getDisplayName(),
presence: user.getStatus(),
role: user.getRole()
})));
conference.on(
JitsiConferenceEvents.USER_LEFT,
(id, user) => commonUserLeftHandling({ dispatch }, conference, user));
(id, user) => !user.isHidden()
&& dispatch(participantLeft(id, conference)));
conference.on(
JitsiConferenceEvents.USER_ROLE_CHANGED,
(...args) => dispatch(participantRoleChanged(...args)));

View File

@@ -1,21 +1,14 @@
// @flow
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import {
getLocalParticipant,
hiddenParticipantJoined,
hiddenParticipantLeft,
participantJoined,
participantLeft
} from '../participants';
import { getLocalParticipant } from '../participants';
import { toState } from '../redux';
import {
AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
JITSI_CONFERENCE_URL_KEY,
VIDEO_QUALITY_LEVELS
JITSI_CONFERENCE_URL_KEY
} from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -50,62 +43,6 @@ export function _addLocalTracksToConference(
return Promise.all(promises);
}
/**
* Logic shared between web and RN which processes the {@code USER_JOINED}
* conference event and dispatches either {@link participantJoined} or
* {@link hiddenParticipantJoined}.
*
* @param {Object} store - The redux store.
* @param {JitsiMeetConference} conference - The conference for which the
* {@code USER_JOINED} event is being processed.
* @param {JitsiParticipant} user - The user who has just joined.
* @returns {void}
*/
export function commonUserJoinedHandling(
{ dispatch }: Object,
conference: Object,
user: Object) {
const id = user.getId();
const displayName = user.getDisplayName();
if (user.isHidden()) {
dispatch(hiddenParticipantJoined(id, displayName));
} else {
dispatch(participantJoined({
botType: user.getBotType(),
conference,
id,
name: displayName,
presence: user.getStatus(),
role: user.getRole()
}));
}
}
/**
* Logic shared between web and RN which processes the {@code USER_LEFT}
* conference event and dispatches either {@link participantLeft} or
* {@link hiddenParticipantLeft}.
*
* @param {Object} store - The redux store.
* @param {JitsiMeetConference} conference - The conference for which the
* {@code USER_LEFT} event is being processed.
* @param {JitsiParticipant} user - The user who has just left.
* @returns {void}
*/
export function commonUserLeftHandling(
{ dispatch }: Object,
conference: Object,
user: Object) {
const id = user.getId();
if (user.isHidden()) {
dispatch(hiddenParticipantLeft(id));
} else {
dispatch(participantLeft(id, conference));
}
}
/**
* Evaluates a specific predicate for each {@link JitsiConference} known to the
* redux state features/base/conference while it returns {@code true}.
@@ -165,38 +102,6 @@ export function getCurrentConference(stateful: Function | Object) {
: joining);
}
/**
* Finds the nearest match for the passed in {@link availableHeight} to am
* enumerated value in {@code VIDEO_QUALITY_LEVELS}.
*
* @param {number} availableHeight - The height to which a matching video
* quality level should be found.
* @returns {number} The closest matching value from
* {@code VIDEO_QUALITY_LEVELS}.
*/
export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
const qualityLevels = [
VIDEO_QUALITY_LEVELS.HIGH,
VIDEO_QUALITY_LEVELS.STANDARD,
VIDEO_QUALITY_LEVELS.LOW
];
let selectedLevel = qualityLevels[0];
for (let i = 1; i < qualityLevels.length; i++) {
const previousValue = qualityLevels[i - 1];
const currentValue = qualityLevels[i];
const diffWithCurrent = Math.abs(availableHeight - currentValue);
const diffWithPrevious = Math.abs(availableHeight - previousValue);
if (diffWithCurrent < diffWithPrevious) {
selectedLevel = currentValue;
}
}
return selectedLevel;
}
/**
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
* manipulating a conference participant (e.g. pin or select participant).

View File

@@ -96,7 +96,6 @@ const WHITELISTED_KEYS = [
'disableRtx',
'disableSuspendVideo',
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',

View File

@@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
// fastest to merely disable them.
disableAudioLevels: true,
// FIXME flow complains about missing 'locationURL' missing in _setConfig
locationURL: undefined,
p2p: {
disableH264: false,
preferH264: true
@@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
const newState = _.merge(
{},
config,
{ error: undefined },
config, {
error: undefined,
locationURL: state.locationURL
},
// The config of _getInitialState() is meant to override the config
// downloaded from the Jitsi Meet deployment because the former contains

View File

@@ -9,7 +9,7 @@ import {
getCurrentConference
} from '../conference';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { parseURIString } from '../util';
import { parseStandardURIString } from '../util';
import {
CONNECTION_DISCONNECTED,
@@ -276,32 +276,18 @@ function _connectionWillConnect(connection) {
* {@code JitsiConnection}.
*/
function _constructOptions(state) {
// Deep clone the options to make sure we don't modify the object in the
// redux store.
const options = _.cloneDeep(state['features/base/config']);
const defaultOptions = state['features/base/connection'].options;
const options = _.merge(
{},
defaultOptions,
// Normalize the BOSH URL.
// Lib-jitsi-meet wants the config passed in multiple places and here is
// the latest one I have discovered.
state['features/base/config'],
);
let { bosh } = options;
if (bosh) {
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
const { locationURL } = state['features/base/connection'];
bosh = `${locationURL.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const { locationURL } = state['features/base/connection'];
const {
protocol,
hostname,
contextRoot
} = parseURIString(locationURL.href);
// eslint-disable-next-line max-len
bosh = `${protocol}//${hostname}${contextRoot || '/'}${bosh.substr(1)}`;
}
// Append room to the URL's search.
const { room } = state['features/base/conference'];
@@ -310,6 +296,16 @@ function _constructOptions(state) {
// not ignore case themselves.
room && (bosh += `?room=${room.toLowerCase()}`);
// XXX By default, config.js does not add a protocol to the BOSH URL.
// Which trips React Native. Make sure there is a protocol in order to
// satisfy React Native.
if (bosh !== defaultOptions.bosh
&& !parseStandardURIString(bosh).protocol) {
const { protocol } = parseStandardURIString(defaultOptions.bosh);
protocol && (bosh = protocol + bosh);
}
options.bosh = bosh;
}

View File

@@ -2,7 +2,8 @@
import { SET_ROOM } from '../conference';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import { assign, set, ReducerRegistry } from '../redux';
import { assign, ReducerRegistry } from '../redux';
import { parseURIString } from '../util';
import {
CONNECTION_DISCONNECTED,
@@ -152,6 +153,50 @@ function _connectionWillConnect(
});
}
/**
* Constructs options to be passed to the constructor of {@code JitsiConnection}
* based on a specific location URL.
*
* @param {string} locationURL - The location URL with which the returned
* options are to be constructed.
* @private
* @returns {Object} The options to be passed to the constructor of
* {@code JitsiConnection} based on the location URL.
*/
function _constructOptions(locationURL: URL) {
const locationURI = parseURIString(locationURL.href);
// FIXME The HTTPS scheme for the BOSH URL works with meet.jit.si on both
// mobile & Web. It also works with beta.meet.jit.si on Web. Unfortunately,
// it doesn't work with beta.meet.jit.si on mobile. Temporarily, use the
// HTTP scheme for the BOSH URL with beta.meet.jit.si on mobile.
let { protocol } = locationURI;
const domain = locationURI.hostname;
if (!protocol && domain === 'beta.meet.jit.si') {
const windowLocation = window.location;
windowLocation && (protocol = windowLocation.protocol);
protocol || (protocol = 'http:');
}
// Default to the HTTPS scheme for the BOSH URL.
protocol || (protocol = 'https:');
return {
bosh:
`${String(protocol)}//${domain}${
locationURI.contextRoot || '/'}http-bind`,
hosts: {
domain,
// Required by:
// - lib-jitsi-meet/modules/xmpp/xmpp.js
muc: `conference.${domain}`
}
};
}
/**
* The current (similar to getCurrentConference in base/conference/functions.js)
* connection which is {@code connection} or {@code connecting}.
@@ -178,7 +223,10 @@ function _getCurrentConnection(baseConnectionState: Object): ?Object {
function _setLocationURL(
state: Object,
{ locationURL }: { locationURL: ?URL }) {
return set(state, 'locationURL', locationURL);
return assign(state, {
locationURL,
options: locationURL ? _constructOptions(locationURL) : undefined
});
}
/**

View File

@@ -86,9 +86,10 @@ export default class AbstractDialog<P : Props, S : State>
* @returns {void}
*/
_onCancel() {
const { cancelDisabled = false, onCancel } = this.props;
const { cancelDisabled, onCancel } = this.props;
if (!cancelDisabled && (!onCancel || onCancel())) {
if ((typeof cancelDisabled === 'undefined' || !cancelDisabled)
&& (!onCancel || onCancel())) {
this._hide();
}
}
@@ -108,9 +109,9 @@ export default class AbstractDialog<P : Props, S : State>
* @returns {void}
*/
_onSubmit(value: ?string) {
const { okDisabled = false, onSubmit } = this.props;
const { okDisabled, onSubmit } = this.props;
if (!okDisabled) {
if (typeof okDisabled === 'undefined' || !okDisabled) {
this.setState({ submitting: true });
// Invoke the React Compnent prop onSubmit if any.

View File

@@ -113,7 +113,7 @@ class Dialog extends AbstractDialog<Props, State> {
[_TAG_KEY]: _SUBMIT_TEXT_TAG_VALUE
};
let el: ?React$Element<*> = (
let el: ?React$Element<*> = ( // eslint-disable-line no-extra-parens
<Prompt
cancelButtonTextStyle = { cancelButtonTextStyle }
cancelText = { t(cancelTitleKey) }

View File

@@ -22,12 +22,7 @@ export class DialogContainer extends Component {
/**
* The props to pass to the component that will be rendered.
*/
_componentProps: PropTypes.object,
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
_reducedUI: PropTypes.bool
_componentProps: PropTypes.object
};
/**
@@ -37,13 +32,10 @@ export class DialogContainer extends Component {
* @returns {ReactElement}
*/
render() {
const {
_component: component,
_reducedUI: reducedUI
} = this.props;
const { _component: component } = this.props;
return (
component && !reducedUI
component
? React.createElement(component, this.props._componentProps)
: null);
}
@@ -57,18 +49,15 @@ export class DialogContainer extends Component {
* @private
* @returns {{
* _component: React.Component,
* _componentProps: Object,
* _reducedUI: boolean
* _componentProps: Object
* }}
*/
function _mapStateToProps(state) {
const stateFeaturesBaseDialog = state['features/base/dialog'];
const { reducedUI } = state['features/base/responsive-ui'];
return {
_component: stateFeaturesBaseDialog.component,
_componentProps: stateFeaturesBaseDialog.componentProps,
_reducedUI: reducedUI
_componentProps: stateFeaturesBaseDialog.componentProps
};
}

View File

@@ -1,39 +0,0 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { dialog as styles } from './styles';
type Props = {
/**
* Children of the component.
*/
children: string | React$Node
};
/**
* Generic dialog content container to provide the same styling for all custom
* dialogs.
*/
export default class DialogContent extends Component<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { children } = this.props;
const childrenComponent = typeof children === 'string'
? <Text>{ children }</Text>
: children;
return (
<View style = { styles.dialogContainer }>
{ childrenComponent }
</View>
);
}
}

View File

@@ -212,7 +212,7 @@ class DialogWithTabs extends Component<Props, State> {
const { onSubmit, tabs } = this.props;
tabs.forEach(({ submit }, idx) => {
submit && submit(this.state.tabStates[idx]);
submit(this.state.tabStates[idx]);
});
onSubmit();

View File

@@ -1,9 +1,8 @@
// @flow
export { default as BottomSheet } from './BottomSheet';
export { default as Dialog } from './Dialog';
export { default as DialogContainer } from './DialogContainer';
export { default as DialogContent } from './DialogContent';
export { default as Dialog } from './Dialog';
export { default as StatelessDialog } from './StatelessDialog';
export { default as DialogWithTabs } from './DialogWithTabs';
export { default as AbstractDialogTab } from './AbstractDialogTab';

View File

@@ -1,6 +1,6 @@
import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette, createStyleSheet } from '../../styles';
import { ColorPalette, createStyleSheet } from '../../styles';
/**
* The React {@code Component} styles of {@code Dialog}.
@@ -13,14 +13,6 @@ export const dialog = createStyleSheet({
color: ColorPalette.blue
},
/**
* Unified container for a consistent Dialog style.
*/
dialogContainer: {
paddingHorizontal: BoxModel.padding,
paddingVertical: 1.5 * BoxModel.padding
},
/**
* The style of the {@code Text} in a {@code Dialog} button which is
* disabled.

View File

@@ -14,7 +14,6 @@ export const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
export const JitsiConnectionEvents = JitsiMeetJS.events.connection;
export const JitsiConnectionQualityEvents
= JitsiMeetJS.events.connectionQuality;
export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
export const JitsiParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus;

View File

@@ -73,6 +73,14 @@ export default function _RTCPeerConnection(...args: any[]) {
_RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype);
_RTCPeerConnection.prototype.constructor = _RTCPeerConnection;
_RTCPeerConnection.prototype.addIceCandidate
= _makePromiseAware(RTCPeerConnection.prototype.addIceCandidate, 1, 0);
_RTCPeerConnection.prototype.createAnswer
= _makePromiseAware(RTCPeerConnection.prototype.createAnswer, 0, 1);
_RTCPeerConnection.prototype.createOffer
= _makePromiseAware(RTCPeerConnection.prototype.createOffer, 0, 1);
_RTCPeerConnection.prototype._invokeOnaddstream = function(...args) {
const onaddstream = this._onaddstream;
@@ -96,14 +104,32 @@ _RTCPeerConnection.prototype._queueOnaddstream = function(...args) {
this._onaddstreamQueue.push(Array.from(args));
};
_RTCPeerConnection.prototype.setRemoteDescription = function(description) {
_RTCPeerConnection.prototype.setLocalDescription
= _makePromiseAware(RTCPeerConnection.prototype.setLocalDescription, 1, 0);
_RTCPeerConnection.prototype.setRemoteDescription = function(
sessionDescription,
successCallback,
errorCallback) {
// If the deprecated callback-based version is used, translate it to the
// Promise-based version.
if (typeof successCallback !== 'undefined'
|| typeof errorCallback !== 'undefined') {
// XXX Returning a Promise is not necessary. But I don't see why it'd
// hurt (much).
return (
_RTCPeerConnection.prototype.setRemoteDescription.call(
this,
sessionDescription)
.then(successCallback, errorCallback));
}
return (
_synthesizeIPv6Addresses(description)
_synthesizeIPv6Addresses(sessionDescription)
.catch(reason => {
reason && _LOGE(reason);
return description;
return sessionDescription;
})
.then(value => _setRemoteDescription.bind(this)(value)));
@@ -119,18 +145,61 @@ function _LOGE(...args) {
logger.error(...args);
}
/**
* Makes a {@code Promise}-returning function out of a specific void function
* with {@code successCallback} and {@code failureCallback}.
*
* @param {Function} f - The (void) function with {@code successCallback} and
* {@code failureCallback}.
* @param {number} beforeCallbacks - The number of arguments before
* {@code successCallback} and {@code failureCallback}.
* @param {number} afterCallbacks - The number of arguments after
* {@code successCallback} and {@code failureCallback}.
* @returns {Promise}
*/
function _makePromiseAware(
f: Function,
beforeCallbacks: number,
afterCallbacks: number) {
return function(...args) {
return new Promise((resolve, reject) => {
if (args.length <= beforeCallbacks + afterCallbacks) {
args.splice(beforeCallbacks, 0, resolve, reject);
}
let fPromise;
try {
// eslint-disable-next-line no-invalid-this
fPromise = f.apply(this, args);
} catch (e) {
reject(e);
}
// If the super implementation returns a Promise from the deprecated
// invocation by any chance, try to make sense of it.
if (fPromise) {
const { then } = fPromise;
typeof then === 'function'
&& then.call(fPromise, resolve, reject);
}
});
};
}
/**
* Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription}
* implementation which uses the deprecated, callback-based version to the
* {@code Promise}-based version.
*
* @param {RTCSessionDescription} description - The RTCSessionDescription
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* which specifies the configuration of the remote end of the connection.
* @private
* @private
* @returns {Promise}
*/
function _setRemoteDescription(description) {
function _setRemoteDescription(sessionDescription) {
return new Promise((resolve, reject) => {
/* eslint-disable no-invalid-this */
@@ -139,8 +208,10 @@ function _setRemoteDescription(description) {
// setRemoteDescription calls. I shouldn't be but... anyway.
this._onaddstreamQueue = [];
RTCPeerConnection.prototype.setRemoteDescription.call(this, description)
.then((...args) => {
RTCPeerConnection.prototype.setRemoteDescription.call(
this,
sessionDescription,
(...args) => {
let q;
try {
@@ -151,7 +222,8 @@ function _setRemoteDescription(description) {
}
this._invokeQueuedOnaddstream(q);
}, (...args) => {
},
(...args) => {
this._onaddstreamQueue = undefined;
reject(...args);

View File

@@ -126,6 +126,7 @@ function _visitNode(node, callback) {
//
// Required by:
// - jQuery
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
// - Strophe
if (typeof global.document === 'undefined') {
const document
@@ -150,6 +151,14 @@ function _visitNode(node, callback) {
document.cookie = '';
}
// document.implementation
//
// Required by:
// - jQuery
if (typeof document.implementation === 'undefined') {
document.implementation = {};
}
// document.implementation.createHTMLDocument
//
// Required by:
@@ -353,9 +362,26 @@ function _visitNode(node, callback) {
const { navigator } = global;
if (navigator) {
// platform
//
// Required by:
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
if (typeof navigator.platform === 'undefined') {
navigator.platform = '';
}
// plugins
//
// Required by:
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
if (typeof navigator.plugins === 'undefined') {
navigator.plugins = [];
}
// userAgent
//
// Required by:
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
// - lib-jitsi-meet/modules/browser/BrowserDetection.js
let userAgent = navigator.userAgent || '';

View File

@@ -3,15 +3,14 @@ import {
MediaStreamTrack,
RTCSessionDescription,
RTCIceCandidate,
mediaDevices,
permissions
getUserMedia
} from 'react-native-webrtc';
import RTCPeerConnection from './RTCPeerConnection';
(global => {
if (typeof global.MediaStream === 'undefined') {
global.MediaStream = MediaStream;
if (typeof global.webkitMediaStream === 'undefined') {
global.webkitMediaStream = MediaStream;
}
if (typeof global.MediaStreamTrack === 'undefined') {
global.MediaStreamTrack = MediaStreamTrack;
@@ -22,7 +21,7 @@ import RTCPeerConnection from './RTCPeerConnection';
if (typeof global.RTCPeerConnection === 'undefined') {
global.RTCPeerConnection = RTCPeerConnection;
}
if (typeof global.RTCPeerConnection === 'undefined') {
if (typeof global.webkitRTCPeerConnection === 'undefined') {
global.webkitRTCPeerConnection = RTCPeerConnection;
}
if (typeof global.RTCSessionDescription === 'undefined') {
@@ -32,11 +31,8 @@ import RTCPeerConnection from './RTCPeerConnection';
const navigator = global.navigator;
if (navigator) {
if (typeof navigator.mediaDevices === 'undefined') {
navigator.mediaDevices = mediaDevices;
}
if (typeof navigator.permissions === 'undefined') {
navigator.permissions = permissions;
if (typeof navigator.webkitGetUserMedia === 'undefined') {
navigator.webkitGetUserMedia = getUserMedia;
}
}

View File

@@ -93,7 +93,7 @@ export default class Video extends Component<*> {
? 'contain'
: (style && style.objectFit) || 'cover';
const rtcView
= (
= ( // eslint-disable-line no-extra-parens
<RTCView
mirror = { this.props.mirror }
objectFit = { objectFit }

View File

@@ -12,11 +12,6 @@ export type Item = {
*/
colorBase: string,
/**
* An optional react element to append to the end of the Item.
*/
elementAfter?: ?ComponentType<any>,
/**
* Item title
*/

View File

@@ -29,12 +29,6 @@ type Props = {
*/
onRefresh: Function,
/**
* Function to be invoked when a secondary action is performed on an item.
* The item's ID is passed.
*/
onSecondaryAction: Function,
/**
* Function to override the rendered default empty list component.
*/
@@ -93,7 +87,7 @@ class NavigateSectionList extends Component<Props> {
*/
render() {
const {
renderListEmptyComponent = this._renderListEmptyComponent(),
renderListEmptyComponent = this._renderListEmptyComponent,
sections
} = this.props;
@@ -134,13 +128,11 @@ class NavigateSectionList extends Component<Props> {
* @returns {Function}
*/
_onPress(url) {
const { disabled, onPress } = this.props;
return () => {
const { disabled, onPress } = this.props;
if (!disabled && url && typeof onPress === 'function') {
return () => onPress(url);
}
return null;
!disabled && url && typeof onPress === 'function' && onPress(url);
};
}
_onRefresh: () => void;
@@ -159,23 +151,6 @@ class NavigateSectionList extends Component<Props> {
}
}
_onSecondaryAction: Object => Function;
/**
* Returns a function that is used in the secondaryAction callback of the
* items.
*
* @param {string} id - The id of the item that secondary action was
* performed on.
* @private
* @returns {Function}
*/
_onSecondaryAction(id) {
return () => {
this.props.onSecondaryAction(id);
};
}
_renderItem: Object => Object;
/**
@@ -188,7 +163,7 @@ class NavigateSectionList extends Component<Props> {
*/
_renderItem(listItem, key: string = '') {
const { item } = listItem;
const { id, url } = item;
const { url } = item;
// XXX The value of title cannot be undefined; otherwise, react-native
// will throw a TypeError: Cannot read property of undefined. While it's
@@ -203,9 +178,7 @@ class NavigateSectionList extends Component<Props> {
<NavigateSectionListItem
item = { item }
key = { key }
onPress = { url ? this._onPress(url) : undefined }
secondaryAction = {
url ? undefined : this._onSecondaryAction(id) } />
onPress = { this._onPress(url) } />
);
}

View File

@@ -17,12 +17,7 @@ type Props = {
/**
* Function to be invoked when an Item is pressed. The Item's URL is passed.
*/
onPress: ?Function,
/**
* Function to be invoked when secondary action was performed on an Item.
*/
secondaryAction: ?Function
onPress: Function
}
/**
@@ -105,24 +100,6 @@ export default class NavigateSectionListItem extends Component<Props> {
return lines && lines.length ? lines.map(this._renderItemLine) : null;
}
/**
* Renders the secondary action label.
*
* @private
* @returns {React$Node}
*/
_renderSecondaryAction() {
const { secondaryAction } = this.props;
return (
<Container
onClick = { secondaryAction }
style = { styles.secondaryActionContainer }>
<Text style = { styles.secondaryActionLabel }>+</Text>
</Container>
);
}
/**
* Renders the content of this component.
*
@@ -158,7 +135,6 @@ export default class NavigateSectionListItem extends Component<Props> {
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.secondaryAction && this._renderSecondaryAction() }
</Container>
);
}

View File

@@ -11,7 +11,6 @@ const HEADER_COLOR = ColorPalette.blue;
// Header height is from Android guidelines. Also, this looks good.
const HEADER_HEIGHT = 56;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
const SECONDARY_ACTION_BUTTON_SIZE = 30;
export const HEADER_PADDING = BoxModel.padding;
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
@@ -267,21 +266,6 @@ const SECTION_LIST_STYLES = {
color: OVERLAY_FONT_COLOR
},
secondaryActionContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.blue,
borderRadius: 3,
height: SECONDARY_ACTION_BUTTON_SIZE,
justifyContent: 'center',
margin: BoxModel.margin * 0.5,
marginRight: BoxModel.margin,
width: SECONDARY_ACTION_BUTTON_SIZE
},
secondaryActionLabel: {
color: ColorPalette.white
},
touchableView: {
flexDirection: 'row'
}

View File

@@ -22,8 +22,11 @@ export default class Container extends AbstractContainer {
* @returns {ReactElement}
*/
render() {
const { visible = true } = this.props;
const { visible } = this.props;
return visible ? super._render('div') : null;
return (
typeof visible === 'undefined' || visible
? super._render('div')
: null);
}
}

View File

@@ -41,7 +41,7 @@ class InlineDialogFailure extends Component<*> {
const supportString = t('inlineDialogFailure.supportMsg');
const supportLinkElem
= supportLink
? (
? ( // eslint-disable-line no-extra-parens
<div className = 'inline-dialog-error-text'>
<span>{ supportString.padEnd(supportString.length + 1) }
</span>

View File

@@ -244,7 +244,7 @@ class MultiSelectAutocomplete extends Component {
if (!this.state.error) {
return null;
}
const content = (
const content = ( // eslint-disable-line no-extra-parens
<div className = 'autocomplete-error'>
<InlineDialogFailure
onRetry = { this._onRetry } />

View File

@@ -6,22 +6,18 @@ import Container from './Container';
import Text from './Text';
import type { Item } from '../../Types';
/**
* The type of the React {@code Component} props of
* {@link NavigateSectionListItem}.
*/
type Props = {
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: ?Function,
onPress: Function,
/**
* A item containing data to be rendered
*/
item: Item
};
}
/**
* Implements a React/Web {@link Component} for displaying an item in a
@@ -29,16 +25,14 @@ type Props = {
*
* @extends Component
*/
export default class NavigateSectionListItem<P: Props>
extends Component<P> {
export default class NavigateSectionListItem extends Component<Props> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { elementAfter, lines, title } = this.props.item;
const { lines, title } = this.props.item;
const { onPress } = this.props;
/**
@@ -58,30 +52,22 @@ export default class NavigateSectionListItem<P: Props>
duration = lines[1];
}
const rootClassName = `navigate-section-list-tile ${
onPress ? 'with-click-handler' : 'without-click-handler'}`;
return (
<Container
className = { rootClassName }
className = 'navigate-section-list-tile'
onClick = { onPress }>
<Container className = 'navigate-section-list-tile-info'>
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
</Container>
<Container className = { 'element-after' }>
{ elementAfter || null }
</Container>
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
</Container>
);
}

View File

@@ -7,11 +7,6 @@ import type { Section } from '../../Types';
type Props = {
/**
* Rendered when the list is empty. Should be a rendered element.
*/
ListEmptyComponent: Object,
/**
* Used to extract a unique key for a given item at the specified index.
* Key is used for caching and as the react key to track item re-ordering.
@@ -54,7 +49,6 @@ export default class SectionList extends Component<Props> {
*/
render() {
const {
ListEmptyComponent,
renderSectionHeader,
renderItem,
sections,
@@ -62,34 +56,34 @@ export default class SectionList extends Component<Props> {
} = this.props;
/**
* If there are no recent items we don't want to display anything
* If there are no recent items we dont want to display anything
*/
if (sections) {
return (
/* eslint-disable no-extra-parens */
<Container
className = 'navigate-section-list'>
{
sections.length === 0
? ListEmptyComponent
: sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
}
</Container>
/* eslint-enable no-extra-parens */
);
}

View File

@@ -105,7 +105,7 @@ class Watermarks extends Component<*, *> {
let reactElement = null;
if (this.state.showBrandWatermark) {
reactElement = (
reactElement = ( // eslint-disable-line no-extra-parens
<div
className = 'watermark rightwatermark'
style = { _RIGHT_WATERMARK_STYLE } />
@@ -114,7 +114,7 @@ class Watermarks extends Component<*, *> {
const { brandWatermarkLink } = this.state;
if (brandWatermarkLink) {
reactElement = (
reactElement = ( // eslint-disable-line no-extra-parens
<a
href = { brandWatermarkLink }
target = '_new'>
@@ -144,7 +144,7 @@ class Watermarks extends Component<*, *> {
const { jitsiWatermarkLink } = this.state;
if (jitsiWatermarkLink) {
reactElement = (
reactElement = ( // eslint-disable-line no-extra-parens
<a
href = { jitsiWatermarkLink }
target = '_new'>

View File

@@ -0,0 +1,14 @@
/**
* FIXME.
*
* {
* type: SET_SESSION,
* session: {
* url: {string},
* state: {string},
* ...data
* }
* }
* @public
*/
export const SET_SESSION = Symbol('SET_SESSION');

View File

@@ -0,0 +1,16 @@
import { SET_SESSION } from './actionTypes';
/**
* FIXME.
*
* @param {string} session - FIXME.
* @returns {{
* type: SET_SESSION
* }}
*/
export function setSession(session) {
return {
type: SET_SESSION,
session
};
}

View File

@@ -0,0 +1,12 @@
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
export const SESSION_ENDED = Symbol('SESSION_ENDED');
export const SESSION_FAILED = Symbol('SESSION_FAILED');
export const SESSION_STARTED = Symbol('SESSION_STARTED');
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');

View File

@@ -0,0 +1,36 @@
// @flow
import { toState } from '../redux';
import { toURLString } from '../util';
/**
* FIXME.
*
* @param {Function|Object} stateful - FIXME.
* @param {string} url - FIXME.
* @returns {*}
*/
export function getSession(stateful: Function | Object, url: string): ?Object {
const state = toState(stateful);
const session = state['features/base/session'].get(url);
if (!session) {
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
}
return session;
}
/**
* FIXME.
*
* @param {Function | Object} stateful - FIXME.
* @returns {Object}
*/
export function getCurrentSession(stateful: Function | Object): ?Object {
const state = toState(stateful);
const { locationURL } = state['features/base/config'];
return getSession(state, toURLString(locationURL));
}

View File

@@ -1,7 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './controller';
export * from './constants';
export * from './functions';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,349 @@
// @flow
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
isRoomValid
} from '../../base/conference';
import {
CONNECTION_DISCONNECTED,
CONNECTION_FAILED,
CONNECTION_WILL_CONNECT
} from '../../base/connection';
import {
MiddlewareRegistry,
toState
} from '../../base/redux';
import { parseURIString, toURLString } from '../../base/util';
import {
SESSION_CONFIGURED,
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SESSION_WILL_END,
SESSION_WILL_START
} from './constants';
import { setSession } from './actions';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../config';
import { getCurrentSession, getSession } from './functions';
/**
* Middleware that maintains conference sessions. The features spans across
* three features strictly related to the conference lifecycle.
* The first one is the base/config which configures the session. It's
* 'locationURL' state is used to tell what's the current conference URL the app
* is working with. The session starts as soon as {@link CONFIG_WILL_LOAD} event
* arrives. The {@code locationURL} instance is stored in the session to
* associate the load config request with the session and be able to distinguish
* between the current and outdated load config request failures. After the
* config is loaded the lifecycle moves to the base/connection feature which
* creates a {@code JitsiConnection} and tries to connect to the server. On
* {@code CONNECTION_WILL_CONNECT} the connection instance is stored in the
* session and used later to filter the events similar to what's done for
* the load config requests. The base/conference feature adds the last part to
* the session's lifecycle. A {@code JitsiConference} instance is stored in the
* session on the {@code CONFERENCE_WILL_JOIN} action. A session is considered
* alive as long as either connection or conference is available and
* operational.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(setSession({
url: session.url,
conference
}));
} else {
console.info(`IGNORED WILL_JOIN FOR: ${url}`);
}
break;
}
case CONFERENCE_JOINED: {
const { conference } = action;
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state === SESSION_CONFIGURED) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_STARTED
}));
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED CONF JOINED FOR: ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`);
}
break;
}
case CONFERENCE_LEFT:
case CONFERENCE_FAILED: {
const { conference, error } = action;
const session = findSessionForConference(store, conference);
// FIXME update comments
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (session) {
if (!error || isGameOver(store, session, error)) {
if (session.connection) {
store.dispatch(
setSession({
url: session.url,
conference: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED FAILED/LEFT for ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`, error);
}
break;
}
// NOTE WILL_JOIN is fired on SET_ROOM
// case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE: {
const { conference } = action;
const url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state && state !== SESSION_WILL_END) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_WILL_END
}));
} else {
console.info(`IGNORED WILL LEAVE FOR ${url}`);
}
break;
}
case CONNECTION_WILL_CONNECT: {
const { connection } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(
setSession({
url: session.url,
connection,
conference: undefined // Detach from the old conference
}));
} else {
console.info(`IGNORED CONNECTION_WILL_CONNECT FOR: ${url}`);
}
break;
}
case CONNECTION_DISCONNECTED:
case CONNECTION_FAILED: {
const { connection, error } = action;
const session = findSessionForConnection(store, connection);
if (session) {
// Remove connection from the session, but wait for
// the conference to be removed as well.
if (!error || isGameOver(store, session, error)) {
if (session.conference) {
store.dispatch(
setSession({
url: session.url,
connection: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
console.info('Ignored DISCONNECTED/FAILED for connection');
}
break;
}
case SET_CONFIG: {
// XXX SET_CONFIG IS ALWAYS RELEVANT
const { locationURL } = store.getState()['features/base/config'];
const url = toURLString(locationURL);
const session = getSession(store, url);
const state = session && session.state;
if (state === SESSION_WILL_START) {
store.dispatch(
setSession({
url,
state: SESSION_CONFIGURED
}));
}
break;
}
case CONFIG_WILL_LOAD: {
const { locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
// The back and forth to string conversion is here, because there's no
// guarantee that the locationURL will be the exact custom structure
// which contains the room property.
let { room } = parseURIString(url);
// Validate the room
room = isRoomValid(room) ? room : undefined;
if (room && !session) {
store.dispatch(
setSession({
url,
state: SESSION_WILL_START,
locationURL,
room
}));
} else if (room && session) {
// Update to the new locationURL instance
store.dispatch(
setSession({
url,
locationURL
}));
} else {
console.info(`IGNORED CFG WILL LOAD FOR ${url}`);
}
break;
}
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session && session.locationURL === locationURL) {
if (isGameOver(store, session, error)) {
store.dispatch(
setSession({
url,
state: SESSION_FAILED,
error
}));
}
} else {
console.info(`IGNORED LOAD_CONFIG_ERROR FOR: ${url}`);
}
break;
}
}
return result;
});
/**
* FIXME A session is to be terminated either when the recoverable flag is
* explicitly set to {@code false} or if the error arrives for a session which
* is no longer current (the app has started working with another session).
* This can happen when a conference which is being disconnected fails in which
* case the session needs to be ended even if the flag is not {@code false}
* because we know that there's no fatal error handling. This is kind of
* a contract between the fatal error feature and the session which probably
* indicates that the fatal error detection and handling should be incorporated
* into the session feature.
*
* @param {Object | Function} stateful - FIXME.
* @param {Object} session - FIXME.
* @param {Object} error - FIXME.
* @returns {boolean}
*/
function isGameOver(stateful, session, error) {
return getCurrentSession(stateful) !== session
|| error.recoverable === false;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConnection} connection - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConnection(stateful, connection) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.connection === connection) {
return session;
}
}
console.info('Session not found for a connection');
return undefined;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConference} conference - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConference(stateful, conference) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.conference === conference) {
return session;
}
}
console.info('Session not found for a conference');
return undefined;
}

View File

@@ -0,0 +1,71 @@
// @flow
import { assign, ReducerRegistry } from '../../base/redux';
import { getSymbolDescription } from '../util';
import { SET_SESSION } from './actionTypes';
import {
SESSION_FAILED,
SESSION_ENDED,
SESSION_WILL_START
} from './constants';
ReducerRegistry.register('features/base/session',
(state = new Map(), action) => {
switch (action.type) {
case SET_SESSION:
return _setSession(state, action);
}
return state;
});
/**
* FIXME.
*
* @param {Object} featureState - FIXME.
* @param {Object} action - FIXME.
* @returns {Map<any, any>} - FIXME.
* @private
*/
function _setSession(featureState, action) {
const { url, state, ...data } = action.session;
const session = featureState.get(url);
const nextState = new Map(featureState);
// Drop the whole action if the url is not defined
if (!url) {
console.error('SET SESSION - NO URL');
return nextState;
}
if (session) {
if (state === SESSION_ENDED || state === SESSION_FAILED) {
nextState.delete(url);
} else {
nextState.set(
url,
assign(session, {
url,
state: state ? state : session.state,
...data
}));
}
} else if (state === SESSION_WILL_START) {
nextState.set(
url, {
url,
state,
...data
});
}
console.info(
'SESSION STATE REDUCED: ',
new Map(nextState),
url,
state && getSymbolDescription(state),
action.session.error);
return nextState;
}

View File

@@ -23,8 +23,6 @@ export const ColorPalette = {
buttonUnderlay: '#495258',
darkGrey: '#555555',
green: '#40b183',
lightGrey: '#AAAAAA',
lighterGrey: '#EEEEEE',
red: '#D00000',
white: 'white',

View File

@@ -67,7 +67,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
// XXX TouchableHighlight requires 1 child. If there's a need to
// show both the icon and the label, then these two need to be
// wrapped in a View.
children = (
children = ( // eslint-disable-line no-extra-parens
<View style = { style }>
{ children }
<Text style = { styles && styles.labelStyle }>

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