Compare commits

..

1 Commits

Author SHA1 Message Date
paweldomas
bc54a87f2e feat(invite): send DTMF tones if specified as part of phone number text
If phone number to be invited into the conference ends with a comma then
the following part will be stored in redux state and sent later as DTMF
tones after Jigasi connects to the conference.
2019-10-03 14:37:16 -05:00
143 changed files with 2490 additions and 5018 deletions

View File

@@ -1,4 +1,4 @@
osx_image: xcode11.1
osx_image: xcode10.2
language: objective-c
script:
- "./ios/travis-ci/build-ipa.sh"

View File

@@ -3,7 +3,6 @@ CLEANCSS = ./node_modules/.bin/cleancss
DEPLOY_DIR = libs
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
RNNOISE_WASM_DIR = node_modules/rnnoise-wasm/dist/
NODE_SASS = ./node_modules/.bin/node-sass
NPM = npm
OUTPUT_DIR = .
@@ -21,7 +20,7 @@ compile:
clean:
rm -fr $(BUILD_DIR)
deploy: deploy-init deploy-appbundle deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
deploy-init:
rm -fr $(DEPLOY_DIR)
@@ -48,8 +47,6 @@ deploy-appbundle:
$(BUILD_DIR)/analytics-ga.min.map \
$(BUILD_DIR)/video-blur-effect.min.js \
$(BUILD_DIR)/video-blur-effect.min.map \
$(BUILD_DIR)/rnnoise-processor.min.js \
$(BUILD_DIR)/rnnoise-processor.min.map \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:
@@ -66,11 +63,6 @@ deploy-libflac:
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js.mem \
$(DEPLOY_DIR)
deploy-rnnoise-binary:
cp \
$(RNNOISE_WASM_DIR)/rnnoise.wasm \
$(DEPLOY_DIR)
deploy-css:
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
@@ -79,7 +71,7 @@ deploy-css:
deploy-local:
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-lib-jitsi-meet deploy-libflac
dev: deploy-init deploy-css deploy-lib-jitsi-meet deploy-libflac
$(WEBPACK_DEV_SERVER)
source-package:

View File

@@ -45,7 +45,6 @@ dependencies {
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.squareup.duktape:duktape-android:1.3.0'
if (!rootProject.ext.libreBuild) {
implementation 'com.amplitude:android-sdk:2.14.1'

View File

@@ -1,57 +0,0 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import com.squareup.duktape.Duktape;
@ReactModule(name = JavaScriptSandboxModule.NAME)
class JavaScriptSandboxModule extends ReactContextBaseJavaModule {
public static final String NAME = "JavaScriptSandbox";
public JavaScriptSandboxModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Evaluates the given code in a Duktape VM.
* @param code - The code that needs to evaluated.
* @param promise - Resolved with the output in case of success or rejected with an exception
* in case of failure.
*/
@ReactMethod
public void evaluate(String code, Promise promise) {
Duktape vm = Duktape.create();
try {
Object res = vm.evaluate(code);
promise.resolve(res.toString());
} catch (Throwable tr) {
promise.reject(tr);
} finally {
vm.close();
}
}
@Override
public String getName() {
return NAME;
}
}

View File

@@ -33,9 +33,6 @@ public class JitsiMeet {
}
public static void setDefaultConferenceOptions(JitsiMeetConferenceOptions options) {
if (options != null && options.getRoom() != null) {
throw new RuntimeException("'room' must be null in the default conference options");
}
defaultConferenceOptions = options;
}

View File

@@ -72,46 +72,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
*/
private JitsiMeetUserInfo userInfo;
public URL getServerURL() {
return serverURL;
}
public String getRoom() {
return room;
}
public String getSubject() {
return subject;
}
public String getToken() {
return token;
}
public Bundle getColorScheme() {
return colorScheme;
}
public Bundle getFeatureFlags() {
return featureFlags;
}
public boolean getAudioMuted() {
return audioMuted;
}
public boolean getAudioOnly() {
return audioOnly;
}
public boolean getVideoMuted() {
return videoMuted;
}
public JitsiMeetUserInfo getUserInfo() {
return userInfo;
}
/**
* Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
*/

View File

@@ -87,7 +87,7 @@ public class JitsiMeetOngoingConferenceService extends Service
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = intent.getAction();
if (Actions.START.equals(action)) {
if (action.equals(Actions.START)) {
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
if (notification == null) {
stopSelf();
@@ -96,7 +96,7 @@ public class JitsiMeetOngoingConferenceService extends Service
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
} else if (Actions.HANGUP.equals(action)) {
} else if (action.equals(Actions.HANGUP)) {
JitsiMeetLogger.i(TAG + " Hangup requested");
// Abort all ongoing calls
if (AudioModeModule.useConnectionService()) {

View File

@@ -67,7 +67,6 @@ class ReactInstanceManagerHolder {
new AudioModeModule(reactContext),
new DropboxModule(reactContext),
new ExternalAPIModule(reactContext),
new JavaScriptSandboxModule(reactContext),
new LocaleDetector(reactContext),
new LogBridgeModule(reactContext),
new PictureInPictureModule(reactContext),

View File

@@ -10,7 +10,7 @@ project(':react-native-community-async-storage').projectDir = new File(rootProje
include ':react-native-community_netinfo'
project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
include ':react-native-google-signin'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/google-signin/android')
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

@@ -104,6 +104,7 @@ import {
trackRemoved
} from './react/features/base/tracks';
import { getJitsiMeetGlobalNS } from './react/features/base/util';
import { addMessage } from './react/features/chat';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import {
@@ -113,6 +114,7 @@ import {
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import { setSharedVideoStatus } from './react/features/shared-video';
import { isButtonEnabled } from './react/features/toolbox';
import { endpointMessageReceived } from './react/features/subtitles';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -242,6 +244,8 @@ class ConferenceConnector {
this._handleConferenceJoined.bind(this));
room.on(JitsiConferenceEvents.CONFERENCE_FAILED,
this._onConferenceFailed.bind(this));
room.on(JitsiConferenceEvents.CONFERENCE_ERROR,
this._onConferenceError.bind(this));
}
/**
@@ -344,6 +348,31 @@ class ConferenceConnector {
}
}
/**
*
*/
_onConferenceError(err, ...params) {
logger.error('CONFERENCE Error:', err, params);
switch (err) {
case JitsiConferenceErrors.CHAT_ERROR:
logger.error('Chat error.', err);
if (isButtonEnabled('chat') && !interfaceConfig.filmStripOnly) {
const [ code, msg ] = params;
APP.store.dispatch(addMessage({
hasRead: true,
error: code,
message: msg,
messageType: 'error',
timestamp: Date.now()
}));
}
break;
default:
logger.error('Unknown error.', err);
}
}
/**
*
*/

View File

@@ -1,6 +1,16 @@
/* eslint-disable no-unused-vars, no-var */
var config = {
// Configuration
//
// Alternative location for the configuration.
// configLocation: './config.json',
// Custom function which given the URL path should return a room name.
// getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
// Connection
//
@@ -50,10 +60,6 @@ var config = {
// Enables the test specific features consumed by jitsi-meet-torture
// testMode: false
// Disables the auto-play behavior of *all* newly created video element.
// This is useful when the client runs on a host with limited resources.
// noAutoPlayVideo: false
},
// Disables ICE/UDP by filtering out local and remote UDP candidates in
@@ -419,10 +425,6 @@ var config = {
// the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false
// If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
// user documentation.
// userDocumentationURL: 'https://docs.example.com/video-meetings.html'
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold

View File

@@ -80,27 +80,6 @@
}
}
#chat-recipient {
align-items: center;
background-color: $defaultWarningColor;
display: flex;
flex-direction: row;
padding: 10px;
span {
color: white;
display: flex;
flex: 1;
}
div {
svg {
cursor: pointer;
fill: white
}
}
}
.chat-header {
background-color: $chatHeaderBackgroundColor;
height: 70px;
@@ -217,11 +196,6 @@
padding: 0;
}
}
.privatemessagenotice {
color: $defaultWarningColor;
font-style: italic;
}
}
.smiley {
@@ -254,7 +228,6 @@
.smileys-panel {
bottom: 100%;
box-sizing: border-box;
background-color: rgba(0, 0, 0, .6) !important;
height: auto;
max-height: 0;
overflow: hidden;
@@ -339,16 +312,6 @@
.chatmessage-wrapper {
max-width: 100%;
.replywrapper {
display: flex;
flex-direction: row;
align-items: center;
.toolbox-icon {
cursor: pointer;
}
}
}
.chatmessage {

View File

@@ -6,7 +6,7 @@
min-width: 75px;
text-align: left;
padding: 0px;
width: 180px;
width: 150px;
white-space: nowrap;
&__item {
@@ -87,7 +87,6 @@
display: inline-block;
min-width: 20px;
height: 100%;
padding-right: 10px;
> * {
@include absoluteAligning();

View File

@@ -28,7 +28,6 @@ $defaultColor: #F1F1F1;
$defaultSideBarFontColor: #44A5FF;
$defaultSemiDarkColor: #ACACAC;
$defaultDarkColor: #2b3d5c;
$defaultWarningColor: rgb(215, 121, 118);
/**
* Toolbar
@@ -164,45 +163,9 @@ $watermarkHeight: 74px;
*/
$welcomePageDescriptionColor: #fff;
$welcomePageFontFamily: inherit;
$welcomePageBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageHeaderBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageTitleColor: #fff;
$welcomePageHeaderBackground: none;
$welcomePageHeaderBackgroundSmall: none;
$welcomePageHeaderBackgroundPosition: none;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: none;
$welcomePageHeaderPaddingBottom: 0px;
$welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextDescriptionDisplay: inherit;
$welcomePageEnterRoomWidth: 680px;
$welcomePageEnterRoomPadding: 25px 30px;
$welcomePageEnterRoomBorderRadius: 0px;
$welcomePageEnterRoomInputContainerPadding: 0 8px 5px 0px;
$welcomePageEnterRoomInputContainerBorderWidth: 0px 0px 2px 0px;
$welcomePageEnterRoomInputContainerBorderStyle: solid;
$welcomePageEnterRoomInputContainerBorderImage: linear-gradient(to right, #dee1e6, #fff) 1;
$welcomePageEnterRoomTitleDisplay: inherit;
$welcomePageTabContainerDisplay: flex;
$welcomePageTabContentDisplay: inherit;
$welcomePageTabButtonsDisplay: flex;
$welcomePageTabDisplay: block;
$welcomePageButtonWidth: 51px;
$welcomePageButtonHeight: 35px;
$welcomePageButtonFontWeight: inherit;
$welcomePageButtonBorderRadius: 4px;
$welcomePageButtonLineHeight: 35px;
/**
* Deep-linking page variables.
*/

View File

@@ -4,7 +4,7 @@ body.welcome-page {
}
.welcome {
background-image: $welcomePageBackground;
background-image: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
@@ -13,11 +13,6 @@ body.welcome-page {
position: relative;
.header {
background-image: $welcomePageHeaderBackground;
background-position: $welcomePageHeaderBackgroundPosition;
background-repeat: $welcomePageHeaderBackgroundRepeat;
background-size: $welcomePageHeaderBackgroundSize;
padding-bottom: $welcomePageHeaderPaddingBottom;
align-items: center;
display: flex;
flex-direction: column;
@@ -29,8 +24,8 @@ body.welcome-page {
.header-text {
display: flex;
flex-direction: column;
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
margin-bottom: $welcomePageHeaderTextMarginBottom;
margin-top: $watermarkHeight + 35;
margin-bottom: 35px;
max-width: calc(100% - 40px);
width: 650px;
z-index: $zindex2;
@@ -41,11 +36,10 @@ body.welcome-page {
font-size: 2.5rem;
font-weight: 500;
line-height: 1.18;
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
margin-bottom: 16px;
}
.header-text-description {
display: $welcomePageHeaderTextDescriptionDisplay;
color: $welcomePageDescriptionColor;
font-size: 1rem;
font-weight: 400;
@@ -57,24 +51,23 @@ body.welcome-page {
display: flex;
align-items: center;
max-width: calc(100% - 40px);
width: $welcomePageEnterRoomWidth;
width: 680px;
z-index: $zindex2;
background-color: #fff;
padding: $welcomePageEnterRoomPadding;
border-radius: $welcomePageEnterRoomBorderRadius;
padding: 25px 30px;
.enter-room-input-container {
width: 100%;
padding: $welcomePageEnterRoomInputContainerPadding;
padding-right: 8px;
padding-bottom: 5px;
text-align: left;
color: #253858;
height: fit-content;
border-width: $welcomePageEnterRoomInputContainerBorderWidth;
border-style: $welcomePageEnterRoomInputContainerBorderStyle;
border-image: $welcomePageEnterRoomInputContainerBorderImage;
border-width: 0px 0px 2px 0px;
border-style: solid;
border-image: linear-gradient(to right, #dee1e6, #fff) 1;
.enter-room-title {
display: $welcomePageEnterRoomTitleDisplay;
font-size: 18px;
font-weight: bold;
padding-bottom: 5px;
@@ -101,11 +94,10 @@ body.welcome-page {
min-height: 354px;
width: 710px;
background: #75A7E7;
display: $welcomePageTabContainerDisplay;
display: flex;
flex-direction: column;
.tab-content{
display: $welcomePageTabContentDisplay;
margin: 5px 0px;
overflow: hidden;
flex-grow: 1;
@@ -119,14 +111,13 @@ body.welcome-page {
.tab-buttons {
font-size: 18px;
color: #FFFFFF;
display: $welcomePageTabButtonsDisplay;
display: flex;
flex-grow: 0;
flex-direction: row;
min-height: 54px;
width: 100%;
.tab {
display: $welcomePageTabDisplay;
text-align: center;
background: rgba(9,30,66,0.37);
height: 55px;
@@ -147,16 +138,15 @@ body.welcome-page {
}
.welcome-page-button {
width: $welcomePageButtonWidth;
height: $welcomePageButtonHeight;
width: 51px;
height: 35px;
font-size: 14px;
font-weight: $welcomePageButtonFontWeight;
background: #0074E0;
border-radius: $welcomePageButtonBorderRadius;
border-radius: 4px;
color: #FFFFFF;
text-align: center;
vertical-align: middle;
line-height: $welcomePageButtonLineHeight;
line-height: 35px;
cursor: pointer;
}

View File

@@ -1 +0,0 @@
/** Insert custom CSS for any additional content in the welcome page settings toolbar **/

View File

@@ -51,7 +51,6 @@ $flagsImagePath: "../images/";
@import 'ringing/ringing';
@import 'welcome_page';
@import 'welcome_page_content';
@import 'welcome_page_settings_toolbar';
@import 'toolbars';
@import 'jquery.contextMenu';
@import 'keyboard-shortcuts';

View File

@@ -148,7 +148,6 @@
<!--#include virtual="title.html" -->
<!--#include virtual="plugin.head.html" -->
<!--#include virtual="static/welcomePageAdditionalContent.html" -->
<!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
</head>
<body>
<div id="react"></div>

View File

@@ -27,7 +27,6 @@ var interfaceConfig = {
SHOW_DEEP_LINKING_IMAGE: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
DISPLAY_WELCOME_PAGE_CONTENT: true,
DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: false,
APP_NAME: 'Jitsi Meet',
NATIVE_APP_NAME: 'Jitsi Meet',
PROVIDER_NAME: 'Jitsi',
@@ -222,13 +221,6 @@ var interfaceConfig = {
* milliseconds, those notifications should remain displayed.
*/
// ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT: 15000,
// List of undocumented settings
/**
INDICATOR_FONT_SIZES
MOBILE_DYNAMIC_LINK
PHONE_NUMBER_REGEX
*/
};
/* eslint-enable no-unused-vars, no-var, max-len */

View File

@@ -62,7 +62,7 @@ target 'JitsiMeet' do
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient'
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-community/async-storage'
pod 'RNGoogleSignin', :path => '../node_modules/@react-native-community/google-signin'
pod 'RNGoogleSignin', :path => '../node_modules/react-native-google-signin'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNSVG', :path => '../node_modules/react-native-svg'
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
@@ -81,7 +81,6 @@ post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['SUPPORTS_MACCATALYST'] = 'NO'
end
end
end

View File

@@ -1,10 +1,5 @@
PODS:
- Amplitude-iOS (4.0.4)
- AppAuth (1.2.0):
- AppAuth/Core (= 1.2.0)
- AppAuth/ExternalUserAgent (= 1.2.0)
- AppAuth/Core (1.2.0)
- AppAuth/ExternalUserAgent (1.2.0)
- boost-for-react-native (1.63.0)
- BVLinearGradient (2.5.6):
- React
@@ -67,10 +62,18 @@ PODS:
- GoogleUtilities/Network (~> 5.2)
- "GoogleUtilities/NSData+zlib (~> 5.2)"
- nanopb (~> 0.3)
- GoogleSignIn (5.0.1):
- AppAuth (~> 1.2)
- GTMAppAuth (~> 1.0)
- GoogleSignIn (4.4.0):
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
- "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)"
- GTMSessionFetcher/Core (~> 1.1)
- GoogleToolboxForMac/DebugUtils (2.2.0):
- GoogleToolboxForMac/Defines (= 2.2.0)
- GoogleToolboxForMac/Defines (2.2.0)
- "GoogleToolboxForMac/NSDictionary+URLArguments (2.2.0)":
- GoogleToolboxForMac/DebugUtils (= 2.2.0)
- GoogleToolboxForMac/Defines (= 2.2.0)
- "GoogleToolboxForMac/NSString+URLArguments (= 2.2.0)"
- "GoogleToolboxForMac/NSString+URLArguments (2.2.0)"
- GoogleUtilities/AppDelegateSwizzler (5.4.1):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
@@ -89,14 +92,7 @@ PODS:
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (5.4.1):
- GoogleUtilities/Logger
- GTMAppAuth (1.0.0):
- AppAuth/Core (~> 1.0)
- GTMSessionFetcher (~> 1.1)
- GTMSessionFetcher (1.2.2):
- GTMSessionFetcher/Full (= 1.2.2)
- GTMSessionFetcher/Core (1.2.2)
- GTMSessionFetcher/Full (1.2.2):
- GTMSessionFetcher/Core (= 1.2.2)
- GTMSessionFetcher/Core (1.2.1)
- nanopb (0.3.901):
- nanopb/decode (= 0.3.901)
- nanopb/encode (= 0.3.901)
@@ -276,7 +272,7 @@ PODS:
- React
- react-native-webrtc (1.75.0):
- React
- react-native-webview (7.4.1):
- react-native-webview (5.8.1):
- React
- React-RCTActionSheet (0.61.1):
- React-Core/RCTActionSheetHeaders (= 0.61.1)
@@ -334,8 +330,8 @@ PODS:
- ReactCommon/turbomodule/core (= 0.61.1)
- RNCAsyncStorage (1.3.4):
- React
- RNGoogleSignin (3.0.1):
- GoogleSignIn (~> 5.0.0)
- RNGoogleSignin (2.0.0):
- GoogleSignIn (~> 4.4.0)
- React
- RNSound (0.11.0):
- React
@@ -390,14 +386,14 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- ReactCommon/turbomodule (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)"
- "RNGoogleSignin (from `../node_modules/@react-native-community/google-signin`)"
- RNGoogleSignin (from `../node_modules/react-native-google-signin`)
- RNSound (from `../node_modules/react-native-sound`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNWatch (from `../node_modules/react-native-watch-connectivity`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
https://github.com/cocoapods/specs.git:
- Amplitude-iOS
- boost-for-react-native
- CocoaLumberjack
@@ -410,14 +406,12 @@ SPEC REPOS:
- FirebaseDynamicLinks
- FirebaseInstanceID
- GoogleAppMeasurement
- GoogleSignIn
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- nanopb
- ObjectiveDropboxOfficial
trunk:
- AppAuth
- GoogleSignIn
- GTMAppAuth
- GTMSessionFetcher
EXTERNAL SOURCES:
BVLinearGradient:
@@ -485,7 +479,7 @@ EXTERNAL SOURCES:
RNCAsyncStorage:
:path: "../node_modules/@react-native-community/async-storage"
RNGoogleSignin:
:path: "../node_modules/@react-native-community/google-signin"
:path: "../node_modules/react-native-google-signin"
RNSound:
:path: "../node_modules/react-native-sound"
RNSVG:
@@ -497,7 +491,6 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Amplitude-iOS: 2ad4d7270c99186236c1272a3a9425463b1ae1a7
AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
@@ -515,10 +508,10 @@ SPEC CHECKSUMS:
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
GoogleAppMeasurement: 6cf307834da065863f9faf4c0de0a936d81dd832
GoogleSignIn: 3a51b9bb8e48b635fd7f4272cee06ca260345b86
GoogleSignIn: 7ff245e1a7b26d379099d3243a562f5747e23d39
GoogleToolboxForMac: ff31605b7d66400dcec09bed5861689aebadda4d
GoogleUtilities: 1e25823cbf46540b4284f6ef8e17b3a68ee12bbc
GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23
GTMSessionFetcher: 32aeca0aa144acea523e1c8e053089dec2cb98ca
nanopb: 2901f78ea1b7b4015c860c2fdd1ea2fee1a18d48
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
RCTRequired: 53825815218847d3e9c7b6d92ad2d197a926d51e
@@ -535,7 +528,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-webrtc: c5e3d631179a933548a8e49bddbd8fad02586095
react-native-webview: 4dbc1d2a4a6b9c5e9e723c62651917aa2b5e579e
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
React-RCTActionSheet: af4d951113b1e068bb30611f91b984a7a73597ff
React-RCTAnimation: 4f518d70bb6890b7c3d9d732f84786d6693ca297
React-RCTBlob: 072a4888c08de0eef6d04eaa727d25e577e6ff26
@@ -547,12 +540,12 @@ SPEC CHECKSUMS:
React-RCTVibration: 8be61459e3749d1fb02cf414edd05b3007622882
ReactCommon: 4fba5be89efdf0b5720e0adb3d8d7edf6e532db0
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
RNGoogleSignin: d030c6c6591db24c3cee649f64c7babf0a1699a0
RNSound: c980916b596cc15c8dcd2f6ecd3b13c4881dbe20
RNSVG: aac12785382e8fd4f28d072fe640612e34914631
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
Yoga: d8c572ddec8d05b7dba08e4e5f1924004a177078
PODFILE CHECKSUM: cb84b325b724c6ef7c8b24aa52ca7b6f681a095c
PODFILE CHECKSUM: 5d4de5dc42643ef3864a9100b834caeafd625db7
COCOAPODS: 1.8.1
COCOAPODS: 1.7.2

View File

@@ -41,6 +41,8 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -50,8 +52,8 @@
ReferencedContainer = "container:app.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -73,6 +75,8 @@
ReferencedContainer = "container:app.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@@ -42,7 +42,6 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */ = {isa = PBXBuildFile; fileRef = DE438CD82350934700DD541D /* JavaScriptSandbox.m */; };
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = DE65AAC92317FFCD00290BEC /* LogUtils.h */; };
DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */; };
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -105,7 +104,6 @@
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
DE438CD82350934700DD541D /* JavaScriptSandbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JavaScriptSandbox.m; sourceTree = "<group>"; };
DE65AAC92317FFCD00290BEC /* LogUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LogUtils.h; sourceTree = "<group>"; };
DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetBaseLogHandler+Private.h"; sourceTree = "<group>"; };
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetUserInfo.h; sourceTree = "<group>"; };
@@ -192,7 +190,6 @@
A4A934E7212F3AB8001E9388 /* dropbox */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
DE438CD82350934700DD541D /* JavaScriptSandbox.m */,
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
DEFE535821FB311F00011A3A /* JitsiMeet+Private.h */,
DEFE535321FB1BF800011A3A /* JitsiMeet.m */,
@@ -496,7 +493,6 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */,
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -638,18 +634,15 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -669,17 +662,14 @@
ENABLE_BITCODE = YES;
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};

View File

@@ -81,7 +81,7 @@
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Script"
scriptText = "exec &gt; /tmp/${PROJECT_NAME}_archive.log 2&gt;&amp;1&#10;&#10;UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal&#10;&#10;if [ &quot;true&quot; == ${ALREADYINVOKED:-false} ]&#10;then&#10;echo &quot;RECURSION: Detected, stopping&quot;&#10;else&#10;export ALREADYINVOKED=&quot;true&quot;&#10;&#10;# make sure the output directory exists&#10;mkdir -p &quot;${UNIVERSAL_OUTPUTFOLDER}&quot;&#10;&#10;echo &quot;Building for iPhoneSimulator&quot;&#10;xcodebuild -workspace &quot;${WORKSPACE_PATH}&quot; -scheme &quot;${TARGET_NAME}&quot; -configuration ${CONFIGURATION} -sdk iphonesimulator -destination &apos;platform=iOS Simulator,name=iPhone 8&apos; ONLY_ACTIVE_ARCH=NO ARCHS=&apos;i386 x86_64&apos; BUILD_DIR=&quot;${BUILD_DIR}&quot; BUILD_ROOT=&quot;${BUILD_ROOT}&quot; ENABLE_BITCODE=YES OTHER_CFLAGS=&quot;-fembed-bitcode&quot; BITCODE_GENERATION_MODE=bitcode clean build&#10;&#10;# Step 1. Copy the framework structure (from iphoneos build) to the universal folder&#10;echo &quot;Copying to output folder&quot;&#10;cp -R &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/&quot;&#10;&#10;# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory&#10;SIMULATOR_SWIFT_MODULES_DIR=&quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/.&quot;&#10;if [ -d &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; ]; then&#10;cp -R &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule&quot;&#10;fi&#10;&#10;# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory&#10;echo &quot;Combining executables&quot;&#10;lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}&quot;&#10;&#10;# Step 4. Create universal binaries for embedded frameworks&#10;#for SUB_FRAMEWORK in $( ls &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks&quot; ); do&#10;#BINARY_NAME=&quot;${SUB_FRAMEWORK%.*}&quot;&#10;#lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot;&#10;#done&#10;&#10;# Step 5. Convenience step to copy the framework to the project&apos;s directory&#10;echo &quot;Copying to project dir&quot;&#10;yes | cp -Rf &quot;${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}&quot; &quot;${PROJECT_DIR}&quot;&#10;&#10;fi&#10;">
scriptText = "exec &gt; /tmp/${PROJECT_NAME}_archive.log 2&gt;&amp;1&#10;&#10;UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal&#10;&#10;if [ &quot;true&quot; == ${ALREADYINVOKED:-false} ]&#10;then&#10;echo &quot;RECURSION: Detected, stopping&quot;&#10;else&#10;export ALREADYINVOKED=&quot;true&quot;&#10;&#10;# make sure the output directory exists&#10;mkdir -p &quot;${UNIVERSAL_OUTPUTFOLDER}&quot;&#10;&#10;echo &quot;Building for iPhoneSimulator&quot;&#10;xcodebuild -workspace &quot;${WORKSPACE_PATH}&quot; -scheme &quot;${TARGET_NAME}&quot; -configuration ${CONFIGURATION} -sdk iphonesimulator -destination &apos;platform=iOS Simulator,name=iPhone 6&apos; ONLY_ACTIVE_ARCH=NO ARCHS=&apos;i386 x86_64&apos; BUILD_DIR=&quot;${BUILD_DIR}&quot; BUILD_ROOT=&quot;${BUILD_ROOT}&quot; ENABLE_BITCODE=YES OTHER_CFLAGS=&quot;-fembed-bitcode&quot; BITCODE_GENERATION_MODE=bitcode clean build&#10;&#10;# Step 1. Copy the framework structure (from iphoneos build) to the universal folder&#10;echo &quot;Copying to output folder&quot;&#10;cp -R &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FULL_PRODUCT_NAME}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/&quot;&#10;&#10;# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory&#10;SIMULATOR_SWIFT_MODULES_DIR=&quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/.&quot;&#10;if [ -d &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; ]; then&#10;cp -R &quot;${SIMULATOR_SWIFT_MODULES_DIR}&quot; &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule&quot;&#10;fi&#10;&#10;# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory&#10;echo &quot;Combining executables&quot;&#10;lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphoneos/${EXECUTABLE_PATH}&quot;&#10;&#10;# Step 4. Create universal binaries for embedded frameworks&#10;#for SUB_FRAMEWORK in $( ls &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks&quot; ); do&#10;#BINARY_NAME=&quot;${SUB_FRAMEWORK%.*}&quot;&#10;#lipo -create -output &quot;${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}&quot; &quot;${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}&quot;&#10;#done&#10;&#10;# Step 5. Convenience step to copy the framework to the project&apos;s directory&#10;echo &quot;Copying to project dir&quot;&#10;yes | cp -Rf &quot;${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}&quot; &quot;${PROJECT_DIR}&quot;&#10;&#10;fi">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"

View File

@@ -1,55 +0,0 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import JavaScriptCore;
#import <React/RCTBridgeModule.h>
@interface JavaScriptSandbox : NSObject<RCTBridgeModule>
@end
@implementation JavaScriptSandbox
RCT_EXPORT_MODULE();
+ (BOOL)requiresMainQueueSetup {
return NO;
}
#pragma mark - Exported methods
RCT_EXPORT_METHOD(evaluate:(NSString *)code
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
__block BOOL hasError = NO;
JSContext *ctx = [[JSContext alloc] init];
ctx.exceptionHandler = ^(JSContext *context, JSValue *exception) {
hasError = YES;
reject(@"evaluate", [exception toString], nil);
};
JSValue *ret = [ctx evaluateScript:code];
if (!hasError) {
NSString *result = [ret toString];
if (result == nil) {
reject(@"evaluate", @"Error in string coercion", nil);
} else {
resolve(result);
}
}
}
@end

View File

@@ -37,6 +37,7 @@
* List of domains used for universal linking.
*/
@property (copy, nonatomic, nullable) NSArray<NSString *> *universalLinkDomains;
/**
* Default conference options used for all conferences. These options will be merged
* with those passed to JitsiMeetView.join when joining a conference.

View File

@@ -90,7 +90,8 @@
if ([RNGoogleSignin application:app
openURL:url
options:options]) {
sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]) {
return YES;
}
@@ -178,15 +179,6 @@
return _universalLinkDomains ? _universalLinkDomains : @[];
}
- (void)setDefaultConferenceOptions:(JitsiMeetConferenceOptions *)defaultConferenceOptions {
if (defaultConferenceOptions != nil && _defaultConferenceOptions.room != nil) {
@throw [NSException exceptionWithName:@"RuntimeError"
reason:@"'room' must be null in the default conference options"
userInfo:nil];
}
_defaultConferenceOptions = defaultConferenceOptions;
}
#pragma mark - Private API methods
- (NSDictionary *)getDefaultProps {

View File

@@ -1,31 +1,27 @@
{
"en": "Английски",
"af": "Африканс",
"az": "Азербайджански",
"bg": "Български",
"ca": "Каталонски",
"cs": "Чешки",
"de": "Немски",
"el": "Гръцки",
"enGB": "Английски (Великобритания)",
"eo": "Есперанто",
"es": "Испански",
"esUS": "Испански (Латинска Америка)",
"fi": "Фински",
"fr": "Френски",
"frCA": "Френски (Канада)",
"hr": "Хърватски",
"hy": "Арменски",
"it": "Италиански",
"ja": "Японски",
"ko": "Корейски",
"nl": "Нидерландски",
"nb": "Норвежки букмол",
"oc": "Окситански",
"pl": "Полски",
"ptBR": "Португалски (Бразилия)",
"ru": "Руски",
"sk": "Словашки",
"sl": "Словенски",
"sv": "Шведски",
"tr": "Турски",
"vi": "Виетнамски",
"zhCN": "Китайски (Китай)",
"zhTW": "Тайвански"
"zhCN": "Китайски (Китай)"
}

View File

@@ -1,31 +1,27 @@
{
"en": "angol",
"af": "afrikaans",
"bg": "bolgár",
"ca": "katalán",
"cs": "cseh",
"de": "német",
"el": "görög",
"enGB": "angol (Egyesült Királyság)",
"eo": "eszperantó",
"es": "spanyol",
"esUS": "spanyol (Latin-Amerika)",
"fi": "finn",
"fr": "francia",
"frCA": "francia (kanadai)",
"hr": "horvát",
"hy": "örmény",
"it": "olasz",
"ja": "japán",
"ko": "koreai",
"nl": "holland",
"oc": "okszitán",
"pl": "lengyel",
"ptBR": "portugál (Brazil)",
"ru": "orosz",
"sv": "svéd",
"tr": "török",
"vi": "vietnámi",
"zhCN": "kínai (Kína)",
"zhTW": "kínai (Tajvan)"
"en": "Angol",
"af": "",
"az": "",
"bg": "Bolgár",
"cs": "",
"de": "Német",
"el": "",
"eo": "Eszperantó",
"es": "Spanyol",
"fr": "Francia",
"hy": "Örmény",
"it": "Olasz",
"ja": "",
"ko": "",
"nb": "Norvég bokmal",
"oc": "Okszitán",
"pl": "Lengyel",
"ptBR": "Portugál (Brazil)",
"ru": "Orosz",
"sk": "Szlovák",
"sl": "Szlovén",
"sv": "Svéd",
"tr": "Török",
"vi": "",
"zhCN": "Kínai (Kína)"
}

View File

@@ -2,23 +2,23 @@
"en": "Anglés",
"af": "Afrikaans",
"bg": "Bulgar",
"ca": "Catalan",
"ca": "",
"cs": "Chèc",
"de": "Aleman",
"el": "Grèc",
"enGB": "Anglés (Reialme Unit)",
"enGB": "",
"eo": "Esperanto",
"es": "Castelhan",
"esUS": "Espanhòl (America latina)",
"fi": "Finés",
"esUS": "",
"fi": "",
"fr": "Francés",
"frCA": "Francés (Canadian)",
"hr": "Croat",
"frCA": "",
"hr": "",
"hy": "Armenian",
"it": "Italian",
"ja": "Japonés",
"ko": "Corean",
"nl": "Neerlandés",
"nl": "",
"oc": "Occitan",
"pl": "Polonés",
"ptBR": "Portugués (Brasil)",
@@ -27,5 +27,5 @@
"tr": "Turc",
"vi": "Vietnamian",
"zhCN": "Chinés (China)",
"zhTW": "Chinés (Taiwan)"
"zhTW": ""
}

View File

@@ -1,31 +1,27 @@
{
"en": "Angielski",
"af": "Afrykanerski",
"en": "Anglik",
"af": "",
"az": "Azerski",
"bg": "Bułgarski",
"ca": "",
"cs": "Czeski",
"de": "Niemiecki",
"el": "Grecki",
"enGB": "",
"eo": "Esperanto",
"es": "Hiszpański",
"esUS": "",
"fi": "",
"fr": "Francuski",
"frCA": "",
"hr": "",
"hy": "Ormiański",
"it": "Włoski",
"ja": "Japoński",
"ko": "Koreański",
"nl": "",
"nb": "Norweski Bokmal",
"oc": "Oksytański",
"pl": "Polski",
"ptBR": "Portugalski (brazylijski)",
"ptBR": "portugalski (brazylijski)",
"ru": "Rosyjski",
"sk": "Słowacki",
"sl": "Słoweński",
"sv": "Szwedzki",
"tr": "Turecki",
"vi": "Wietnamski",
"zhCN": "Chiński (Chiny)",
"zhTW": ""
"zhCN": "Chiński (Chiny)"
}

View File

@@ -1,31 +1,27 @@
{
"en": "Inglês",
"af": "Africâner",
"az": "Azerbaijanês",
"bg": "Búlgaro",
"ca": "",
"cs": "Checo",
"de": "Alemão",
"el": "Grego",
"enGB": "",
"eo": "Esperanto",
"es": "Espanhol",
"esUS": "",
"fi": "",
"fr": "Francês",
"frCA": "",
"hr": "",
"hy": "Armênio",
"it": "Italiano",
"ja": "Japonês",
"ko": "Coreano",
"nl": "",
"nb": "Bokmal norueguês",
"oc": "Occitano",
"pl": "Polonês",
"ptBR": "Português (Brasil)",
"ru": "Russo",
"sk": "Eslovaco",
"sl": "Esloveno",
"sv": "Sueco",
"tr": "Turco",
"vi": "Vietnamita",
"zhCN": "Chinês (China)",
"zhTW": ""
"zhCN": "Chinês (China)"
}

View File

@@ -14,7 +14,6 @@
"fr": "French",
"frCA": "French (Canadian)",
"hr": "Croatian",
"hu": "Hungarian",
"hy": "Armenian",
"it": "Italian",
"ja": "Japanese",

View File

@@ -4,8 +4,8 @@
"countryNotSupported": "Желаната дестинация не се поддържа.",
"countryReminder": "Международно обаждане? Започнете номера с международният код!",
"disabled": "Не можете да каните хора.",
"failedToAdd": "Неуспешно добавяне на участници",
"footerText": "Изходящите разговори не са разрешени.",
"failedToAdd": "",
"footerText": "Изходящиите разговори не са разрешени.",
"loading": "Търсене на хора и телефонни номера.",
"loadingNumber": "Валидиране на номера",
"loadingPeople": "Търсене на хора",
@@ -13,49 +13,48 @@
"noValidNumbers": "Моля въведете телефонен номер",
"searchNumbers": "Добавяне на номера",
"searchPeople": "Търсене на хора",
"searchPeopleAndNumbers": "Търсене на участници или добавяне с телефони номера",
"telephone": "Телефон: {{number}}",
"title": "Добавяне на участници в срещата"
"searchPeopleAndNumbers": "",
"telephone": "",
"title": ""
},
"audioDevices": {
"bluetooth": "Bluetooth",
"bluetooth": "",
"headphones": "Слушалки",
"phone": "Телефон",
"speaker": "Говорещ",
"none": "Няма налични устройства за звук"
"speaker": "Говорещ"
},
"audioOnly": {
"audioOnly": "Нисък дебит"
"audioOnly": "Само звук"
},
"calendarSync": {
"addMeetingURL": "Добавяне на връзка за среща",
"confirmAddLink": "Искате ли да добавите връзка към това събитие?",
"addMeetingURL": "",
"confirmAddLink": "",
"error": {
"appConfiguration": "Интеграцията с календара не е настроена.",
"generic": "Грешка, моля проверете настройката за календара или го обновете.",
"notSignedIn": "Грешка при идентификация за изтегляне на събития. Моля проверете настройките на календара и опитайте отново."
"appConfiguration": "",
"generic": "",
"notSignedIn": ""
},
"join": "Влизане",
"joinTooltip": "Влизане в срещата",
"nextMeeting": "следваща среща",
"noEvents": "Няма насрочени бъдещи събития.",
"ongoingMeeting": "настояща среща",
"permissionButton": "Отваряне на настройки",
"permissionMessage": "За показване на срещите ви е нужно позволение за ползване на календара.",
"refresh": "Обновяване на календара",
"today": "Днес"
"join": "",
"joinTooltip": "",
"nextMeeting": "",
"noEvents": "",
"ongoingMeeting": "",
"permissionButton": "",
"permissionMessage": "",
"refresh": "",
"today": ""
},
"chat": {
"error": "Грешка: вашето съобщение \"{{originalText}}\" не е бе изпратено. Поради: {{error}}",
"messagebox": "Въведете съобщение",
"error": "",
"messagebox": "",
"nickname": {
"popover": "Избор на име",
"title": "Въведете име за да обменяте съобщения"
"title": ""
},
"title": "Текстови съобщения"
"title": ""
},
"connectingOverlay": {
"joiningRoom": "Свързване с вашата среща..."
"joiningRoom": ""
},
"connection": {
"ATTACHED": "Прикрепен",
@@ -73,8 +72,8 @@
"address": "Адрес:",
"bandwidth": "Предполагаема скорост:",
"bitrate": "Скорост:",
"bridgeCount": "Брой сървъри:",
"connectedTo": "Свързан към:",
"bridgeCount": "",
"connectedTo": "",
"framerate": "Кадри в секунда:",
"less": "Скриване",
"localaddress": "Локален адрес:",
@@ -97,25 +96,26 @@
"resolution": "Резолюция:",
"status": "Връзка:",
"transport": "Транспорт:",
"transport_plural": "Транспорти:"
"transport_plural": "Транспорти:",
"turn": " (обръщане)"
},
"dateUtils": {
"earlier": "По-рано",
"today": "Днес",
"yesterday": "Вчера"
"earlier": "",
"today": "",
"yesterday": ""
},
"deepLinking": {
"appNotInstalled": "Имате нужда от мобилното приложение {{app}} за влизане в тази среща от телефона.",
"description": "Нищо не се случва? Опитахме се да заредим срещата в приложението {{app}}. Пробвайте отново или влезте чрез уеб приложението {{app}}.",
"descriptionWithoutWeb": "Нищо не се случва? Опитахме се да заредим срещата в приложението {{app}}.",
"downloadApp": "Свалете приложението",
"launchWebButton": "Заредете уеб страницата",
"openApp": "Продължете към приложението",
"title": "Зареждане на срещата в {{app}}...",
"tryAgainButton": "Пробвайте отново"
"appNotInstalled": "",
"description": "",
"descriptionWithoutWeb": "",
"downloadApp": "Сваляне не приложението",
"launchWebButton": "",
"openApp": "",
"title": "",
"tryAgainButton": ""
},
"\u0005deepLinking": {},
"defaultLink": "напр. {{url}}",
"defaultNickname": "напр. Иван Иванов",
"deviceError": {
"cameraError": "Камерата е недостъпна",
"cameraPermission": "Грешка при получаване на разрешение за достъп до камерата",
@@ -126,14 +126,14 @@
"noPermission": "Не е получено разрешение",
"previewUnavailable": "Няма възможност за преглед",
"selectADevice": "Изберете устройство",
"testAudio": "Пусни пробен звук"
"testAudio": ""
},
"dialog": {
"accessibilityLabel": {
"liveStreaming": "Излъчване на живо"
},
"allow": "Разрешаване",
"alreadySharedVideoMsg": "Друг участник вече е споделил видео. Тази среща позволява само едно споделено видео.",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "Разрешено е споделянето само на едно видео в даден момент",
"applicationWindow": "Прозореца на програмата",
"Back": "Назад",
@@ -150,8 +150,8 @@
"conferenceDisconnectTitle": "Връзката се разпадна.",
"conferenceReloadMsg": "Опитваме се да оправим нещата. Повторно свързване след {{seconds}} сек…",
"conferenceReloadTitle": "За съжаление, нещо се обърка.",
"confirm": "Потвърждение",
"confirmNo": "Не",
"confirm": "",
"confirmNo": "",
"confirmYes": "Да",
"connectError": "Опа! Нещо се обърка и не успяхме да се свържем с конференцията.",
"connectErrorWithMsg": "Опа! Нещо се обърка и не успяхме да се свържем с конференцията: {{msg}}",
@@ -159,66 +159,66 @@
"contactSupport": "Връзка с отдела по поддръжка",
"copy": "Копиране",
"dismiss": "Отхвърляне",
"displayNameRequired": "Здравей! Как се казваш?",
"displayNameRequired": "",
"done": "Готово",
"enterDisplayName": "Моля въведете вашето име",
"enterDisplayName": "",
"error": "Грешка",
"externalInstallationMsg": "Трябва да инсталирате разширението за споделяне на екрана.",
"externalInstallationTitle": "Нужно е разширение",
"goToStore": "Към магазина в Интернет",
"gracefulShutdown": "Услугата временно не е достъпна поради профилактика. Моля опитайте по-късно.",
"IamHost": "Аз съм домакина",
"incorrectRoomLockPassword": "Грешна парола",
"incorrectRoomLockPassword": "",
"incorrectPassword": "Неправилно потребителско име или парола",
"inlineInstallationMsg": "Трябва да инсталирате разширението за споделяне на екрана.",
"inlineInstallExtension": "Инсталиране сега",
"internalError": "Опа! Нещо се обърка. Възникна следната грешка: {{error}}",
"internalErrorTitle": "Вътрешна грешка",
"kickMessage": "Може да се свържете с {{participantDisplayName}} за повече подробности.",
"kickParticipantButton": "Изгони",
"kickParticipantDialog": "Сигурни ли сте че искате да изгоните участника?",
"kickParticipantTitle": "Изгонване на този участник?",
"kickTitle": "Ауч! {{participantDisplayName}} ви изгони от тази среща",
"kickMessage": "",
"kickParticipantButton": "",
"kickParticipantDialog": "",
"kickParticipantTitle": "",
"kickTitle": "",
"liveStreaming": "Излъчване на живо",
"liveStreamingDisabledForGuestTooltip": "Гостите не могат да стартират излъчване на живо.",
"liveStreamingDisabledTooltip": "Излъчването на живо е деактивирано.",
"liveStreamingDisabledForGuestTooltip": "",
"liveStreamingDisabledTooltip": "",
"lockMessage": "Неуспешно заключване на конференцията.",
"lockRoom": "Добавяне $t(lockRoomPasswordUppercase) за срещата",
"lockRoom": "",
"lockTitle": "Неуспешно заключване",
"logoutQuestion": "Сигурни ли сте, че искате да излезете и да прекъснете конференцията?",
"logoutTitle": "Изход",
"maxUsersLimitReached": "Лимитът за максимален брой участници бе достигнат. Капацитета на срещата е запълнен. Моля свържете се с организатора или опитайте по-късно!",
"maxUsersLimitReachedTitle": "Достигнат е лимита за максимален брой участници",
"maxUsersLimitReached": "",
"maxUsersLimitReachedTitle": "",
"micConstraintFailedError": "Микрофонът Ви не покрива някои от изискванията.",
"micNotFoundError": "Не е открит микрофон.",
"micNotSendingData": "Пуснете микрофона си от системните настройки на компютъра ви.",
"micNotSendingDataTitle": "Микрофона ви е спрян от системните настройки",
"micNotSendingData": "",
"micNotSendingDataTitle": "",
"micPermissionDeniedError": "Не сте дали разрешение за използване на микрофона. Ще можете да се присъедините в беседата, но другите няма да Ви чуват. Използвайте бутона с камерата в адресната лента, за да оправите това.",
"micUnknownError": "Невъзможен достъп до микрофона по неясна причина.",
"micUnknownError": "Не възможен достъп до микрофона по неясна причина.",
"muteParticipantBody": "Вие няма да можете да спрете заглушаването на участника, но той ще може да го направи по всяко време.",
"muteParticipantButton": "Изключи микрофона",
"muteParticipantDialog": "Сигурни ли сте че искате да заглушите този участник? Няма да можете да пуснете обратно звука му, но участника ще може да направи това сам.",
"muteParticipantTitle": "Спиране звука на участник?",
"muteParticipantDialog": "",
"muteParticipantTitle": "",
"Ok": "Готово",
"passwordLabel": "Парола",
"passwordNotSupported": "Задаването на $t(lockRoomPassword) за срещата не се поддържа.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) не се поддържа",
"passwordRequired": "Изисква се $t(lockRoomPassword) ",
"passwordLabel": "",
"passwordNotSupported": "Задаването на парола за срещата не се поддържа.",
"passwordNotSupportedTitle": "",
"passwordRequired": "",
"popupError": "Браузърът Ви блокира изскачащите прозорци от този уеб сайт. Моля, разрешете изскачащите прозорци от настройките за сигурност на браузъра си и след това опитайте отново.",
"popupErrorTitle": "Блокиран изскачащ прозорец",
"recording": "Запис",
"recordingDisabledForGuestTooltip": "Гостите не могат да стартират запис",
"recordingDisabledTooltip": "Стартирането на запис е спряно",
"recordingDisabledForGuestTooltip": "",
"recordingDisabledTooltip": "",
"rejoinNow": "Повторно присъединяване сега",
"remoteControlAllowedMessage": "{{user}} прие заявката Ви за отдалечено управление!",
"remoteControlDeniedMessage": "{{user}} отказа заявката Ви за отдалечено управление!",
"remoteControlErrorMessage": "Възникна грешка при опита за искане на разрешение за отдалечено управление от {{user}}!",
"remoteControlErrorMessage": "Възникна грешка при опита за искана на разрешение за отдалечено управление от {{user}}!",
"remoteControlRequestMessage": "Ще позволите ли на {{user}} да управлява отдалечено компютъра Ви?",
"remoteControlShareScreenWarning": "Ако натиснете „Разрешаване“, ще споделите екрана си!",
"remoteControlStopMessage": "Сесията за отдалечено управление приключи!",
"remoteControlTitle": "Отдалечено управление на компютъра",
"Remove": "Премахване",
"removePassword": "Премахване на $t(lockRoomPassword)",
"removePassword": "",
"removeSharedVideoMsg": "Наистина ли искате да премахнете споделеното си видео?",
"removeSharedVideoTitle": "Край на споделянето на видео",
"reservationError": "Грешка в системата за резервации",
@@ -226,8 +226,8 @@
"retry": "Повторен опит",
"screenSharingFailedToInstall": "Опа! Разширението за споделяне на екрана не успя да се инсталира.",
"screenSharingFailedToInstallTitle": "Разширението за споделяне на екрана не успя да се инсталира",
"screenSharingFirefoxPermissionDeniedError": "Нещо се обърка докато се опитвахме да споделим екрана. Моля уверете се че сте дали права за това.",
"screenSharingFirefoxPermissionDeniedTitle": "Упс! Не успяхме да стартираме споделянето на екрана!",
"screenSharingFirefoxPermissionDeniedError": "",
"screenSharingFirefoxPermissionDeniedTitle": "",
"screenSharingPermissionDeniedError": "Опа! Нещо се обърка с разрешенията на разширението за споделяне на екрана. Моля, презаредете и опитайте отново.",
"serviceUnavailable": "Услугата не е налична",
"sessTerminated": "Разговорът приключи",
@@ -235,86 +235,91 @@
"shareVideoLinkError": "Моля въведете правилна връзка към YouTube.",
"shareVideoTitle": "Сподели видео",
"shareYourScreen": "Споделяне на екрана",
"shareYourScreenDisabled": "Споделянето на екрана не се поддържа.",
"shareYourScreenDisabledForGuest": "Гостите не могат да споделят екрана.",
"shareYourScreenDisabled": "",
"shareYourScreenDisabledForGuest": "",
"startLiveStreaming": "Започване на излъчване на живо",
"startRecording": "Стартиране на запис",
"startRecording": "Край на записа",
"startRemoteControlErrorMessage": "Възникна грешка при опита за започване на сесията за отдалечено управление!",
"stopLiveStreaming": "Спиране на излъчването на живо",
"stopRecording": "Край на записа",
"stopRecordingWarning": "Наистина ли искате да спрем записа?",
"stopStreamingWarning": "Наистина ли искате да спрете излъчването на живо?",
"streamKey": "Ключ за излъчване на живо",
"streamKey": "",
"Submit": "Изпращане",
"thankYou": "Благодарим, че използвахте {{appName}}!",
"token": "код за достъп",
"tokenAuthFailed": "Съжаляваме, но не можете да се присъедините към този разговор.",
"tokenAuthFailedTitle": "Неуспешна идентификация",
"transcribing": "Транскрипция",
"unlockRoom": "Премахване $t(lockRoomPassword) от срещата",
"transcribing": "",
"unlockRoom": "",
"userPassword": "потребителска парола",
"WaitForHostMsg": "Конференцията <b>{{room}}</b> все още не е започнала. Ако сте домакинът тогава се идентифицирайте. В противен случай изчакайте докато домакинът пристигне.",
"WaitForHostMsgWOk": "Конференцията <b>{{room}}</b> все още не е започнала. Ако сте домакинът тогава натиснете бутона за да се идентифицирате. В противен случай изчакайте докато домакинът пристигне.",
"WaitForHostMsg": "",
"WaitForHostMsgWOk": "",
"WaitingForHost": "Чакаме домакина ...",
"Yes": "Да",
"yourEntireScreen": "Целия екран"
},
"\u0005dialog": {
"accessibilityLabel": {}
},
"dialOut": {
"statusMessage": "в момента е {{status}}"
},
"feedback": {
"average": "Средно",
"bad": "Лошо",
"detailsLabel": "Разкажете ни повече.",
"detailsLabel": "",
"good": "Добра",
"rateExperience": "Моля, оценете качеството на срещата.",
"veryBad": "Много лошо",
"veryGood": "Много добра"
},
"\u0005feedback": {},
"incomingCall": {
"answer": "Вдигни",
"audioCallTitle": "Входящ разговор",
"answer": "",
"audioCallTitle": "",
"decline": "Отхвърляне",
"productLabel": "от Jitsi Meet",
"videoCallTitle": "Входящ видео разговор"
"productLabel": "",
"videoCallTitle": ""
},
"info": {
"accessibilityLabel": "Покажи информация",
"addPassword": "Добави $t(lockRoomPassword)",
"cancelPassword": "Премахни $t(lockRoomPassword)",
"conferenceURL": "Връзка:",
"country": "Страна",
"dialANumber": "За влизане в срещата, наберете един от изброените номера и въведете кода.",
"dialInConferenceID": "Код:",
"dialInNotSupported": "Съжаляваме, обаждането в момента не се поддържа. ",
"dialInNumber": "Тел:",
"dialInSummaryError": "Проблем при достъпа на информация за опциите за влизане през телефон. Моля опитайте отново по-късно.",
"dialInTollFree": "Безплатен",
"genericError": "Упс, нещо се случи.",
"inviteLiveStream": "За да видите предаването на живо на срещата, използвйте тази връзка: {{url}}",
"invitePhone": "За влизане през телефон, използвайте: {{number}},,{{conferenceID}}#\n",
"invitePhoneAlternatives": "Търсене на друг номер за набиране?\nВижте още номера: : {{url}}\n\n\nАко вече сте набрали от телефон в стаята, влезте без да е пуснат звука: {{silentUrl}}",
"inviteURLFirstPartGeneral": "Поканени сте да се присъедините към среща.",
"inviteURLFirstPartPersonal": "{{name}} ви кани за среща.\n",
"inviteURLSecondPart": "\nВлезте в срещата:\n{{url}}\n",
"liveStreamURL": "Излъчване на живо:",
"moreNumbers": "Повече номера",
"noNumbers": "Няма номера за набиране.",
"accessibilityLabel": "",
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "",
"country": "",
"dialANumber": "",
"dialInConferenceID": "",
"dialInNotSupported": "",
"dialInNumber": "",
"dialInSummaryError": "",
"dialInTollFree": "",
"genericError": "",
"inviteLiveStream": "",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURLFirstPartGeneral": "",
"inviteURLFirstPartPersonal": "",
"inviteURLSecondPart": "",
"liveStreamURL": "Излъчване на живо",
"moreNumbers": "",
"noNumbers": "",
"noPassword": "Няма",
"noRoom": "Няма посочена стая за информация за номера за набиране.",
"numbers": "Номера",
"password": "$t(lockRoomPasswordUppercase):",
"noRoom": "",
"numbers": "",
"password": "",
"title": "Споделяне",
"tooltip": "Споделете връзката и информацията за номера свързани със срещата",
"label": "Информация за срещата"
"tooltip": "",
"label": ""
},
"\u0005info": {},
"inviteDialog": {
"alertText": "Не успях да поканя участниците.",
"alertText": "",
"header": "Покани",
"searchCallOnlyPlaceholder": "Въведете телефонен номер",
"searchPeopleOnlyPlaceholder": "Търсене на участници",
"searchPlaceholder": "Участник или телефонен номер",
"send": "Изпрати"
"searchPeopleOnlyPlaceholder": "",
"searchPlaceholder": "",
"send": ""
},
"inlineDialogFailure": {
"msg": "Имаше грешка.",
@@ -324,182 +329,181 @@
},
"keyboardShortcuts": {
"focusLocal": "Фокусиране върху Вашето видео",
"focusRemote": "Фокусирай видеото на друг участник",
"focusRemote": "Фокусиране върху видеото на друг участник",
"fullScreen": "Влизане/излизане от режим на цял екран",
"keyboardShortcuts": "Клавишни комбинации",
"localRecording": "Показване или скриване на контролите за локален запис",
"localRecording": "",
"mute": "Спиране/пускане на микрофона",
"pushToTalk": "Натиснете, за да говорите",
"raiseHand": "Вдигнете или свалете ръка",
"showSpeakerStats": "Показване на статистика за говорителя",
"toggleChat": "Отваряне/скриване на текстовите съобщения",
"toggleFilmstrip": "Показване или скриване на видео миниатюрите",
"toggleFilmstrip": "",
"toggleScreensharing": "Смяна между камера и споделен екран",
"toggleShortcuts": "Показване или скриване на клавишните комбинации",
"videoMute": "Пускане/спиране на камерата",
"videoQuality": "Управление на качество на обаждането"
"toggleShortcuts": "",
"videoMute": "Пускане/спиране на камерата"
},
"\u0005keyboardShortcuts": {},
"liveStreaming": {
"busy": "Работим върху това да освободим ресурси за излъчване. Моля, опитайте отново след няколко минути.",
"busyTitle": "Всички излъчватели в момента са заети.",
"changeSignIn": "Смяна на акаунти.",
"choose": "Изберете предаване на живо",
"chooseCTA": "Изберете опция за предаване. Влезли сте като {{email}}.",
"enterStreamKey": "Въведете ключа от YouTube за предаване на живо.",
"changeSignIn": "",
"choose": "",
"chooseCTA": "",
"enterStreamKey": "",
"error": "Излъчването на живо беше неуспешно. Моля, опитайте отново.",
"errorAPI": "Изникна проблем с връзката към YouTube. Моля опитайте отново.",
"errorLiveStreamNotEnabled": "Предаването на живо не е пуснато за {{email}}. Моля активирайте го или сменете акаунта.",
"expandedOff": "Предаването на живо бе спряно",
"expandedOn": "Срещата се излъчва на живо в YouTube.",
"expandedPending": "Излъчването на живо се стартира...",
"errorAPI": "",
"errorLiveStreamNotEnabled": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "Излъчването на живо не успя да започне",
"getStreamKeyManually": "Не успяхме да открием никакво предаване на живо. Опитайте да вземете ключа за такова от YouTube.",
"invalidStreamKey": "Ключът за предаване на живо е грешен.",
"getStreamKeyManually": "",
"invalidStreamKey": "",
"off": "Край на излъчването на живо",
"offBy": "{{name}} спря излъчването на живо",
"on": "Излъчване на живо",
"onBy": "{{name}} пусна излъчване на живо",
"pending": "Започване на излъчването на живо…",
"serviceName": "Предаване на живо",
"signedInAs": "В момента сте влезли като:",
"signIn": "Влезте с Гугъл",
"signInCTA": "Влезте или въведете ключът за излъчване на живо от YouTube.",
"serviceName": "",
"signedInAs": "",
"signIn": "",
"signInCTA": "",
"signOut": "",
"start": "Започни излъчване на живо",
"streamIdHelp": "Какво е това?",
"start": "Започване на излъчване на живо",
"streamIdHelp": "",
"unavailableTitle": "Излъчването на живо е недостъпно"
},
"\u0005liveStreaming": {},
"localRecording": {
"clientState": {
"off": "Изключено",
"on": "Включено",
"unknown": "Непознат"
"off": "",
"on": "",
"unknown": ""
},
"dialogTitle": "Управление на локален запис",
"duration": "Продължителност",
"durationNA": "Няма",
"encoding": "Кодек",
"label": "Етикет",
"labelToolTip": "Локалния запис е включен",
"localRecording": "Локален запис",
"dialogTitle": "",
"duration": "",
"durationNA": "",
"encoding": "",
"label": "",
"labelToolTip": "",
"localRecording": "",
"me": "Аз",
"messages": {
"engaged": "Локалния запис е включен.",
"finished": "Записа на сесията {{token}} приключи. Моля изпратете записа на вашият домакин.",
"finishedModerator": "Сесията {{token}} за запис приключи. Локалният запис беше запазен. Моля поканете останалите участници да ви изпратят техните записи.",
"notModerator": "Нямате права да пускате спирате локален запис."
"engaged": "",
"finished": "",
"finishedModerator": "",
"notModerator": ""
},
"moderator": "Модератор",
"no": "Не",
"no": "",
"participant": "Участник",
"participantStats": "Статистика на участник",
"sessionToken": "Тоукън за сесията",
"start": "Започни запис",
"stop": "Спри записа",
"participantStats": "",
"sessionToken": "",
"start": "Край на записа",
"stop": "Край на записа",
"yes": "Да"
},
"\u0005localRecording": {},
"lockRoomPassword": "парола",
"lockRoomPasswordUppercase": "Парола",
"me": "аз",
"notify": {
"connectedOneMember": "{{name}} влезна в срещата",
"connectedThreePlusMembers": "{{name}} и още {{count}} влезнаха в срещата",
"connectedTwoMembers": "{{first}} и {{second}} влезнаха в срещата",
"disconnected": "Напусна срещата",
"connectedOneMember": "",
"connectedThreePlusMembers": "",
"connectedTwoMembers": "",
"disconnected": "Връзка:",
"focus": "Конферентен фокус",
"focusFail": "{{component}} не е на раположение - следващ опит след {{ms}} секунди",
"focusFail": "{{component}} не е на раположения - следващ опит след {{ms}} секунди",
"grantedTo": "Даване на роля модератор на {{to}}!",
"invitedOneMember": "{{name}} бе поканен",
"invitedThreePlusMembers": "{{name}} и още {{count}} бяха поканени",
"invitedTwoMembers": "{{first}} и {{second}} бяха поканени",
"kickParticipant": "{{kicked}} беше изгонен от {{kicker}}",
"invitedOneMember": "",
"invitedThreePlusMembers": "",
"invitedTwoMembers": "",
"kickParticipant": "",
"me": "Аз",
"moderator": "Придобихте права на модератор!",
"muted": "Започвате разговора без звук.",
"mutedTitle": "Звукът ви е спрян!",
"mutedRemotelyTitle": "Микрофонът ви бе спрян от {{participantDisplayName}}!",
"mutedRemotelyDescription": "Винаги можете да пуснете микрофона си когато сте готови да говорите. Заглушете го отново за да не изпращате шум в срещата.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) е премахната от друг потребител",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) създадена от друг потребител",
"raisedHand": "{{name}} иска думата.",
"mutedRemotelyTitle": "",
"mutedRemotelyDescription": "",
"passwordRemovedRemotely": "",
"passwordSetRemotely": "",
"raisedHand": "",
"somebody": "Някой",
"startSilentTitle": "Влязохте с опция да не чувате аудио!",
"startSilentDescription": "Влезте повторно за да пуснете звука",
"suboptimalBrowserWarning": "Опасяваме се, че няма да можете да се насладите на срещата. Работим по въпроса, междувременно използвайте някой от <a href='static/recommendedBrowsers.html' target='_blank'>напълно поддържаните браузъри</a>.",
"suboptimalExperienceTitle": "Внимание",
"unmute": "Пускане на микрофона",
"newDeviceCameraTitle": "Засечена е нова камера",
"newDeviceAudioTitle": "Ново аудио устройство е засечено",
"newDeviceAction": "Използвай"
"startSilentTitle": "",
"startSilentDescription": "",
"suboptimalExperienceDescription": "",
"suboptimalExperienceTitle": "",
"unmute": "",
"newDeviceCameraTitle": "",
"newDeviceAudioTitle": "",
"newDeviceAction": ""
},
"passwordSetRemotely": "зададена от друг участник",
"passwordDigitsOnly": "До {{number}} цифри",
"passwordSetRemotely": "",
"passwordDigitsOnly": "",
"poweredby": "с подкрепата на",
"presenceStatus": {
"busy": "Зает",
"calling": "Обаждане...",
"busy": "",
"calling": "",
"connected": "Свързан",
"connecting": "Свързване...",
"connecting2": "Свързване*...",
"connecting": "Свързване",
"connecting2": "Свързване",
"disconnected": "Изключен",
"expired": "Изтекъл",
"ignored": "Пренебрегнат",
"initializingCall": "Свързване на обаждането...",
"invited": "Поканен",
"rejected": "Отхвърлен",
"ringing": "Звъни..."
"expired": "",
"ignored": "",
"initializingCall": "",
"invited": "Покани",
"rejected": "",
"ringing": ""
},
"\u0005presenceStatus": {},
"profile": {
"setDisplayNameLabel": "Задайте екранното си име",
"setEmailInput": "Въведете е-поща",
"setEmailLabel": "Задайте е-пощата си в „gravatar“",
"title": "Профил"
},
"raisedHand": "Иска думата",
"recording": {
"authDropboxText": "Качете в Dropbox",
"availableSpace": "Налично място: {{spaceLeft}} MB (приблизително {{duration}} минути запис)",
"beta": "БЕТА",
"authDropboxText": "",
"availableSpace": "",
"beta": "",
"busy": "Работим върху това да освободим ресурси за запис. Моля, опитайте отново след няколко минути.",
"busyTitle": "Всички възможности за запис в момента са заети",
"error": "Грешка при опит за запис. Моля опитайте отново.",
"expandedOff": "Записът спря",
"expandedOn": "Срещата се записва в момента",
"expandedPending": "Записът започва...",
"expandedOff": "Записът спрян",
"expandedOn": "",
"expandedPending": "Записът започна",
"failedToStart": "Неуспешен опит за записване",
"fileSharingdescription": "Споделете записа с участниците в срещата",
"live": "На Живо",
"loggedIn": "Влезли сте като {{userName}}",
"fileSharingdescription": "",
"live": "",
"loggedIn": "",
"off": "Записът спрян",
"offBy": "{{name}} спря записът",
"on": "Запис",
"onBy": "{{name}} пусна запис",
"pending": "Стартира запис на срещата...",
"rec": "ЗАПИС",
"serviceDescription": "Записът ви ще се запише от специална записваща услуга",
"serviceName": "Записваща услуга",
"signIn": "Влизане",
"signOut": "Излизане",
"unavailable": "Упс! В момент {{serviceName}} е недостъпна. В момента се опитваме да решим проблема. Моля опитайте отново малко по-късно.",
"pending": "",
"rec": "",
"serviceDescription": "",
"serviceName": "",
"signIn": "",
"signOut": "",
"unavailable": "",
"unavailableTitle": "Записът е невъзможен"
},
"\u0005recording": {},
"sectionList": {
"pullToRefresh": "Издърпай за да се обнови"
"pullToRefresh": ""
},
"settings": {
"calendar": {
"about": "Календарната интеграция на {{appName}} сигурно достъпва вашия календар за да покаже настъпващите събития.",
"disconnect": "Разкачи",
"microsoftSignIn": "Влез с Microsoft акаунт",
"signedIn": "В момента достъпва календара с {{email}}. Натиснете бутона Разкачи за да спрете достъпа.",
"about": "",
"disconnect": "Изключен",
"microsoftSignIn": "",
"signedIn": "",
"title": ""
},
"devices": "Устройства",
"devices": "",
"followMe": "Всички ме следват",
"language": "Език",
"loggedIn": "Влезли сте като {{name}}",
"language": "",
"loggedIn": "",
"moderator": "Модератор",
"more": "Повече",
"more": "",
"name": "Име",
"noDevice": "Няма",
"selectAudioOutput": "Звуков изход",
@@ -509,24 +513,27 @@
"startVideoMuted": "Всички започват скрити",
"title": "Настройки"
},
"\u0005settings": {
"calendar": {}
},
"settingsView": {
"alertOk": "Потвърди",
"alertOk": "",
"alertTitle": "Внимание",
"alertURLText": "Въведената връзка за сървър е невалидна",
"buildInfoSection": "Информация за програмата",
"conferenceSection": "Конференция",
"displayName": "Име",
"email": "Поща",
"alertURLText": "",
"buildInfoSection": "",
"conferenceSection": "",
"displayName": "",
"email": "",
"header": "Настройки",
"profileSection": "Профил",
"serverURL": "Линк на сървъра",
"startWithAudioMuted": "Започни със спрян звук",
"startWithVideoMuted": "Започни със спряно видео",
"version": "Версия"
"serverURL": "",
"startWithAudioMuted": "",
"startWithVideoMuted": "",
"version": ""
},
"share": {
"dialInfoText": "\n\n=====\n\nИскате да наберете от телефона?\n\n{{defaultDialInNumber}}Използвайте този линк за повече номера\n{{dialInfoPageUrl}}",
"mainText": "Използвайте линка за да влезете в срещата:\n{{roomUrl}}"
"dialInfoText": "",
"mainText": ""
},
"speaker": "Говорещ",
"speakerStats": {
@@ -548,95 +555,99 @@
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "Пускане на режим само с звук",
"audioOnly": "",
"audioRoute": "",
"callQuality": "Управление на качестото на обаждането",
"cc": "Пускане на субтитри",
"chat": "Активиране на прозорец за съобщения",
"document": "Показване на споделен документ",
"callQuality": "",
"cc": "",
"chat": "",
"document": "Отваряне/затваряне на споделен документ",
"feedback": "",
"fullScreen": "Пускане/Спиране на изглед в цял екран",
"hangup": "Напускане на срещата",
"fullScreen": "",
"hangup": "",
"invite": "Поканете участници",
"kick": "Изгони участник",
"localRecording": "Показване на контроли за локален запис",
"lockRoom": "Смяна парола на среща",
"moreActions": "Показване на меню с повече опции",
"moreActionsMenu": "Меню с повече опции",
"mute": "Пускане/спиране на видеото",
"pip": "Пускане на Картина-в-Картина",
"kick": "",
"localRecording": "",
"lockRoom": "",
"moreActions": "",
"moreActionsMenu": "",
"mute": "",
"pip": "",
"profile": "Редактиране на профила",
"raiseHand": "Смяна искане на думата",
"recording": "Пускане/спиране на запис",
"remoteMute": "Заглуши участник",
"Settings": "Промяна на настройки",
"sharedvideo": "Споделяне на YouTube видео",
"raiseHand": "",
"recording": "",
"remoteMute": "",
"Settings": "",
"sharedvideo": "",
"shareRoom": "",
"shareYourScreen": "Споделяне на екрана",
"shortcuts": "Бързи клавиши",
"shareYourScreen": "",
"shortcuts": "",
"show": "",
"speakerStats": "Показване на статистики за участниците",
"speakerStats": "",
"tileView": "",
"toggleCamera": "",
"videomute": "Пускане/спиране на видеото",
"videoblur": "Пускане/спиране на замъгляване на видеото"
"videomute": "",
"videoblur": ""
},
"addPeople": "Добавяне на участници в разговора",
"audioOnlyOff": "Спиране режима с нисък трафик",
"audioOnlyOn": "Пускане режима с нисък трафик",
"audioRoute": "Изберете устройство за звук",
"audioOnlyOff": "",
"audioOnlyOn": "",
"audioRoute": "",
"authenticate": "Идентификация",
"callQuality": "Промяна качеството на видеото",
"callQuality": "",
"chat": "Отваряне/затваряне на текстовите съобщения",
"closeChat": "Затваряне на съобщенията",
"documentClose": "Затваряне на споделеният документ",
"documentOpen": "Отваряне на споделен документ",
"enterFullScreen": "Вижте на цял екран",
"enterTileView": "Влизане в изглед галерия",
"exitFullScreen": "Изход от цял екран",
"exitTileView": "Спиране на изглед галерия",
"feedback": "Отзиви",
"closeChat": "",
"documentClose": "Отваряне/затваряне на споделен документ",
"documentOpen": "Отваряне/затваряне на споделен документ",
"enterFullScreen": "",
"enterTileView": "",
"exitFullScreen": "",
"exitTileView": "",
"feedback": "",
"hangup": "Напускане",
"invite": "Поканете участници",
"login": "Влез",
"logout": "Изход",
"lowerYourHand": "Махни искането на думата",
"moreActions": "Още опции",
"lowerYourHand": "",
"moreActions": "",
"mute": "Спиране/пускане на микрофона",
"openChat": "Отвори съобщенията",
"pip": "Пусни Картина-в-Картина",
"openChat": "",
"pip": "",
"profile": "Редактиране на профила",
"raiseHand": "Вдигане/сваляне на ръка",
"raiseYourHand": "Поискай думата",
"raiseYourHand": "Вдигни ръка.",
"Settings": "Настройки",
"sharedvideo": "Пускане/спиране на споделянето на екрана",
"shareRoom": "Добавете някого",
"shortcuts": "Виж бързите клавиши",
"speakerStats": "Статистика за говорителите",
"startScreenSharing": "Започни споделяне на екрана",
"startSubtitles": "Пускане на субтитри",
"stopScreenSharing": "Спиране споделяне на екрана",
"stopSubtitles": "Спиране на субтитри",
"stopSharedVideo": "Спиране на YouTube видео",
"shareRoom": "",
"shortcuts": "",
"speakerStats": "Статистика на говорителя",
"startScreenSharing": "",
"startSubtitles": "",
"stopScreenSharing": "",
"stopSubtitles": "",
"stopSharedVideo": "",
"talkWhileMutedPopup": "Опитвате се да говорите? В момента микрофонът Ви е заглушен.",
"tileViewToggle": "Превключване на изглед галерия",
"toggleCamera": "Пускане/спиране на камера",
"tileViewToggle": "",
"toggleCamera": "",
"videomute": "Пускане/спиране на камерата",
"startvideoblur": "Замъгли фона ми",
"stopvideoblur": "Спиране замъгляването на фона"
"startvideoblur": "",
"stopvideoblur": ""
},
"\u0005toolbar": {
"accessibilityLabel": {}
},
"transcribing": {
"ccButtonTooltip": "Пускане / Спиране на субтитри",
"error": "Грешка при опит за транскрибиране. Моля опитайте отново.",
"expandedLabel": "Транскрибирането е пуснато",
"failedToStart": "Транскрибирането не успя при пускане",
"labelToolTip": "Тази среща се транскрибира",
"off": "Транскрибирането спря",
"pending": "Стартира се транскрибиране на срещата...",
"start": "Започва показване на субтитри",
"stop": "Спира показване на субтитри",
"tr": "СУБ"
"ccButtonTooltip": "",
"error": "Грешка при опит за запис. Моля опитайте отново.",
"expandedLabel": "",
"failedToStart": "",
"labelToolTip": "",
"off": "",
"pending": "",
"start": "",
"stop": "",
"tr": ""
},
"\u0005transcribing": {},
"userMedia": {
"androidGrantPermissions": "Изберете <b><i>Разрешаване</i></b>, когато браузърът Ви помоли за разрешение.",
"chromeGrantPermissions": "Изберете <b><i>Разрешаване</i></b>, когато браузърът Ви помоли за разрешение.",
@@ -650,34 +661,31 @@
"safariGrantPermissions": "Изберете <b><i>Добре</i></b>, когато браузърът Ви помоли за разрешение."
},
"videoSIPGW": {
"busy": "Работим по освобождаване на ресурси. Моля, опитайте след няколко минути.",
"busyTitle": "Услугата за стаи в момента е заета",
"errorAlreadyInvited": "{{displayName}} вече е поканен",
"errorInvite": "Конференцията не е стартирана. Моля, опитайте по-късно.",
"errorInviteFailed": "Работим по разрешаването на проблем. Моля, опитайте по-късно.",
"errorInviteFailedTitle": "Добавянето на {{displayName}} не успя",
"errorInviteTitle": "Грешка при добавяне на стая",
"pending": "{{displayName}} бе поканен"
"busy": "",
"busyTitle": "",
"errorAlreadyInvited": "",
"errorInvite": "",
"errorInviteFailed": "",
"errorInviteFailedTitle": "",
"errorInviteTitle": "",
"pending": ""
},
"videoStatus": {
"audioOnly": "АУДИО",
"audioOnlyExpanded": "Вие сте в режим на нисък трафик. В този режим ще получавате само аудио или споделени екрани.",
"callQuality": "Качество на видеото",
"audioOnly": "",
"audioOnlyExpanded": "",
"callQuality": "",
"hd": "ВК",
"hdTooltip": "Гледате високо качество на видеото",
"highDefinition": "Високо качество",
"labelTooiltipNoVideo": "Няма видео",
"labelTooltipAudioOnly": "Пуснат режим на нисък трафик",
"labelTooiltipNoVideo": "",
"labelTooltipAudioOnly": "Включен е режим само със звук",
"ld": "НК",
"ldTooltip": "Виждате ниско качество на видеото",
"lowDefinition": "Ниско качество",
"onlyAudioAvailable": "Само аудио е налично",
"onlyAudioSupported": "Този браузър поддържа само аудио.",
"onlyAudioAvailable": "",
"onlyAudioSupported": "",
"p2pEnabled": "Вкл. директно свързване",
"p2pVideoQualityDescription": "В директна връзка, получаваното качество може да се сменя между високо и само аудио. Останалите настройки ще са достъпни когато връзката не е директна.",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "Ще се предпочита високо качество.",
"sd": "СК",
"sdTooltip": "Гледате стандартно качество на видеото",
"standardDefinition": "Стандартно качество"
},
"videothumbnail": {
@@ -685,39 +693,38 @@
"flip": "Огледално",
"kick": "Изгони",
"moderator": "Модератор",
"mute": "Участника е с изключен микрофон",
"mute": "Учасника е с изключен микрофон",
"muted": "Изключен микрофон",
"remoteControl": "Отдалечено управление",
"show": "Покажи на главния екран",
"videomute": "Участник е спрял камерата си"
"show": "",
"videomute": ""
},
"welcomepage": {
"accessibilityLabel": {
"join": "Натиснете за да влезете",
"join": "",
"roomname": "Въведете име на стаята"
},
"appDescription": "Хайде на видео разговорите с целият ви екип. Всъщност, поканете всички познати. {{app}} е напълно защитено, 100% решение за видеоконференции с отворен код което може да ползвате по цял ден, всеки ден, безплатно - без да ви е нужна регистрация.",
"appDescription": "",
"audioVideoSwitch": {
"audio": "Глас",
"video": "Видео"
"audio": "",
"video": ""
},
"calendar": "Календар",
"connectCalendarButton": "Свържете вашия календар",
"connectCalendarText": "Свържете вашия календар за да видите срещите си в {{app}}. Добавяйки {{provider}} срещите в календара си ще можете да ги старирате с едно докосване.",
"enterRoomTitle": "Започни нова среща",
"onlyAsciiAllowed": "Името на срещата може да съдържа само латински букви и цифри.",
"calendar": "",
"connectCalendarButton": "",
"connectCalendarText": "",
"enterRoomTitle": "",
"go": "НАПРЕД",
"join": "ПРИСЪЕДИНЯВАНЕ",
"info": "Информация",
"info": "",
"privacy": "Поверителност",
"recentList": "Скорошни срещи",
"recentListDelete": "Изтрий",
"recentListEmpty": "Списъка с скорошни срещи е празен. След като участвате в някоя среща, ще я намерите тук.",
"reducedUIText": "Добре дошли в {{app}}!",
"recentList": "",
"recentListDelete": "",
"recentListEmpty": "",
"reducedUIText": "",
"roomname": "Въведете име на стаята",
"roomnameHint": "Въведете името или връзката на стаята в която искате да влезете. Също може да си измислите име. Само го споделете с някой, за да може и той да въведе същото име за да се срещнете.",
"roomnameHint": "",
"sendFeedback": "Изпращане на отзиви",
"terms": "Условия",
"title": "Сигурна, с много възможности, и напълно безплатна платформа за видео конференции"
"title": ""
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
"headphones": "Escotadors",
"phone": "Telefòn",
"speaker": "Nautparlaire",
"none": "Cap de periferic àudio pas disponible"
"none": ""
},
"audioOnly": {
"audioOnly": "Benda passanta febla"
@@ -182,7 +182,7 @@
"liveStreamingDisabledForGuestTooltip": "Los convidats pòdon pas aviar una difusion en dirècte",
"liveStreamingDisabledTooltip": "Difusion en dirècte desactivada.",
"lockMessage": "Impossible de verrolhar la conferéncia.",
"lockRoom": "Ajustar un $t(lockRoomPasswordUppercase) a la conferéncia",
"lockRoom": "",
"lockTitle": "Fracàs del verrolhatge",
"logoutQuestion": "Sètz segur que vos volètz desconnectar e arrestar la conferéncia ?",
"logoutTitle": "Desconnexion",
@@ -199,10 +199,10 @@
"muteParticipantDialog": "",
"muteParticipantTitle": "Copar lo micro als participants ?",
"Ok": "D'acòrdi",
"passwordLabel": "SENHAL",
"passwordNotSupported": "Ajustar un $t(lockRoomPassword) a una conferéncia es pas suportat.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) pas suportat",
"passwordRequired": "$t(lockRoomPasswordUppercase) requesit",
"passwordLabel": "",
"passwordNotSupported": "Ajustar un senhal a una conferéncia es pas suportat.",
"passwordNotSupportedTitle": "",
"passwordRequired": "",
"popupError": "Vòstre navigator bloca las fenèstras que sorgisson a partir d'aqueste site. Mercés d'activar aquelas fenèstras dins los paramètres de vòstre navigator e de tornar ensajar.",
"popupErrorTitle": "Fenèstra que sorgís blocada",
"recording": "Enregistrament",
@@ -217,7 +217,7 @@
"remoteControlStopMessage": "La session de contraròtle alonhat es acabada !",
"remoteControlTitle": "Contraròtle a distància",
"Remove": "Suprimir",
"removePassword": "Suprimir lo",
"removePassword": "",
"removeSharedVideoMsg": "Sètz segur que volètz suprimir vòstra vidèo partejada ?",
"removeSharedVideoTitle": "Suprimir la vidèo partejada",
"reservationError": "Error del sistèma de reservacion",
@@ -250,7 +250,7 @@
"tokenAuthFailed": "O planhèm, sètz pas autorizat a rejónher l'apèl.",
"tokenAuthFailedTitle": "Fracàs de l'autentificacion",
"transcribing": "Transcripcion",
"unlockRoom": "Suprimir lo $t(lockRoomPassword) de la conferéncia",
"unlockRoom": "",
"userPassword": "senhal utilizaire",
"WaitForHostMsg": "La conferéncia <b>{{room}}</b> a pas encara començat. Se sètz lòst volgatz ben vos identificar. Autrament esperatz quarribe lòste.",
"WaitForHostMsgWOk": "La conferéncia <b>{{room}}</b> a pas encara començat. Se sètz lòst volgatz ben clicar Ok per vos identificar. Autrament esperatz quarribe lòste.",
@@ -279,8 +279,8 @@
},
"info": {
"accessibilityLabel": "Mostrar las info",
"addPassword": "Ajustar un $t(lockRoomPassword)",
"cancelPassword": "Anullar lo $t(lockRoomPassword)",
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "Ligam:",
"country": "País",
"dialANumber": "Per participar a la conferéncia, sonatz un daquestes numèros puèi picatz lo senhal.",
@@ -355,9 +355,7 @@
"getStreamKeyManually": "",
"invalidStreamKey": "La clau de difusion en dirècte es benlèu pas corrècta.",
"off": "La difusion en dirècte es estada arrestada",
"offBy": "",
"on": "La difusion en dirècte es estada arrestada",
"onBy": "",
"pending": "Començar lo dirècte...",
"serviceName": "Servici de difusion en dirècte",
"signedInAs": "Sètz connectat coma :",
@@ -418,8 +416,8 @@
"mutedTitle": "Sètz en mut !",
"mutedRemotelyTitle": "{{participantDisplayName}} vos a mes en silenci !",
"mutedRemotelyDescription": "",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) tirat per un autre participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) definit per un autre participant",
"passwordRemovedRemotely": "",
"passwordSetRemotely": "",
"raisedHand": "{{name}} volriá parlar.",
"somebody": "Qualqu'un",
"startSilentTitle": "Avètz jonch sens cap de sortida àudio !",
@@ -470,9 +468,7 @@
"live": "DIRÈCTE",
"loggedIn": "Session a {{userName}}",
"off": "Enregistrament arrestar",
"offBy": "",
"on": "Enregistrament",
"onBy": "",
"pending": "Preparacion de lenregistrament de la conferéncia...",
"rec": "ENRG",
"serviceDescription": "Vòstre enregistrament serà salvagardat pel servici dedicat.",
@@ -512,7 +508,7 @@
"alertOk": "Dacòrdi",
"alertTitle": "Avertiment",
"alertURLText": "LURL del servidor es pas valida",
"buildInfoSection": "Informacions de generacion",
"buildInfoSection": "",
"conferenceSection": "Conferéncia",
"displayName": "Escais-nom",
"email": "Corrièl",
@@ -557,7 +553,7 @@
"fullScreen": "Passar al ecran complèt",
"hangup": "Quitar la sonada",
"invite": "Convidar de monde",
"kick": "Exclure un participan ",
"kick": "",
"localRecording": "Passar al panèl denregistraments locals",
"lockRoom": "Tirar lo senhal de la conferéncia",
"moreActions": "Passar al menú mai daccions",
@@ -567,7 +563,7 @@
"profile": "Modificar vòstre perfil",
"raiseHand": "Demandar la paraula",
"recording": "Passar al enregistraments",
"remoteMute": "Copar lo son del participant",
"remoteMute": "",
"Settings": "Passar als paramètres",
"sharedvideo": "Passar al partatge de vidèo Youtube",
"shareRoom": "Convidar qualquun",
@@ -581,8 +577,8 @@
"videoblur": ""
},
"addPeople": "Ajustar de monde a vòstra sonada",
"audioOnlyOff": "Desactivar lo mòde connexion febla",
"audioOnlyOn": "Activar lo mòde connexion febla",
"audioOnlyOff": "",
"audioOnlyOn": "",
"audioRoute": "Seleccionar lo periferic àudio",
"authenticate": "Autentificatz-vos",
"callQuality": "Gerir la qualitat vidèo",
@@ -591,9 +587,9 @@
"documentClose": "Tampar los documents partejats",
"documentOpen": "Dobrir los documents partejats",
"enterFullScreen": "Veire lecran complèt",
"enterTileView": "Dintrar dins la vista mosaïca",
"enterTileView": "",
"exitFullScreen": "Sortir de lecran complèt",
"exitTileView": "Quitar la vista mosaïca",
"exitTileView": "",
"feedback": "Daissar un comentari",
"hangup": "Quitar",
"invite": "Convidar de monde",
@@ -621,8 +617,8 @@
"tileViewToggle": "Activar/Desactivar la vista en mosaïc",
"toggleCamera": "Passar a la camèra",
"videomute": "Aviar / Arrestar la camèra",
"startvideoblur": "Trebolar mon rèire-plan",
"stopvideoblur": "Desactivar lo borrolatge del rèire-plan"
"startvideoblur": "",
"stopvideoblur": ""
},
"transcribing": {
"ccButtonTooltip": "Aviar / Arrestat los sostítols",
@@ -660,13 +656,13 @@
},
"videoStatus": {
"audioOnly": "AUD",
"audioOnlyExpanded": "Sètz en mòde connexion febla. Amb aqueste mòde recebretz pas que làudio e lo partatge decran.",
"audioOnlyExpanded": "",
"callQuality": "Qualitat vidèo",
"hd": "HD",
"hdTooltip": "Difusion vidèo en nauta definicion",
"highDefinition": "Nauta definicion",
"labelTooiltipNoVideo": "Pas cap de vidèo",
"labelTooltipAudioOnly": "Mòde connexion febla activat",
"labelTooltipAudioOnly": "",
"ld": "Bassa definicion",
"ldTooltip": "Difusion vidèo en bassa definicion",
"lowDefinition": "Bassa definicion",
@@ -688,7 +684,7 @@
"muted": "Mut",
"remoteControl": "Contraròtle alonhat",
"show": "",
"videomute": "Lo participant a arrestat la camèra"
"videomute": ""
},
"welcomepage": {
"accessibilityLabel": {
@@ -702,12 +698,11 @@
},
"calendar": "Calendari",
"connectCalendarButton": "Connectar lo calendari",
"connectCalendarText": "Connectatz vòstre calendièr per veire vòstras reünions dins {{app}}. Ajustatz tanben las reünions de {{provider}} a vòstre calendièr e aviatz-las amb un sol clic.",
"connectCalendarText": "",
"enterRoomTitle": "Començar una nòva conferéncia",
"onlyAsciiAllowed": "",
"go": "Crear",
"join": "PARTICIPATZ",
"info": "Infor",
"info": "",
"privacy": "Vida privada",
"recentList": "Recents",
"recentListDelete": "Suprimits",

File diff suppressed because it is too large Load Diff

View File

@@ -21,11 +21,10 @@
"bluetooth": "Bluetooth",
"headphones": "Fones de ouvido",
"phone": "Celular",
"speaker": "Alto-falantes",
"none": ""
"speaker": "Apresentador"
},
"audioOnly": {
"audioOnly": "Largura de banda baixa"
"audioOnly": "Somente áudio"
},
"calendarSync": {
"addMeetingURL": "Adicionar um link da reunião",
@@ -50,9 +49,9 @@
"messagebox": "Digite uma mensagem",
"nickname": {
"popover": "Escolha um apelido",
"title": "Digite um apelido para usar o bate-papo"
"title": "Digite um apelido para usar o chat"
},
"title": "Bate-papo"
"title": "Chat"
},
"connectingOverlay": {
"joiningRoom": "Conectando você à reunião…"
@@ -97,7 +96,8 @@
"resolution": "Resolução:",
"status": "Conexão:",
"transport": "Transporte:",
"transport_plural": "Transportes:"
"transport_plural": "Transportes:",
"turn": " (virar)"
},
"dateUtils": {
"earlier": "Mais cedo",
@@ -107,7 +107,7 @@
"deepLinking": {
"appNotInstalled": "Você precisa do aplicativo móvel {{app}} para participar da reunião no seu telefone.",
"description": "Nada acontece? Estamos tentando iniciar sua reunião no aplicativo desktop {{app}}. Tente novamente ou inicie ele na aplicação web {{app}}.",
"descriptionWithoutWeb": "Nada aconteceu? Tentamos iniciar sua reunião no aplicativo de desktop {{app}}.",
"descriptionWithoutWeb": "",
"downloadApp": "Baixe o Aplicativo",
"launchWebButton": "Iniciar na web",
"openApp": "Continue na aplicação",
@@ -115,7 +115,6 @@
"tryAgainButton": "Tente novamente no desktop"
},
"defaultLink": "ex.: {{url}}",
"defaultNickname": "ex.: João Pedro",
"deviceError": {
"cameraError": "Falha ao acessar sua câmera",
"cameraPermission": "Erro ao obter permissão para a câmera",
@@ -133,7 +132,7 @@
"liveStreaming": "Transmissão ao vivo"
},
"allow": "Permitir",
"alreadySharedVideoMsg": "Outro participante já está compartilhando um vídeo. Esta conferência permite apenas um vídeo compartilhado por vez.",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "Somente um vídeo compartilhado é permitido por vez",
"applicationWindow": "Janela de aplicativo",
"Back": "Voltar",
@@ -159,51 +158,51 @@
"contactSupport": "Contate o suporte",
"copy": "Copiar",
"dismiss": "Dispensar",
"displayNameRequired": "Oi! Qual o seu nome?",
"displayNameRequired": "",
"done": "Feito",
"enterDisplayName": "Digite seu nome aqui",
"enterDisplayName": "",
"error": "Erro",
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela.",
"externalInstallationTitle": "Extensão requerida",
"goToStore": "Vá para a loja virtual",
"gracefulShutdown": "O sistema está em manutenção. Por favor tente novamente mais tarde.",
"IamHost": "Eu sou o anfitrião",
"incorrectRoomLockPassword": "Senha incorreta",
"incorrectRoomLockPassword": "",
"incorrectPassword": "Usuário ou senha incorretos",
"inlineInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela.",
"inlineInstallExtension": "Instalar agora",
"internalError": "Oops! Alguma coisa está errada. O seguinte erro ocorreu: {{error}}",
"internalErrorTitle": "Erro interno",
"kickMessage": "Você pode contatar com {{participantDisplayName}} para obter mais detalhes.",
"kickMessage": "",
"kickParticipantButton": "Remover",
"kickParticipantDialog": "Tem certeza de que deseja remover este participante?",
"kickParticipantTitle": "Chutar este participante?",
"kickTitle": "Ai! {{participantDisplayName}} expulsou você da reunião",
"kickParticipantTitle": "Deixar mudo este participante?",
"kickTitle": "",
"liveStreaming": "Transmissão ao Vivo",
"liveStreamingDisabledForGuestTooltip": "Visitantes não podem iniciar transmissão ao vivo.",
"liveStreamingDisabledTooltip": "Iniciar transmissão ao vivo desativada.",
"lockMessage": "Falha ao travar a conferência.",
"lockRoom": "Adicionar reunião $t(lockRoomPasswordUppercase)",
"lockRoom": "",
"lockTitle": "Bloqueio falhou",
"logoutQuestion": "Deseja encerrar a sessão e finalizar a conferência?",
"logoutTitle": "Encerrar sessão",
"maxUsersLimitReached": "O limite para o número máximo de participantes foi atingido. A conferência está cheia. Entre em contato com o proprietário da reunião ou tente novamente mais tarde!",
"maxUsersLimitReachedTitle": "Limite máximo de participantes atingido",
"maxUsersLimitReached": "",
"maxUsersLimitReachedTitle": "",
"micConstraintFailedError": "Seu microfone não satisfaz algumas condições necessárias.",
"micNotFoundError": "O microfone não foi encontrado.",
"micNotSendingData": "Vá para as configurações do seu computador para ativar o som do microfone e ajustar seu nível",
"micNotSendingDataTitle": "Seu microfone está mudo pelas configurações do sistema",
"micNotSendingData": "",
"micNotSendingDataTitle": "",
"micPermissionDeniedError": "Não foi permitido acessar o seu microfone. Você ainda pode entrar na conferência, mas sem enviar áudio. Clique no botão do microfone para tentar reparar.",
"micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
"muteParticipantButton": "Mudo",
"muteParticipantDialog": "Tem certeza de que deseja silenciar este participante? Você não poderá desfazer isso, mas o participante pode reabilitar o áudio a qualquer momento.",
"muteParticipantDialog": "Tem certeza de que deseja silenciar este participante? Você não poderá desativar a opção silenciar dele, mas ele poderá fazer isso quando desejar.",
"muteParticipantTitle": "Deixar mudo este participante?",
"Ok": "Ok",
"passwordLabel": "$t(lockRoomPasswordUppercase)",
"passwordNotSupported": "A configuração de uma reunião $t(lockRoomPassword) não é suportada.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) não suportado",
"passwordRequired": "$t(lockRoomPasswordUppercase) requerido",
"passwordLabel": "",
"passwordNotSupported": "Configuração de senha para a reunião não é suportada.",
"passwordNotSupportedTitle": "",
"passwordRequired": "",
"popupError": "Seu navegador está bloqueando janelas popup deste site. Habilite os popups nas configurações de segurança no seu navegador e tente novamente.",
"popupErrorTitle": "Popup bloqueado",
"recording": "Gravando",
@@ -218,7 +217,7 @@
"remoteControlStopMessage": "A sessão de controle remoto terminou!",
"remoteControlTitle": "Conexão de área de trabalho remota",
"Remove": "Remover",
"removePassword": "Remove $t(lockRoomPassword)",
"removePassword": "",
"removeSharedVideoMsg": "Deseja remover seu vídeo compartilhado?",
"removeSharedVideoTitle": "Remover vídeo compartilhado",
"reservationError": "Erro de sistema de reserva",
@@ -251,7 +250,7 @@
"tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
"tokenAuthFailedTitle": "Falha de autenticação",
"transcribing": "Transcrevendo",
"unlockRoom": "Remove a reunião $t(lockRoomPassword)",
"unlockRoom": "",
"userPassword": "senha do usuário",
"WaitForHostMsg": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, faça a autenticação. Do contrário, aguarde a chegada do anfitrião.",
"WaitForHostMsgWOk": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, pressione Ok para autenticar. Do contrário, aguarde a chegada do anfitrião.",
@@ -280,8 +279,8 @@
},
"info": {
"accessibilityLabel": "Mostrar info",
"addPassword": "Adicione $t(lockRoomPassword)",
"cancelPassword": "Cancela $t(lockRoomPassword)",
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "Link:",
"country": "País",
"dialANumber": "Para entrar na reunião, disque um desses números e depois insira o PIN.",
@@ -292,18 +291,18 @@
"dialInTollFree": "Chamada gratuita",
"genericError": "Oops, alguma coisa deu errado.",
"inviteLiveStream": "Para ver a transmissão ao vivo da reunião, clique no link: {{url}}",
"invitePhone": "Para participar por telefone, toque aqui: {{number}} ,, {{conferenceID}} # \\ n",
"invitePhoneAlternatives": "Procurando um número de discagem diferente?\nVeja os números de discagem da reunião: {{url}} \n\n\nSe você também estiver discando através de um telefone da sala, participe sem conectar-se ao áudio: {{silentUrl}}",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURLFirstPartGeneral": "Você foi convidado para uma reunião.",
"inviteURLFirstPartPersonal": "{{name}} está convidando você para uma reunião.\n",
"inviteURLSecondPart": "\nEntre na reunião:\n{{url}}\n",
"inviteURLFirstPartPersonal": "",
"inviteURLSecondPart": "",
"liveStreamURL": "Transmissão ao vivo:",
"moreNumbers": "Mais números",
"noNumbers": "Sem números de discagem.",
"noPassword": "Nenhum",
"noRoom": "Nenhuma sala foi especificada para entrar.",
"numbers": "Números de discagem",
"password": "$t(lockRoomPasswordUppercase):",
"password": "",
"title": "Compartilhar",
"tooltip": "Compartilhar link e discagem para esta reunião",
"label": "Informações da reunião"
@@ -336,8 +335,7 @@
"toggleFilmstrip": "Mostrar ou ocultar miniaturas de vídeo",
"toggleScreensharing": "Trocar entre câmera e compartilhamento de tela",
"toggleShortcuts": "Mostrar ou ocultar atalhos de teclado",
"videoMute": "Iniciar ou parar sua câmera",
"videoQuality": "Gerenciar qualidade da chamada"
"videoMute": "Iniciar ou parar sua câmera"
},
"liveStreaming": {
"busy": "Estamos trabalhando para liberar os recursos de transmissão. Tente novamente em alguns minutos.",
@@ -351,17 +349,15 @@
"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.",
"expandedOff": "A transmissão ao vivo foi encerrada",
"expandedOn": "A reunião está sendo transmitida pelo YouTube.",
"expandedPending": "Iniciando a transmissão ao vivo...",
"expandedPending": "A transmissão ao vivo está sendo iniciada…",
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
"getStreamKeyManually": "Não conseguimos buscar nenhuma transmissão ao vivo. Tente obter sua chave de transmissão ao vivo no YouTube.",
"getStreamKeyManually": "",
"invalidStreamKey": "A senha para transmissão ao vivo pode estar incorreta.",
"off": "Transmissão ao vivo encerrada",
"offBy": "",
"on": "Transmissão ao Vivo",
"onBy": "",
"pending": "Iniciando Transmissão ao Vivo...",
"serviceName": "Serviço de Transmissão ao Vivo",
"signedInAs": "Você está conectado como:",
"signedInAs": "Você está conectado atualmente como:",
"signIn": "Faça login no Google",
"signInCTA": "Faça login ou insira sua chave de transmissão ao vivo do YouTube.",
"signOut": "Sair",
@@ -371,16 +367,16 @@
},
"localRecording": {
"clientState": {
"off": "Desligado",
"on": "Ligado",
"off": "Off",
"on": "On",
"unknown": "Desconhecido"
},
"dialogTitle": "Controles da Gravação Local",
"duration": "Duração",
"durationNA": "N/D",
"durationNA": "N/A",
"encoding": "Codificando",
"label": "LOR",
"labelToolTip": "Gravação local ativada",
"labelToolTip": "Gravação local está envolvida",
"localRecording": "Gravação local",
"me": "Eu",
"messages": {
@@ -410,30 +406,30 @@
"focusFail": "{{component}} não disponĩvel - tente em {{ms}} seg.",
"grantedTo": "Direitos de moderador concedido para {{to}}!",
"invitedOneMember": "{{displayName}} foi convidado",
"invitedThreePlusMembers": "{{name}} e {{count}} outros foram convidados",
"invitedTwoMembers": "{{first}} e {{second}} foram convidados",
"kickParticipant": "{{kicked}} foi chutado por {{kicker}}",
"invitedThreePlusMembers": "",
"invitedTwoMembers": "",
"kickParticipant": "",
"me": "Eu",
"moderator": "Direitos de moderador concedidos!",
"muted": "Você iniciou uma conversa em mudo.",
"mutedTitle": "Você está mudo!",
"mutedRemotelyTitle": "Você foi silenciado por {{participantDisplayName}}!",
"mutedRemotelyDescription": "Você sempre pode ativar o som quando estiver pronto para falar. Retire o som quando terminar para manter o ruído longe da reunião.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) definido por outro participante",
"mutedRemotelyTitle": "",
"mutedRemotelyDescription": "",
"passwordRemovedRemotely": "",
"passwordSetRemotely": "",
"raisedHand": "{{name}} gostaria de falar.",
"somebody": "Alguém",
"startSilentTitle": "Você entrou sem saída de áudio!",
"startSilentDescription": "Volte à reunião para habilitar o áudio",
"suboptimalBrowserWarning": "",
"startSilentTitle": "",
"startSilentDescription": "",
"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>.",
"suboptimalExperienceTitle": "Alerta do navegador",
"unmute": "Ativar som",
"unmute": "",
"newDeviceCameraTitle": "Nova câmera detectada",
"newDeviceAudioTitle": "Novo dispositivo de áudio detectado",
"newDeviceAction": "Usar"
},
"passwordSetRemotely": "Definido por outro participante",
"passwordDigitsOnly": "Até {{number}} dígitos",
"passwordDigitsOnly": "",
"poweredby": "distribuído por",
"presenceStatus": {
"busy": "Ocupado",
@@ -447,7 +443,7 @@
"initializingCall": "Iniciando Chamada...",
"invited": "Convidar",
"rejected": "Rejeitado",
"ringing": "Tocando..."
"ringing": "Chamando..."
},
"profile": {
"setDisplayNameLabel": "Definir seu nome de exibição",
@@ -455,7 +451,6 @@
"setEmailLabel": "Definir seu email de gravatar",
"title": "Perfil"
},
"raisedHand": "Gostaria de falar",
"recording": {
"authDropboxText": "Enviar para o Dropbox.",
"availableSpace": "Espaço disponível: {{spaceLeft}} MB (aproximadamente {{duration}} minutos de gravação)",
@@ -471,14 +466,12 @@
"live": "AOVIVO",
"loggedIn": "Conectado como {{userName}}",
"off": "Gravação parada",
"offBy": "",
"on": "Gravando",
"onBy": "",
"pending": "Preparando para gravar a reunião...",
"rec": "REC",
"serviceDescription": "Sua gravação será salva pelo serviço de gravação",
"serviceName": "Serviço de gravação",
"signIn": "Entrar",
"signIn": "entrar",
"signOut": "Sair",
"unavailable": "Oops! O {{serviceName}} está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"unavailableTitle": "Gravação indisponível"
@@ -525,10 +518,10 @@
"version": "Versão"
},
"share": {
"dialInfoText": "\n\n=====\n\nDeseja apenas discar no seu telefone?\n\n{{defaultDialInNumber}}Clique neste link para ver os números de telefone para esta reunião\n{{dialInfoPageUrl}}",
"dialInfoText": "",
"mainText": "Clique no seguinte link para entrar na reunião:{{roomUrl}}\n"
},
"speaker": "Alto-falantes",
"speaker": "Apresentador",
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
@@ -550,7 +543,7 @@
"accessibilityLabel": {
"audioOnly": "Alternar para apenas áudio",
"audioRoute": "Selecionar o dispositivo de som",
"callQuality": "Gerenciar qualidade do vídeo",
"callQuality": "Gerenciar qualidade da chamada",
"cc": "Alternar legendas",
"chat": "Alternar para janela de chat",
"document": "Alternar para documento compartilhado",
@@ -574,19 +567,19 @@
"shareRoom": "Convidar alguém",
"shareYourScreen": "Alternar compartilhamento de tela",
"shortcuts": "Alternar atalhos",
"show": "Mostrar no palco",
"show": "",
"speakerStats": "Alternar estatísticas do apresentador",
"tileView": "Alternar visualização em blocos",
"toggleCamera": "Alternar câmera",
"videomute": "Alternar mudo do vídeo",
"videoblur": "Alternar desfoque de vídeo"
"videoblur": ""
},
"addPeople": "Adicionar pessoas à sua chamada",
"audioOnlyOff": "",
"audioOnlyOn": "",
"audioOnlyOff": "Desativar modo somente áudio",
"audioOnlyOn": "Desativar modo somente áudio",
"audioRoute": "Selecionar o dispositivo de som",
"authenticate": "Autenticar",
"callQuality": "Gerenciar qualidade do vídeo",
"callQuality": "Gerenciar qualidade da chamada",
"chat": "Abrir ou fechar o bate-papo",
"closeChat": "Fechar chat",
"documentClose": "Fechar documento compartilhado",
@@ -622,19 +615,19 @@
"tileViewToggle": "Alternar visualização em blocos",
"toggleCamera": "Alternar câmera",
"videomute": "Iniciar ou parar a câmera",
"startvideoblur": "Desfocar meu plano de fundo",
"stopvideoblur": "Desativar desfoque de fundo"
"startvideoblur": "",
"stopvideoblur": ""
},
"transcribing": {
"ccButtonTooltip": "Iniciar/parar legendas",
"error": "Transcrição falhou. Tente novamente.",
"expandedLabel": "Transcrição ativada",
"expandedLabel": "Transcrição ligada",
"failedToStart": "Transcrição falhou ao iniciar",
"labelToolTip": "A reunião esta sendo transcrita",
"off": "Transcrição parada",
"pending": "Preparando a transcrição da reunião...",
"start": "Exibir legendas",
"stop": "Não exibir legendas",
"start": "Iniciar / Parar de mostrar as legendas",
"stop": "Iniciar / Parar de mostrar as legendas",
"tr": "TR"
},
"userMedia": {
@@ -661,23 +654,20 @@
},
"videoStatus": {
"audioOnly": "AUD",
"audioOnlyExpanded": "",
"callQuality": "Qualidade de vídeo",
"audioOnlyExpanded": "Você está no modo somente áudio. Esse modo economiza internet mas não permite ver o vídeo dos outros.",
"callQuality": "",
"hd": "HD",
"hdTooltip": "Ver vídeo em alta definição",
"highDefinition": "Alta definição (HD)",
"labelTooiltipNoVideo": "Sem vídeo",
"labelTooltipAudioOnly": "",
"labelTooltipAudioOnly": "Modo somente de áudio habilitado",
"ld": "LD",
"ldTooltip": "Ver vídeo em baixa definição",
"lowDefinition": "Baixa definição (LD)",
"onlyAudioAvailable": "Somente áudio disponível",
"onlyAudioSupported": "Suportamos somente áudio neste navegador.",
"p2pEnabled": "Ponto-a-ponto habilitada",
"p2pVideoQualityDescription": "No modo ponto a ponto, a qualidade do vídeo recebido só pode ser alternada entre alta e apenas áudio. Outras configurações não serão respeitadas até que o ponto a ponto seja encerrado.",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "Preferência para alta definição",
"sd": "SD",
"sdTooltip": "Ver vídeo em definição padrão",
"standardDefinition": "Definição padrão"
},
"videothumbnail": {
@@ -688,8 +678,8 @@
"mute": "Participante está mudo",
"muted": "Mudo",
"remoteControl": "Controle remoto",
"show": "Mostrar no palco",
"videomute": "O participante parou a câmera"
"show": "",
"videomute": ""
},
"welcomepage": {
"accessibilityLabel": {
@@ -705,7 +695,6 @@
"connectCalendarButton": "Conectar seu calendário",
"connectCalendarText": "Conecte seu calendário para ver todas as reuniões em {{app}}. Além disso, adicione reuniões de {{provider}} ao seu calendário e inicie-as com apenas um clique.",
"enterRoomTitle": "Iniciar uma nova reunião",
"onlyAsciiAllowed": "",
"go": "IR",
"join": "Entrar",
"info": "Informações",
@@ -713,7 +702,7 @@
"recentList": "Recente",
"recentListDelete": "Remover",
"recentListEmpty": "Sua lista recente está vazia. As reuniões que você realizar serão exibidas aqui.",
"reducedUIText": "Bem-vindo ao {{app}}!",
"reducedUIText": "",
"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",

View File

@@ -46,16 +46,13 @@
"today": "Today"
},
"chat": {
"error": "Error: your message was not sent. Reason: {{error}}",
"error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
"messagebox": "Type a message",
"messageTo": "Private message to {{recipient}}",
"nickname": {
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat"
},
"privateNotice": "Private message to {{recipient}}",
"title": "Chat",
"you": "you"
"title": "Chat"
},
"connectingOverlay": {
"joiningRoom": "Connecting you to your meeting..."
@@ -238,10 +235,6 @@
"screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
"screenSharingFirefoxPermissionDeniedTitle": "Oops! We werent able to start screen sharing!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
"sendPrivateMessage": "You recently received a private message. Did you intend to reply to that privately, or you want to send your message to the group?",
"sendPrivateMessageCancel": "Send to the group",
"sendPrivateMessageOk": "Send privately",
"sendPrivateMessageTitle": "Send privately?",
"serviceUnavailable": "Service unavailable",
"sessTerminated": "Call terminated",
"Share": "Share",
@@ -275,9 +268,6 @@
"dialOut": {
"statusMessage": "is now {{status}}"
},
"documentSharing" : {
"title": "Shared Document"
},
"feedback": {
"average": "Average",
"bad": "Bad",
@@ -372,9 +362,7 @@
"getStreamKeyManually": "We werent able to fetch any live streams. Try getting your live stream key from YouTube.",
"invalidStreamKey": "Live stream key may be incorrect.",
"off": "Live Streaming stopped",
"offBy": "{{name}} stopped the live streaming",
"on": "Live Streaming",
"onBy": "{{name}} started the live streaming",
"pending": "Starting Live Stream...",
"serviceName": "Live Streaming service",
"signedInAs": "You are currently signed in as:",
@@ -487,9 +475,7 @@
"live": "LIVE",
"loggedIn": "Logged in as {{userName}}",
"off": "Recording stopped",
"offBy": "{{name}} stopped the recording",
"on": "Recording",
"onBy": "{{name}} started the recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"serviceDescription": "Your recording will be saved by the recording service",
@@ -573,7 +559,6 @@
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
"help": "Help",
"invite": "Invite people",
"kick": "Kick participant",
"localRecording": "Toggle local recording controls",
@@ -582,7 +567,6 @@
"moreActionsMenu": "More actions menu",
"mute": "Toggle mute audio",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Toggle raise hand",
"recording": "Toggle recording",
@@ -615,7 +599,6 @@
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"hangup": "Leave",
"help": "Help",
"invite": "Invite people",
"login": "Login",
"logout": "Logout",
@@ -624,7 +607,6 @@
"mute": "Mute / Unmute",
"openChat": "Open chat",
"pip": "Enter Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",

View File

@@ -15,7 +15,7 @@ import Filmstrip from './videolayout/Filmstrip';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
import { setNotificationsEnabled } from '../../react/features/notifications';
import {
@@ -240,12 +240,10 @@ UI.initEtherpad = name => {
return;
}
logger.log('Etherpad is enabled');
etherpadManager
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
etherpadManager = new EtherpadManager(eventEmitter);
const url = new URL(name, config.etherpad_base);
APP.store.dispatch(setDocumentUrl(url.toString()));
APP.store.dispatch(setEtherpadHasInitialzied());
};
/**

View File

@@ -1,12 +1,22 @@
/* global $, APP, interfaceConfig */
import { getSharedDocumentUrl, setDocumentEditingState } from '../../../react/features/etherpad';
import { setDocumentEditingState } from '../../../react/features/etherpad';
import { getToolboxHeight } from '../../../react/features/toolbox';
import VideoLayout from '../videolayout/VideoLayout';
import LargeContainer from '../videolayout/LargeContainer';
import Filmstrip from '../videolayout/Filmstrip';
/**
* Etherpad options.
*/
const options = $.param({
showControls: true,
showChat: false,
showLineNumbers: true,
useMonospaceFont: false
});
/**
*
*/
@@ -60,13 +70,13 @@ class Etherpad extends LargeContainer {
/**
* Creates new Etherpad object
*/
constructor(url) {
constructor(domain, name) {
super();
const iframe = document.createElement('iframe');
iframe.id = 'etherpadIFrame';
iframe.src = url;
iframe.src = `${domain + name}?${options}`;
iframe.frameBorder = 0;
iframe.scrolling = 'no';
iframe.width = DEFAULT_WIDTH;
@@ -189,7 +199,13 @@ export default class EtherpadManager {
/**
*
*/
constructor(eventEmitter) {
constructor(domain, name, eventEmitter) {
if (!domain || !name) {
throw new Error('missing domain or name');
}
this.domain = domain;
this.name = name;
this.eventEmitter = eventEmitter;
this.etherpad = null;
}
@@ -212,7 +228,7 @@ export default class EtherpadManager {
* Create new Etherpad frame.
*/
openEtherpad() {
this.etherpad = new Etherpad(getSharedDocumentUrl(APP.store.getState));
this.etherpad = new Etherpad(this.domain, this.name);
VideoLayout.addLargeVideoContainer(
ETHERPAD_CONTAINER_TYPE,
this.etherpad

View File

@@ -273,7 +273,7 @@ LocalVideo.prototype._updateVideoElement = function() {
// case video does not autoplay.
const video = this.container.querySelector('video');
video && !config.testing?.noAutoPlayVideo && video.play();
video && video.play();
};
export default LocalVideo;

View File

@@ -7,7 +7,6 @@ import { Provider } from 'react-redux';
import { I18nextProvider } from 'react-i18next';
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { createThumbnailOffsetParentIsNullEvent, sendAnalytics } from '../../../react/features/analytics';
import { i18next } from '../../../react/features/base/i18n';
import {
JitsiParticipantConnectionStatus
@@ -513,31 +512,11 @@ RemoteVideo.prototype.addRemoteStreamElement = function(stream) {
$(streamElement).hide();
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
// TODO: Remove once we verify that this.container.offsetParent === null was the reason for not attached video
// streams to the thumbnail.
if (isVideo && this.container.offsetParent === null) {
sendAnalytics(createThumbnailOffsetParentIsNullEvent(this.id));
const parentNodesDisplayProps = [
'#filmstripRemoteVideosContainer',
'#filmstripRemoteVideos',
'#remoteVideos',
'.filmstrip',
'#videospace',
'#videoconference_page',
'#react'
].map(selector => `${selector} - ${$(selector).css('display')}`);
const videoConferencePageParent = $('#videoconference_page').parent();
const reactDiv = document.getElementById('react');
parentNodesDisplayProps.push(
`${videoConferencePageParent.attr('class')} - ${videoConferencePageParent.css('display')}`);
parentNodesDisplayProps.push(`this.container - ${this.$container.css('display')}`);
logger.debug(`this.container.offsetParent is null [user: ${this.id}, ${
parentNodesDisplayProps.join(', ')}, #react.offsetParent - ${
reactDiv && reactDiv.offsetParent !== null ? 'not null' : 'null'}]`);
// If the container is currently visible
// we attach the stream to the element.
if (!isVideo || (this.container.offsetParent !== null && isVideo)) {
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
}
if (!isVideo) {

View File

@@ -199,7 +199,7 @@ SmallVideo.createStreamElement = function(stream) {
element.muted = true;
}
element.autoplay = !config.testing?.noAutoPlayVideo;
element.autoplay = true;
element.id = SmallVideo.getStreamElementID(stream);
return element;

31
package-lock.json generated
View File

@@ -3062,11 +3062,6 @@
}
}
},
"@react-native-community/google-signin": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/google-signin/-/google-signin-3.0.1.tgz",
"integrity": "sha512-RC9c7ATGdq5IKFqw/h4d8eVTDve8FZxMtsarBHKfP09SrQfEgvOebzVr7YNC+4qs7dFqR+0E2vD7nle1s/dQ3A=="
},
"@react-native-community/netinfo": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.1.5.tgz",
@@ -11686,8 +11681,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"from": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"version": "github:jitsi/lib-jitsi-meet#3f7613748d7669cd3fd031bbdf9069e4309f6f56",
"from": "github:jitsi/lib-jitsi-meet#3f7613748d7669cd3fd031bbdf9069e4309f6f56",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",
@@ -15446,6 +15441,11 @@
"jssha": "^2.2.0"
}
},
"react-native-google-signin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-2.0.0.tgz",
"integrity": "sha512-9loM4lcCIdbco5BnmNio7yGaXQKCpCaY1VRmYiTSvC5NjuSf6Ui6jZRee46p/YdaU4yRnS3u5Vct6Psrvr0HNg=="
},
"react-native-immersive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-2.0.0.tgz",
@@ -15520,19 +15520,14 @@
}
},
"react-native-webview": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-7.4.1.tgz",
"integrity": "sha512-AVT5HIEEWc/NZdNwXRVev0cAs1Si0O3BA4Crqyor8JbwuxUUCllLv+NK7TO3eOw/ENl+QyIPHu9dizkoycmgJQ==",
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-5.8.1.tgz",
"integrity": "sha512-b6pSvmjoiWtcz6YspggW02X+BRXJWuquHwkh37BRx1NMW1iwMZA31SnFQvTpPzWYYIb9WF/mRsy2nGtt9C6NIg==",
"requires": {
"escape-string-regexp": "2.0.0",
"escape-string-regexp": "1.0.5",
"invariant": "2.2.4"
},
"dependencies": {
"escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@@ -16418,10 +16413,6 @@
"inherits": "^2.0.1"
}
},
"rnnoise-wasm": {
"version": "github:jitsi/rnnoise-wasm#db96d11f175a22ef56c7db1ba9550835b716e615",
"from": "github:jitsi/rnnoise-wasm#db96d11f175a22ef56c7db1ba9550835b716e615"
},
"rsvp": {
"version": "4.8.5",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",

View File

@@ -35,7 +35,6 @@
"@atlaskit/tooltip": "12.1.13",
"@microsoft/microsoft-graph-client": "1.1.0",
"@react-native-community/async-storage": "1.3.4",
"@react-native-community/google-signin": "3.0.1",
"@react-native-community/netinfo": "4.1.5",
"@svgr/webpack": "4.3.2",
"@tensorflow-models/body-pix": "1.1.2",
@@ -57,7 +56,7 @@
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#3f7613748d7669cd3fd031bbdf9069e4309f6f56",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -72,6 +71,7 @@
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#902e6e92d6bae450a6052f76ba4d02f977ffd8f2",
"react-native-callstats": "3.61.0",
"react-native-google-signin": "2.0.0",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
"react-native-linear-gradient": "2.5.6",
@@ -81,13 +81,12 @@
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#047b019a7ce1ec93ab4a2f6796e997d7a02e8e5d",
"react-native-webview": "7.4.1",
"react-native-webview": "5.8.1",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
"react-transition-group": "2.4.0",
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#db96d11f175a22ef56c7db1ba9550835b716e615",
"styled-components": "3.4.9",
"util": "0.12.1",
"uuid": "3.1.0",

View File

@@ -692,21 +692,6 @@ export function createSyncTrackStateEvent(mediaType, muted) {
};
}
/**
* Creates an event that indicates the thumbnail offset parent is null.
*
* @param {string} id - The id of the user related to the thumbnail.
* @returns {Object} The event in a format suitable for sending via sendAnalytics.
*/
export function createThumbnailOffsetParentIsNullEvent(id) {
return {
action: 'OffsetParentIsNull',
attributes: {
id
}
};
}
/**
* Creates an event associated with a toolbar button being clicked/pressed. By
* convention, where appropriate an attribute named 'enable' should be used to

View File

@@ -15,7 +15,6 @@ import { connect, disconnect, setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { createDesiredLocalTracks } from '../base/tracks';
import {
getBackendSafeRoomName,
getLocationContextRoot,
parseURIString,
toURLString
@@ -86,7 +85,7 @@ export function appNavigate(uri: ?string) {
let url = `${baseURL}config.js`;
// XXX In order to support multiple shards, tell the room to the deployment.
room && (url += `?room=${getBackendSafeRoomName(room)}`);
room && (url += `?room=${room.toLowerCase()}`);
let config;

View File

@@ -23,10 +23,7 @@ import {
participantUpdated
} from '../participants';
import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
import {
getBackendSafeRoomName,
getJitsiMeetGlobalNS
} from '../util';
import { getJitsiMeetGlobalNS } from '../util';
import {
AUTH_STATUS_CHANGED,
@@ -77,11 +74,6 @@ declare var APP: Object;
* @returns {void}
*/
function _addConferenceListeners(conference, dispatch) {
// A simple logger for conference errors received through
// the listener. These errors are not handled now, but logged.
conference.on(JitsiConferenceEvents.CONFERENCE_ERROR,
error => logger.error('Conference error.', error));
// Dispatches into features/base/conference follow:
conference.on(
@@ -396,7 +388,8 @@ export function createConference() {
const conference
= connection.initJitsiConference(
getBackendSafeRoomName(room), {
// XXX Lib-jitsi-meet does not accept uppercase letters.
room.toLowerCase(), {
...state['features/base/config'],
applicationName: getName(),
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,

View File

@@ -1,137 +0,0 @@
/**
* The config keys to whitelist, the keys that can be overridden.
* Currently we can only whitelist the first part of the properties, like
* 'p2p.useStunTurn' and 'p2p.enabled' we whitelist all p2p options.
* The whitelist is used only for config.js.
*
* @type Array
*/
export default [
'_desktopSharingSourceDevice',
'_peerConnStatusOutOfLastNTimeout',
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'analytics.disabled',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
'callFlowsEnabled',
'callStatsConfIDNamespace',
'callStatsID',
'callStatsSecret',
/**
* The display name of the CallKit call representing the conference/meeting
* associated with this config.js including while the call is ongoing in the
* UI presented by CallKit and in the system-wide call history. The property
* is meant for use cases in which the room name is not desirable as a
* display name for CallKit purposes and the desired display name is not
* provided in the form of a JWT callee. As the value is associated with a
* conference/meeting, the value makes sense not as a deployment-wide
* configuration, only as a runtime configuration override/overwrite
* provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callDisplayName',
/**
* The handle
* ({@link https://developer.apple.com/documentation/callkit/cxhandle}) of
* the CallKit call representing the conference/meeting associated with this
* config.js. The property is meant for use cases in which the room URL is
* not desirable as the handle for CallKit purposes. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callHandle',
/**
* The UUID of the CallKit call representing the conference/meeting
* associated with this config.js. The property is meant for use cases in
* which Jitsi Meet is to work with a CallKit call created outside of Jitsi
* Meet and to be adopted by Jitsi Meet such as, for example, an incoming
* and/or outgoing CallKit call created by Jitsi Meet SDK for iOS
* clients/consumers prior to giving control to Jitsi Meet. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callUUID',
'channelLastN',
'constraints',
'debug',
'debugAudioLevels',
'defaultLanguage',
'desktopSharingChromeDisabled',
'desktopSharingChromeExtId',
'desktopSharingChromeMinExtVersion',
'desktopSharingChromeSources',
'desktopSharingFrameRate',
'desktopSharingFirefoxDisabled',
'desktopSharingSources',
'disable1On1Mode',
'disableAEC',
'disableAGC',
'disableAP',
'disableAudioLevels',
'disableDeepLinking',
'disableH264',
'disableHPF',
'disableNS',
'disableRemoteControl',
'disableRtx',
'disableSuspendVideo',
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',
'disableLocalVideoFlip',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'etherpad_base',
'failICE',
'fileRecordingsEnabled',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'liveStreamingEnabled',
'localRecording',
'minParticipants',
'nick',
'openBridgeChannel',
'p2p',
'preferH264',
'requireDisplayName',
'resolution',
'startAudioMuted',
'startAudioOnly',
'startBitrate',
'startSilent',
'startScreenSharing',
'startVideoMuted',
'startWithVideoMuted',
'subject',
'testing',
'useIPv6',
'useNicks',
'useStunTurn',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
];

View File

@@ -2,12 +2,152 @@
import _ from 'lodash';
import CONFIG_WHITELIST from './configWhitelist';
import { _CONFIG_STORE_PREFIX } from './constants';
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
import parseURLParams from './parseURLParams';
import logger from './logger';
declare var $: Object;
/**
* The config keys to whitelist, the keys that can be overridden.
* Currently we can only whitelist the first part of the properties, like
* 'p2p.useStunTurn' and 'p2p.enabled' we whitelist all p2p options.
* The whitelist is used only for config.js.
*
* @private
* @type Array
*/
const WHITELISTED_KEYS = [
'_desktopSharingSourceDevice',
'_peerConnStatusOutOfLastNTimeout',
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'analytics.disabled',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
'callFlowsEnabled',
'callStatsConfIDNamespace',
'callStatsID',
'callStatsSecret',
/**
* The display name of the CallKit call representing the conference/meeting
* associated with this config.js including while the call is ongoing in the
* UI presented by CallKit and in the system-wide call history. The property
* is meant for use cases in which the room name is not desirable as a
* display name for CallKit purposes and the desired display name is not
* provided in the form of a JWT callee. As the value is associated with a
* conference/meeting, the value makes sense not as a deployment-wide
* configuration, only as a runtime configuration override/overwrite
* provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callDisplayName',
/**
* The handle
* ({@link https://developer.apple.com/documentation/callkit/cxhandle}) of
* the CallKit call representing the conference/meeting associated with this
* config.js. The property is meant for use cases in which the room URL is
* not desirable as the handle for CallKit purposes. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callHandle',
/**
* The UUID of the CallKit call representing the conference/meeting
* associated with this config.js. The property is meant for use cases in
* which Jitsi Meet is to work with a CallKit call created outside of Jitsi
* Meet and to be adopted by Jitsi Meet such as, for example, an incoming
* and/or outgoing CallKit call created by Jitsi Meet SDK for iOS
* clients/consumers prior to giving control to Jitsi Meet. As the value is
* associated with a conference/meeting, the value makes sense not as a
* deployment-wide configuration, only as a runtime configuration
* override/overwrite provided by, for example, Jitsi Meet SDK for iOS.
*
* @type string
*/
'callUUID',
'channelLastN',
'constraints',
'debug',
'debugAudioLevels',
'defaultLanguage',
'desktopSharingChromeDisabled',
'desktopSharingChromeExtId',
'desktopSharingChromeMinExtVersion',
'desktopSharingChromeSources',
'desktopSharingFrameRate',
'desktopSharingFirefoxDisabled',
'desktopSharingSources',
'disable1On1Mode',
'disableAEC',
'disableAGC',
'disableAP',
'disableAudioLevels',
'disableDeepLinking',
'disableH264',
'disableHPF',
'disableNS',
'disableRemoteControl',
'disableRtx',
'disableSuspendVideo',
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',
'disableLocalVideoFlip',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'etherpad_base',
'failICE',
'fileRecordingsEnabled',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'liveStreamingEnabled',
'localRecording',
'minParticipants',
'nick',
'openBridgeChannel',
'p2p',
'preferH264',
'requireDisplayName',
'resolution',
'startAudioMuted',
'startAudioOnly',
'startBitrate',
'startSilent',
'startScreenSharing',
'startVideoMuted',
'startWithAudioMuted',
'startWithVideoMuted',
'subject',
'testing',
'useIPv6',
'useNicks',
'useStunTurn',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable'
];
// XXX The functions getRoomName and parseURLParams are split out of
// functions.js because they are bundled in both app.bundle and
// do_external_connect, webpack 1 does not support tree shaking, and we don't
@@ -38,6 +178,69 @@ export function createFakeConfig(baseURL: string) {
};
}
/**
* Promise wrapper on obtain config method. When HttpConfigFetch will be moved
* to React app it's better to use load config instead.
*
* @param {string} location - URL of the domain from which the config is to be
* obtained.
* @param {string} room - Room name.
* @private
* @returns {Promise<void>}
*/
export function obtainConfig(location: string, room: string): Promise<void> {
return new Promise((resolve, reject) =>
_obtainConfig(location, room, (success, error) => {
success ? resolve() : reject(error);
})
);
}
/**
* Sends HTTP POST request to specified {@code endpoint}. In request the name
* of the room is included in JSON format:
* {
* "rooomName": "someroom12345"
* }.
*
* @param {string} endpoint - The name of HTTP endpoint to which to send
* the HTTP POST request.
* @param {string} roomName - The name of the conference room for which config
* is requested.
* @param {Function} complete - The callback to invoke upon success or failure.
* @returns {void}
*/
function _obtainConfig(endpoint: string, roomName: string, complete: Function) {
logger.info(`Send config request to ${endpoint} for room: ${roomName}`);
$.ajax(
endpoint,
{
contentType: 'application/json',
data: JSON.stringify({ roomName }),
dataType: 'json',
method: 'POST',
error(jqXHR, textStatus, errorThrown) {
logger.error('Get config error: ', jqXHR, errorThrown);
complete(false, `Get config response status: ${textStatus}`);
},
success(data) {
const { config, interfaceConfig, loggingConfig } = window;
try {
overrideConfigJSON(
config, interfaceConfig, loggingConfig,
data);
complete(true);
} catch (e) {
logger.error('Parse config error: ', e);
complete(false, e);
}
}
}
);
}
/* eslint-disable max-params, no-shadow */
/**
@@ -103,8 +306,8 @@ export function overrideConfigJSON(
/* eslint-enable max-params, no-shadow */
/**
* Apply whitelist filtering for configs with whitelists, skips this for others
* configs (loggingConfig).
* Whitelist only config.js, skips this for others configs
* (interfaceConfig, loggingConfig).
* Only extracts overridden values for keys we allow to be overridden.
*
* @param {string} configName - The config name, one of config,
@@ -115,13 +318,11 @@ export function overrideConfigJSON(
* that are whitelisted.
*/
function _getWhitelistedJSON(configName, configJSON) {
if (configName === 'interfaceConfig') {
return _.pick(configJSON, INTERFACE_CONFIG_WHITELIST);
} else if (configName === 'config') {
return _.pick(configJSON, CONFIG_WHITELIST);
if (configName !== 'config') {
return configJSON;
}
return configJSON;
return _.pick(configJSON, WHITELISTED_KEYS);
}
/**

View File

@@ -11,8 +11,8 @@ export * from './functions.any';
* @returns {void}
*/
export function _cleanupConfig(config: Object) {
config.analytics.scriptURLs = [];
if (NativeModules.AppInfo.LIBRE_BUILD) {
config.analytics.scriptURLs = [];
delete config.analytics.amplitudeAPPKey;
delete config.analytics.googleAnalyticsTrackingId;
delete config.callStatsID;

View File

@@ -1,6 +1,6 @@
// @flow
/* @flow */
import { getBackendSafeRoomName } from '../util';
declare var config: Object;
/**
* Builds and returns the room name.
@@ -8,10 +8,22 @@ import { getBackendSafeRoomName } from '../util';
* @returns {string}
*/
export default function getRoomName(): ?string {
const { getroomnode } = config;
const path = window.location.pathname;
let roomName;
// The last non-directory component of the path (name) is the room.
const roomName = path.substring(path.lastIndexOf('/') + 1) || undefined;
// Determine the room node from the URL.
if (getroomnode && typeof getroomnode === 'function') {
roomName = getroomnode.call(config, path);
} else {
// Fall back to the default strategy of making assumptions about how the
// URL maps to the room (name). It currently assumes a deployment in
// which the last non-directory component of the path (name) is the
// room.
roomName
= path.substring(path.lastIndexOf('/') + 1).toLowerCase()
|| undefined;
}
return getBackendSafeRoomName(roomName);
return roomName;
}

View File

@@ -1,70 +0,0 @@
/**
* The interface config keys to whitelist, the keys that can be overridden.
*
* @private
* @type Array
*/
export default [
'ANDROID_APP_PACKAGE',
'APP_NAME',
'APP_SCHEME',
'AUDIO_LEVEL_PRIMARY_COLOR',
'AUDIO_LEVEL_SECONDARY_COLOR',
'AUTHENTICATION_ENABLE',
'AUTO_PIN_LATEST_SCREEN_SHARE',
'BRAND_WATERMARK_LINK',
'CLOSE_PAGE_GUEST_HINT',
'CONNECTION_INDICATOR_AUTO_HIDE_ENABLED',
'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
'CONNECTION_INDICATOR_DISABLED',
'DEFAULT_BACKGROUND',
'DEFAULT_LOCAL_DISPLAY_NAME',
'DEFAULT_REMOTE_DISPLAY_NAME',
'DISABLE_DOMINANT_SPEAKER_INDICATOR',
'DISABLE_FOCUS_INDICATOR',
'DISABLE_RINGING',
'DISABLE_TRANSCRIPTION_SUBTITLES',
'DISABLE_VIDEO_BACKGROUND',
'DISPLAY_WELCOME_PAGE_CONTENT',
'ENABLE_FEEDBACK_ANIMATION',
'ENFORCE_NOTIFICATION_AUTO_DISMISS_TIMEOUT',
'FILM_STRIP_MAX_HEIGHT',
'GENERATE_ROOMNAMES_ON_WELCOME_PAGE',
'INDICATOR_FONT_SIZES',
'INITIAL_TOOLBAR_TIMEOUT',
'INVITATION_POWERED_BY',
'JITSI_WATERMARK_LINK',
'LANG_DETECTION',
'LIVE_STREAMING_HELP_LINK',
'LOCAL_THUMBNAIL_RATIO',
'MAXIMUM_ZOOMING_COEFFICIENT',
'MOBILE_APP_PROMO',
'MOBILE_DOWNLOAD_LINK_ANDROID',
'MOBILE_DOWNLOAD_LINK_IOS',
'MOBILE_DYNAMIC_LINK',
'NATIVE_APP_NAME',
'OPTIMAL_BROWSERS',
'PHONE_NUMBER_REGEX',
'POLICY_LOGO',
'PROVIDER_NAME',
'RANDOM_AVATAR_URL_PREFIX',
'RANDOM_AVATAR_URL_SUFFIX',
'RECENT_LIST_ENABLED',
'REMOTE_THUMBNAIL_RATIO',
'SETTINGS_SECTIONS',
'SHOW_BRAND_WATERMARK',
'SHOW_DEEP_LINKING_IMAGE',
'SHOW_JITSI_WATERMARK',
'SHOW_POWERED_BY',
'SHOW_WATERMARK_FOR_GUESTS',
'SUPPORT_URL',
'TILE_VIEW_MAX_COLUMNS',
'TOOLBAR_ALWAYS_VISIBLE',
'TOOLBAR_BUTTONS',
'TOOLBAR_TIMEOUT',
'UNSUPPORTED_BROWSERS',
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED',
'filmStripOnly'
];

View File

@@ -9,10 +9,7 @@ import {
getCurrentConference
} from '../conference';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import {
getBackendSafeRoomName,
parseURIString
} from '../util';
import { parseURIString } from '../util';
import {
CONNECTION_DISCONNECTED,
@@ -310,7 +307,10 @@ function _constructOptions(state) {
// Append room to the URL's search.
const { room } = state['features/base/conference'];
room && (bosh += `?room=${getBackendSafeRoomName(room)}`);
// XXX The Jitsi Meet deployments require the room argument to be in
// lower case at the time of this writing but, unfortunately, they do
// not ignore case themselves.
room && (bosh += `?room=${room.toLowerCase()}`);
options.bosh = bosh;
}

View File

@@ -6,7 +6,6 @@ declare var APP: Object;
declare var config: Object;
import { configureInitialDevices } from '../devices';
import { getBackendSafeRoomName } from '../util';
export {
connectionEstablished,
@@ -22,7 +21,8 @@ import logger from './logger';
*/
export function connect() {
return (dispatch: Dispatch<any>, getState: Function) => {
const room = getBackendSafeRoomName(getState()['features/base/conference'].room);
// XXX Lib-jitsi-meet does not accept uppercase letters.
const room = getState()['features/base/conference'].room.toLowerCase();
// XXX For web based version we use conference initialization logic
// from the old app (at the moment of writing).

View File

@@ -61,7 +61,11 @@ class BottomSheet extends PureComponent<Props> {
styles.sheetItemContainer,
_styles.sheet
] }>
{ this._getWrappedContent() }
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }>
{ this._getWrappedContent() }
</ScrollView>
</View>
</View>
</SlidingView>
@@ -69,32 +73,24 @@ class BottomSheet extends PureComponent<Props> {
}
/**
* Wraps the content when needed (iOS 11 and above), or just returns the original content.
* Wraps the content when needed (iOS 11 and above), or just returns the original children.
*
* @returns {React$Element}
*/
_getWrappedContent() {
const content = (
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false } >
{ this.props.children }
</ScrollView>
);
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS > 10) {
return (
<SafeAreaView>
{ content }
{ this.props.children }
</SafeAreaView>
);
}
}
return content;
return this.props.children;
}
}

View File

@@ -73,12 +73,6 @@ const _LANGUAGES = {
main: require('../../../../lang/main-hr')
},
// Hungarian
'hu': {
languages: require('../../../../lang/languages-hu'),
main: require('../../../../lang/main-hu')
},
// Italian
'it': {
languages: require('../../../../lang/languages-it'),

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

Before

Width:  |  Height:  |  Size: 381 B

View File

@@ -28,7 +28,6 @@ export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconFullScreen } from './full-screen.svg';
export { default as IconHangup } from './hangup.svg';
export { default as IconHelp } from './help.svg';
export { default as IconInfo } from './info.svg';
export { default as IconInvite } from './invite.svg';
export { default as IconKick } from './kick.svg';
@@ -37,7 +36,6 @@ export { default as IconMenu } from './menu.svg';
export { default as IconMenuDown } from './menu-down.svg';
export { default as IconMenuThumb } from './thumb-menu.svg';
export { default as IconMenuUp } from './menu-up.svg';
export { default as IconMessage } from './message.svg';
export { default as IconMicDisabled } from './mic-disabled.svg';
export { default as IconMicrophone } from './microphone.svg';
export { default as IconModerator } from './star.svg';
@@ -50,7 +48,6 @@ export { default as IconRaisedHand } from './raised-hand.svg';
export { default as IconRec } from './rec.svg';
export { default as IconRemoteControlStart } from './play.svg';
export { default as IconRemoteControlStop } from './stop.svg';
export { default as IconReply } from './reply.svg';
export { default as IconRestore } from './restore.svg';
export { default as IconRoomLock } from './security.svg';
export { default as IconRoomUnlock } from './security-locked.svg';

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 256 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 194 B

View File

@@ -1,7 +1,12 @@
// @flow
import { toState } from '../redux';
import { loadScript } from '../util';
import JitsiMeetJS from './_';
import logger from './logger';
declare var APP: Object;
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
@@ -92,3 +97,42 @@ export function isFatalJitsiConnectionError(error: Object | string) {
|| error === JitsiConnectionErrors.OTHER_ERROR
|| error === JitsiConnectionErrors.SERVER_ERROR);
}
/**
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @returns {Promise<Object>}
*/
export function loadConfig(url: string): Promise<Object> {
let promise;
if (typeof APP === 'undefined') {
promise
= loadScript(url, 2.5 * 1000 /* Timeout in ms */)
.then(() => {
const { config } = window;
// We don't want to pollute the global scope.
window.config = undefined;
if (typeof config !== 'object') {
throw new Error('window.config is not an object');
}
return config;
})
.catch(err => {
logger.error(`Failed to load config from ${url}`, err);
throw err;
});
} else {
// Return "the config.js file" from the global scope - that is how the
// Web app on both the client and the server was implemented before the
// React Native app was even conceived.
promise = Promise.resolve(window.config);
}
return promise;
}

View File

@@ -1,36 +0,0 @@
// @flow
import { NativeModules } from 'react-native';
import { loadScript } from '../util';
import logger from './logger';
export * from './functions.any';
const { JavaScriptSandbox } = NativeModules;
/**
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @returns {Promise<Object>}
*/
export async function loadConfig(url: string): Promise<Object> {
try {
const configTxt = await loadScript(url, 2.5 * 1000 /* Timeout in ms */, true /* skipeval */);
const configJson = await JavaScriptSandbox.evaluate(`${configTxt}\nJSON.stringify(config);`);
const config = JSON.parse(configJson);
if (typeof config !== 'object') {
throw new Error('config is not an object');
}
logger.info(`Config loaded from ${url}`);
return config;
} catch (err) {
logger.error(`Failed to load config from ${url}`, err);
throw err;
}
}

View File

@@ -1,16 +0,0 @@
// @flow
export * from './functions.any';
/**
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @returns {Promise<Object>}
*/
export async function loadConfig(url: string): Promise<Object> { // eslint-disable-line no-unused-vars
// Return "the config.js file" from the global scope - that is how the
// Web app on both the client and the server was implemented before the
// React Native app was even conceived.
return window.config;
}

View File

@@ -26,13 +26,7 @@ type Props = {
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object,
/**
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
autoPlay: boolean
videoTrack: ?Object
};
/**
@@ -50,7 +44,7 @@ class Video extends Component<Props> {
*/
static defaultProps = {
className: '',
autoPlay: true,
id: ''
};
@@ -137,7 +131,7 @@ class Video extends Component<Props> {
render() {
return (
<video
autoPlay = { this.props.autoPlay }
autoPlay = { true }
className = { this.props.className }
id = { this.props.id }
ref = { this._setVideoElement } />

View File

@@ -23,14 +23,7 @@ type Props = AbstractVideoTrackProps & {
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: string,
/**
*
* Used to determine the value of the autoplay attribute of the underlying
* video element.
*/
_noAutoPlayVideo: boolean
id: string
};
/**
@@ -60,7 +53,7 @@ class VideoTrack extends AbstractVideoTrack<Props> {
render() {
return (
<Video
autoPlay = { !this.props._noAutoPlayVideo }
autoPlay = { true }
className = { this.props.className }
id = { this.props.id }
onVideoPlaying = { this._onVideoPlaying }
@@ -71,22 +64,4 @@ class VideoTrack extends AbstractVideoTrack<Props> {
_onVideoPlaying: () => void;
}
/**
* Maps (parts of) the Redux state to the associated VideoTracks props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _noAutoPlayVideo: boolean
* }}
*/
function _mapStateToProps(state) {
const testingConfig = state['features/base/config'].testing;
return {
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
};
}
export default connect(_mapStateToProps)(VideoTrack);
export default connect()(VideoTrack);

View File

@@ -28,7 +28,6 @@ export const ColorPalette = {
overflowMenuItemUnderlay: '#EEEEEE',
red: '#D00000',
transparent: 'rgba(0, 0, 0, 0)',
warning: 'rgb(215, 121, 118)',
white: '#FFFFFF',
/**

View File

@@ -1,5 +1,4 @@
export * from './helpers';
export * from './httpUtils';
export * from './loadScript';
export * from './openURLInBrowser';
export * from './uri';

View File

@@ -1,9 +1,6 @@
// @flow
/**
* Default timeout for loading scripts.
*/
const DEFAULT_TIMEOUT = 5000;
import { timeoutPromise } from './timeoutPromise';
/**
* Loads a script from a specific URL. React Native cannot load a JS
@@ -16,49 +13,63 @@ const DEFAULT_TIMEOUT = 5000;
* @param {number} [timeout] - The timeout in millisecnods after which the
* loading of the specified {@code url} is to be aborted/rejected (if not
* settled yet).
* @param {boolean} skipEval - Wether we want to skip evaluating the loaded content or not.
* @returns {void}
*/
export async function loadScript(
url: string, timeout: number = DEFAULT_TIMEOUT, skipEval: boolean = false): Promise<any> {
// XXX The implementation of fetch on Android will throw an Exception on
// the Java side which will break the app if the URL is invalid (which
// the implementation of fetch on Android calls 'unexpected url'). In
// order to try to prevent the breakage of the app, try to fail on an
// invalid URL as soon as possible.
const { hostname, pathname, protocol } = new URL(url);
export function loadScript(url: string, timeout: ?number): Promise<void> {
return new Promise((resolve, reject) => {
// XXX The implementation of fetch on Android will throw an Exception on
// the Java side which will break the app if the URL is invalid (which
// the implementation of fetch on Android calls 'unexpected url'). In
// order to try to prevent the breakage of the app, try to fail on an
// invalid URL as soon as possible.
const { hostname, pathname, protocol } = new URL(url);
// XXX The standard URL implementation should throw an Error if the
// specified URL is relative. Unfortunately, the polyfill used on
// react-native does not.
if (!hostname || !pathname || !protocol) {
throw new Error(`unexpected url: ${url}`);
}
// XXX The standard URL implementation should throw an Error if the
// specified URL is relative. Unfortunately, the polyfill used on
// react-native does not.
if (!hostname || !pathname || !protocol) {
reject(`unexpected url: ${url}`);
const controller = new AbortController();
const signal = controller.signal;
const timer = setTimeout(() => {
controller.abort();
}, timeout);
const response = await fetch(url, { signal });
// If the timeout hits the above will raise AbortError.
clearTimeout(timer);
switch (response.status) {
case 200: {
const txt = await response.text();
if (skipEval) {
return txt;
return;
}
return eval.call(window, txt); // eslint-disable-line no-eval
}
default:
throw new Error(`loadScript error: ${response.statusText}`);
}
let fetch_ = fetch(url, { method: 'GET' });
// The implementation of fetch provided by react-native is based on
// XMLHttpRequest. Which defines timeout as an unsigned long with
// default value 0, which means there is no timeout.
if (timeout) {
// FIXME I don't like the approach with timeoutPromise because:
//
// * It merely abandons the underlying XHR and, consequently, opens
// us to potential issues with NetworkActivityIndicator which
// tracks XHRs.
//
// * @paweldomas also reported that timeouts seem to be respected by
// the XHR implementation on iOS. Given that we have
// implementation of loadScript based on fetch and XHR (in an
// earlier revision), I don't see why we're not using an XHR
// directly on iOS.
//
// * The approach of timeoutPromise I found on the Internet is to
// directly use XHR instead of fetch and abort the XHR on timeout.
// Which may deal with the NetworkActivityIndicator at least.
fetch_ = timeoutPromise(fetch_, timeout);
}
fetch_
.then(response => {
switch (response.status) {
case 200:
return response.responseText || response.text();
default:
throw response.statusText;
}
})
.then(responseText => {
eval.call(window, responseText); // eslint-disable-line no-eval
})
.then(resolve, reject);
});
}

View File

@@ -1,5 +0,0 @@
// @flow
import { getLogger } from '../logging/functions';
export default getLogger('features/base/util');

View File

@@ -1,17 +0,0 @@
// @flow
import { Linking } from 'react-native';
import logger from './logger';
/**
* Opens URL in the browser.
*
* @param {string} url - The URL to be opened.
* @returns {void}
*/
export function openURLInBrowser(url: string) {
Linking.openURL(url).catch(error => {
logger.error(`An error occurred while trying to open ${url}`, error);
});
}

View File

@@ -1,11 +0,0 @@
// @flow
/**
* Opens URL in the browser.
*
* @param {string} url - The URL to be opened.
* @returns {void}
*/
export function openURLInBrowser(url: string) {
window.open(url, '', 'noopener');
}

View File

@@ -96,47 +96,6 @@ function _fixURIStringScheme(uri: string) {
return uri;
}
/**
* Converts a room name to a backend-safe format. Properly lowercased and url encoded.
*
* @param {string?} room - The room name to convert.
* @returns {string?}
*/
export function getBackendSafeRoomName(room: ?string): ?string {
if (!room) {
return room;
}
/* eslint-disable no-param-reassign */
try {
// We do not know if we get an already encoded string at this point
// as different platforms do it differently, but we need a decoded one
// for sure. However since decoding a non-encoded string is a noop, we're safe
// doing it here.
room = decodeURIComponent(room);
} catch (e) {
// This can happen though if we get an unencoded string and it contains
// some characters that look like an encoded entity, but it's not.
// But in this case we're fine goin on...
}
// Normalize the character set
room = room.normalize('NFKC');
// Only decoded and normalized strings can be lowercased properly.
room = room.toLowerCase();
// But we still need to (re)encode it.
room = encodeURIComponent(room);
/* eslint-enable no-param-reassign */
// Unfortunately we still need to lowercase it, because encoding a string will
// add some uppercase characters, but some backend services
// expect it to be full lowercase. However lowercasing an encoded string
// doesn't change the string value.
return room.toLowerCase();
}
/**
* Gets the (Web application) context root defined by a specific location (URI).
*

View File

@@ -30,23 +30,11 @@ export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
*
* {
* type: SEND_MESSAGE,
* ignorePrivacy: boolean,
* message: string
* }
*/
export const SEND_MESSAGE = 'SEND_MESSAGE';
/**
* The type of action which signals the initiation of sending of as private message to the
* supplied recipient.
*
* {
* participant: Participant,
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* }
*/
export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
/**
* The type of the action which signals to toggle the display of the chat panel.
*

View File

@@ -4,7 +4,6 @@ import {
ADD_MESSAGE,
CLEAR_MESSAGES,
SEND_MESSAGE,
SET_PRIVATE_MESSAGE_RECIPIENT,
TOGGLE_CHAT
} from './actionTypes';
@@ -54,37 +53,18 @@ export function clearMessages() {
* Sends a chat message to everyone in the conference.
*
* @param {string} message - The chat message to send out.
* @param {boolean} ignorePrivacy - True if the privacy notification should be ignored.
* @returns {{
* type: SEND_MESSAGE,
* ignorePrivacy: boolean,
* message: string
* }}
*/
export function sendMessage(message: string, ignorePrivacy: boolean = false) {
export function sendMessage(message: string) {
return {
type: SEND_MESSAGE,
ignorePrivacy,
message
};
}
/**
* Initiates the sending of a private message to the supplied participant.
*
* @param {Participant} participant - The participant to set the recipient to.
* @returns {{
* participant: Participant,
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* }}
*/
export function setPrivateMessageRecipient(participant: Object) {
return {
participant,
type: SET_PRIVATE_MESSAGE_RECIPIENT
};
}
/**
* Toggles display of the chat side panel.
*

View File

@@ -56,32 +56,4 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
.format(TIMESTAMP_FORMAT);
}
/**
* Generates the message text to be redered in the component.
*
* @returns {string}
*/
_getMessageText() {
const { message } = this.props;
return message.messageType === 'error'
? this.props.t('chat.error', {
error: message.message
})
: message.message;
}
/**
* Returns the message that is displayed as a notice for private messages.
*
* @returns {string}
*/
_getPrivateNoticeMessage() {
const { message, t } = this.props;
return t('chat.privateNotice', {
recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
});
}
}

View File

@@ -1,116 +0,0 @@
// @flow
import { PureComponent } from 'react';
import { sendMessage, setPrivateMessageRecipient } from '../actions';
import { getParticipantById } from '../../base/participants';
type Props = {
/**
* The message that is about to be sent.
*/
message: Object,
/**
* The ID of the participant that we think the message may be intended to.
*/
participantID: string,
/**
* Function to be used to translate i18n keys.
*/
t: Function,
/**
* Prop to be invoked on sending the message.
*/
_onSendMessage: Function,
/**
* Prop to be invoked when the user wants to set a private recipient.
*/
_onSetMessageRecipient: Function,
/**
* The participant retreived from Redux by the participanrID prop.
*/
_participant: Object
};
/**
* Abstract class for the dialog displayed to avoid mis-sending private messages.
*/
export class AbstractChatPrivacyDialog extends PureComponent<Props> {
/**
* Instantiates a new instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onSendGroupMessage = this._onSendGroupMessage.bind(this);
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
}
_onSendGroupMessage: () => boolean;
/**
* Callback to be invoked for cancel action (user wants to send a group message).
*
* @returns {boolean}
*/
_onSendGroupMessage() {
this.props._onSendMessage(this.props.message);
return true;
}
_onSendPrivateMessage: () => boolean;
/**
* Callback to be invoked for submit action (user wants to send a private message).
*
* @returns {void}
*/
_onSendPrivateMessage() {
const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
_onSetMessageRecipient(_participant);
_onSendMessage(message);
return true;
}
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_onSendMessage: (message: Object) => {
dispatch(sendMessage(message, true));
},
_onSetMessageRecipient: participant => {
dispatch(setPrivateMessageRecipient(participant));
}
};
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
return {
_participant: getParticipantById(state, ownProps.participantID)
};
}

View File

@@ -1,61 +0,0 @@
// @flow
import { PureComponent } from 'react';
import { getParticipantDisplayName } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
type Props = {
/**
* Function used to translate i18n labels.
*/
t: Function,
/**
* Function to remove the recipent setting of the chat window.
*/
_onRemovePrivateMessageRecipient: Function,
/**
* The name of the message recipient, if any.
*/
_privateMessageRecipient: ?string
};
/**
* Abstract class for the {@code MessageRecipient} component.
*/
export default class AbstractMessageRecipient extends PureComponent<Props> {
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_onRemovePrivateMessageRecipient: () => {
dispatch(setPrivateMessageRecipient());
}
};
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const { privateMessageRecipient } = state['features/chat'];
return {
_privateMessageRecipient:
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined
};
}

View File

@@ -1,101 +0,0 @@
// @flow
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
import { translate } from '../../base/i18n';
import { IconMessage, IconReply } from '../../base/icons';
import { getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { setPrivateMessageRecipient } from '../actions';
export type Props = AbstractButtonProps & {
/**
* The ID of the participant that the message is to be sent.
*/
participantID: string,
/**
* True if the button is rendered as a reply button.
*/
reply: boolean,
/**
* Function to be used to translate i18n labels.
*/
t: Function,
/**
* The participant object retreived from Redux.
*/
_participant: Object,
/**
* Function to dispatch the result of the participant selection to send a private message.
*/
_setPrivateMessageRecipient: Function
};
/**
* Class to render a button that initiates the sending of a private message through chet.
*/
class PrivateMessageButton extends AbstractButton<Props, any> {
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
icon = IconMessage;
label = 'toolbar.privateMessage';
toggledIcon = IconReply;
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { _participant, _setPrivateMessageRecipient } = this.props;
_setPrivateMessageRecipient(_participant);
}
/**
* Helper function to be implemented by subclasses, which must return a
* {@code boolean} value indicating if this button is toggled or not.
*
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props.reply;
}
}
/**
* Maps part of the props of this component to Redux actions.
*
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Props}
*/
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_setPrivateMessageRecipient: participant => {
dispatch(setPrivateMessageRecipient(participant));
}
};
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
return {
_participant: getParticipantById(state, ownProps.participantID)
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageButton));

View File

@@ -1,4 +1,3 @@
// @flow
export * from './native';
export { default as PrivateMessageButton } from './PrivateMessageButton';

View File

@@ -1,5 +1,3 @@
// @flow
export * from './web';
export { default as PrivateMessageButton } from './PrivateMessageButton';

View File

@@ -16,7 +16,6 @@ import AbstractChat, {
import ChatInputBar from './ChatInputBar';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import styles from './styles';
/**
@@ -54,7 +53,6 @@ class Chat extends AbstractChat<Props> {
onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<MessageRecipient />
<ChatInputBar onSend = { this.props._onSendMessage } />
</SafeAreaView>
</KeyboardAvoidingView>

View File

@@ -10,7 +10,6 @@ import { Linkify } from '../../../base/react';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import styles from './styles';
@@ -47,30 +46,25 @@ class ChatMessage extends AbstractChatMessage<Props> {
textWrapperStyle.push(styles.systemTextWrapper);
}
const messageText = message.messageType === 'error'
? this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message;
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
<View style = { detailsWrapperStyle }>
<View style = { styles.replyWrapper }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(this._getMessageText()) }
</Linkify>
{
message.privateMessage
&& this._renderPrivateNotice()
}
</View>
{ message.privateMessage && !localMessage
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { styles.replyStyles } /> }
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(messageText) }
</Linkify>
</View>
{ this.props.showTimestamp && this._renderTimestamp() }
</View>
@@ -80,10 +74,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
_getFormattedTimestamp: () => string;
_getMessageText: () => string;
_getPrivateNoticeMessage: () => string;
/**
* Renders the avatar of the sender.
*
@@ -116,19 +106,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
);
}
/**
* Renders the message privacy notice.
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
return (
<Text style = { styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
</Text>
);
}
/**
* Renders the time at which the message was sent.
*

View File

@@ -1,37 +0,0 @@
// @flow
import React from 'react';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
/**
* Implements a component for the dialog displayed to avoid mis-sending private messages.
*/
class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<ConfirmDialog
cancelKey = 'dialog.sendPrivateMessageCancel'
contentKey = 'dialog.sendPrivateMessage'
okKey = 'dialog.sendPrivateMessageOk'
onCancel = { this._onSendGroupMessage }
onSubmit = { this._onSendPrivateMessage } />
);
}
_onSendGroupMessage: () => boolean;
_onSendPrivateMessage: () => boolean;
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

View File

@@ -1,52 +0,0 @@
// @flow
import React from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
} from '../AbstractMessageRecipient';
import styles from './styles';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
if (!_privateMessageRecipient) {
return null;
}
const { t } = this.props;
return (
<View style = { styles.messageRecipientContainer }>
<Text style = { styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
</Text>
<TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection }
style = { styles.messageRecipientCancelIcon } />
</TouchableHighlight>
</View>
);
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

View File

@@ -2,4 +2,3 @@
export { default as Chat } from './Chat';
export { default as ChatButton } from './ChatButton';
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

View File

@@ -80,23 +80,6 @@ export default {
flex: 1
},
messageRecipientCancelIcon: {
color: ColorPalette.white,
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.warning,
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: ColorPalette.white,
flex: 1
},
/**
* The message text itself.
*/
@@ -132,25 +115,6 @@ export default {
borderTopRightRadius: 0
},
replyWrapper: {
alignItems: 'center',
flexDirection: 'row'
},
replyStyles: {
iconStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 22,
margin: BoxModel.margin / 2
}
},
privateNotice: {
color: ColorPalette.warning,
fontSize: 13,
fontStyle: 'italic'
},
sendButtonIcon: {
color: ColorPalette.darkGrey,
fontSize: 22

View File

@@ -14,7 +14,6 @@ import AbstractChat, {
import ChatInput from './ChatInput';
import DisplayNameForm from './DisplayNameForm';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
/**
* React Component for holding the chat feature in a side panel that slides in
@@ -117,10 +116,7 @@ class Chat extends AbstractChat<Props> {
<MessageContainer
messages = { this.props._messages }
ref = { this._messageContainerRef } />
<MessageRecipient />
<ChatInput
onResize = { this._onChatInputResize }
onSend = { this.props._onSendMessage } />
<ChatInput onResize = { this._onChatInputResize } />
</>
);
}

View File

@@ -8,6 +8,8 @@ import type { Dispatch } from 'redux';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { sendMessage } from '../../actions';
import SmileysPanel from './SmileysPanel';
/**
@@ -26,11 +28,6 @@ type Props = {
*/
onResize: ?Function,
/**
* Callback to invoke on message send.
*/
onSend: Function,
/**
* Invoked to obtain translated strings.
*/
@@ -166,7 +163,7 @@ class ChatInput extends Component<Props, State> {
const trimmed = this.state.message.trim();
if (trimmed) {
this.props.onSend(trimmed);
this.props.dispatch(sendMessage(trimmed));
this.setState({ message: '' });
}

View File

@@ -10,7 +10,6 @@ import { Linkify } from '../../../base/react';
import AbstractChatMessage, {
type Props
} from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
/**
* Renders a single chat message.
@@ -24,10 +23,23 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/
render() {
const { message } = this.props;
const messageToDisplay = message.messageType === 'error'
? this.props.t('chat.error', {
error: message.error,
originalText: message.message
})
: message.message;
// replace links and smileys
// Strophe already escapes special symbols on sending,
// so we escape here only tags to avoid double &amp;
const escMessage = messageToDisplay.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br/>');
const processedMessage = [];
// content is an array of text and emoji components
const content = toArray(this._getMessageText(), { className: 'smiley' });
const content = toArray(escMessage, { className: 'smiley' });
content.forEach(i => {
if (typeof i === 'string') {
@@ -39,19 +51,11 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<div className = 'chatmessage-wrapper'>
<div className = 'replywrapper'>
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
</div>
{ message.privateMessage && message.messageType !== 'local'
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false } /> }
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>
@@ -60,10 +64,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
_getFormattedTimestamp: () => string;
_getMessageText: () => string;
_getPrivateNoticeMessage: () => string;
/**
* Renders the display name of the sender.
*
@@ -77,19 +77,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
);
}
/**
* Renders the message privacy notice.
*
* @returns {React$Element<*>}
*/
_renderPrivateNotice() {
return (
<div className = 'privatemessagenotice'>
{ this._getPrivateNoticeMessage() }
</div>
);
}
/**
* Renders the time at which the message was sent.
*

View File

@@ -1,42 +0,0 @@
/* @flow */
import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
/**
* Implements a component for the dialog displayed to avoid mis-sending private messages.
*/
class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
cancelKey = 'dialog.sendPrivateMessageCancel'
okKey = 'dialog.sendPrivateMessageOk'
onCancel = { this._onSendGroupMessage }
onSubmit = { this._onSendPrivateMessage }
titleKey = 'dialog.sendPrivateMessageTitle'
width = 'small'>
<div>
{ this.props.t('dialog.sendPrivateMessage') }
</div>
</Dialog>
);
}
_onSendGroupMessage: () => boolean;
_onSendPrivateMessage: () => boolean;
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

View File

@@ -1,48 +0,0 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
} from '../AbstractMessageRecipient';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
if (!_privateMessageRecipient) {
return null;
}
const { t } = this.props;
return (
<div id = 'chat-recipient'>
<span>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
</span>
<div onClick = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection } />
</div>
</div>
);
}
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

View File

@@ -2,4 +2,3 @@
export { default as Chat } from './Chat';
export { default as ChatCounter } from './ChatCounter';
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

View File

@@ -1,17 +1,14 @@
// @flow
import UIUtil from '../../../modules/UI/util/UIUtil';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import {
CONFERENCE_JOINED,
getCurrentConference
} from '../base/conference';
import { openDialog } from '../base/dialog';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import {
JitsiConferenceErrors,
JitsiConferenceEvents
} from '../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName
} from '../base/participants';
@@ -19,23 +16,14 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { isButtonEnabled, showToolbox } from '../toolbox';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { SEND_MESSAGE } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
import { ChatPrivacyDialog } from './components';
import { INCOMING_MSG_SOUND_ID } from './constants';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
declare var APP: Object;
declare var interfaceConfig : Object;
/**
* Timeout for when to show the privacy notice after a private message was received.
*
* E.g. if this value is 20 secs (20000ms), then we show the privacy notice when sending a non private
* message after we have received a private message in the last 20 seconds.
*/
const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
/**
* Implements the middleware of the chat feature.
*
@@ -43,16 +31,14 @@ const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const { dispatch } = store;
switch (action.type) {
case APP_WILL_MOUNT:
dispatch(
store.dispatch(
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
break;
case APP_WILL_UNMOUNT:
dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
break;
case CONFERENCE_JOINED:
@@ -60,43 +46,18 @@ MiddlewareRegistry.register(store => next => action => {
break;
case SEND_MESSAGE: {
const state = store.getState();
const { conference } = state['features/base/conference'];
const { conference } = store.getState()['features/base/conference'];
if (conference) {
// There may be cases when we intend to send a private message but we forget to set the
// recipient. This logic tries to mitigate this risk.
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
const escapedMessage = UIUtil.escapeHtml(action.message);
if (shouldSendPrivateMessageTo) {
dispatch(openDialog(ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo
}));
} else {
// Sending the message if privacy notice doesn't need to be shown.
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(action.message);
}
const { privateMessageRecipient } = state['features/chat'];
if (privateMessageRecipient) {
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
} else {
conference.sendTextMessage(action.message);
}
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(escapedMessage);
}
conference.sendTextMessage(escapedMessage);
}
break;
}
case SET_PRIVATE_MESSAGE_RECIPIENT: {
_maybeFocusField();
break;
}
}
return next(action);
@@ -142,10 +103,10 @@ StateListenerRegistry.register(
* @private
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
function _addChatMsgListener(conference, { dispatch, getState }) {
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|| (typeof APP !== 'undefined' && !isButtonEnabled('chat'))
|| store.getState()['features/base/config'].iAmRecorder) {
|| getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we're in filmStripOnly mode, or
// the chat button is not enabled in interfaceConfig.
// or we are in iAmRecorder mode
@@ -155,199 +116,44 @@ function _addChatMsgListener(conference, store) {
conference.on(
JitsiConferenceEvents.MESSAGE_RECEIVED,
(id, message, timestamp, nick) => {
_handleReceivedMessage(store, {
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
if (!isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}
// Provide a default for for the case when a message is being
// backfilled for a participant that has left the conference.
const participant = getParticipantById(state, id) || {};
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp
? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
dispatch(addMessage({
displayName,
hasRead,
id,
messageType: participant.local ? 'local' : 'remote',
message,
nick,
privateMessage: false,
timestamp
});
timestamp: millisecondsTimestamp
}));
if (typeof APP !== 'undefined') {
// Logic for web only:
APP.API.notifyReceivedChatMessage({
body: message,
id,
nick: displayName,
ts: timestamp
});
dispatch(showToolbox(4000));
}
}
);
conference.on(
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
(id, message, timestamp) => {
_handleReceivedMessage(store, {
id,
message,
privateMessage: true,
timestamp,
nick: undefined
});
}
);
conference.on(
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
});
}
/**
* Handles a chat error received from the xmpp server.
*
* @param {Store} store - The Redux store.
* @param {string} error - The error message.
* @returns {void}
*/
function _handleChatError({ dispatch }, error) {
dispatch(addMessage({
hasRead: true,
messageType: 'error',
message: error,
privateMessage: false,
timestamp: Date.now()
}));
}
/**
* Function to handle an incoming chat message.
*
* @param {Store} store - The Redux store.
* @param {Object} message - The message object.
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, privateMessage, timestamp }) {
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
if (!isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}
// Provide a default for for the case when a message is being
// backfilled for a participant that has left the conference.
const participant = getParticipantById(state, id) || {};
const localParticipant = getLocalParticipant(getState);
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp
? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
dispatch(addMessage({
displayName,
hasRead,
id,
messageType: participant.local ? 'local' : 'remote',
message,
privateMessage,
recipient: getParticipantDisplayName(state, localParticipant.id),
timestamp: millisecondsTimestamp
}));
if (typeof APP !== 'undefined') {
// Logic for web only:
APP.API.notifyReceivedChatMessage({
body: message,
id,
nick: displayName,
ts: timestamp
});
dispatch(showToolbox(4000));
}
}
/**
* Focuses the chat text field on web after the message recipient was updated, if needed.
*
* @returns {void}
*/
function _maybeFocusField() {
if (navigator.product !== 'ReactNative') {
const textField = document.getElementById('usermsg');
textField && textField.focus();
}
}
/**
* Persists the sent private messages as if they were received over the muc.
*
* This is required as we rely on the fact that we receive all messages from the muc that we send
* (as they are sent to everybody), but we don't receive the private messages we send to another participant.
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
*
* @param {Store} store - The Redux store.
* @param {string} recipientID - The ID of the recipient the private message was sent to.
* @param {string} message - The sent message.
* @returns {void}
*/
function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message) {
const localParticipant = getLocalParticipant(getState);
const displayName = getParticipantDisplayName(getState, localParticipant.id);
dispatch(addMessage({
displayName,
hasRead: true,
id: localParticipant.id,
messageType: 'local',
message,
privateMessage: true,
recipient: getParticipantDisplayName(getState, recipientID),
timestamp: Date.now()
}));
}
/**
* Returns the ID of the participant who we may have wanted to send the message
* that we're about to send.
*
* @param {Object} state - The Redux state.
* @param {Object} action - The action being dispatched now.
* @returns {string?}
*/
function _shouldSendPrivateMessageTo(state, action): ?string {
if (action.ignorePrivacy) {
// Shortcut: this is only true, if we already displayed the notice, so no need to show it again.
return undefined;
}
const { messages, privateMessageRecipient } = state['features/chat'];
if (privateMessageRecipient) {
// We're already sending a private message, no need to warn about privacy.
return undefined;
}
if (!messages.length) {
// No messages yet, no need to warn for privacy.
return undefined;
}
// Platforms sort messages differently
const lastMessage = navigator.product === 'ReactNative'
? messages[0] : messages[messages.length - 1];
if (lastMessage.messageType === 'local') {
// The sender is probably aware of any private messages as already sent
// a message since then. Doesn't make sense to display the notice now.
return undefined;
}
if (lastMessage.privateMessage) {
// We show the notice if the last received message was private.
return lastMessage.id;
}
// But messages may come rapidly, we want to protect our users from mis-sending a message
// even when there was a reasonable recently received private message.
const now = Date.now();
const recentPrivateMessages = messages.filter(
message =>
message.messageType !== 'local'
&& message.privateMessage
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
const recentPrivateMessage = navigator.product === 'ReactNative'
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
if (recentPrivateMessage) {
return recentPrivateMessage.id;
}
return undefined;
}

View File

@@ -2,18 +2,12 @@
import { ReducerRegistry } from '../base/redux';
import {
ADD_MESSAGE,
CLEAR_MESSAGES,
SET_PRIVATE_MESSAGE_RECIPIENT,
TOGGLE_CHAT
} from './actionTypes';
import { ADD_MESSAGE, CLEAR_MESSAGES, TOGGLE_CHAT } from './actionTypes';
const DEFAULT_STATE = {
isOpen: false,
lastReadMessage: undefined,
messages: [],
privateMessageRecipient: undefined
messages: []
};
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
@@ -25,8 +19,6 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
id: action.id,
messageType: action.messageType,
message: action.message,
privateMessage: action.privateMessage,
recipient: action.recipient,
timestamp: action.timestamp
};
@@ -56,20 +48,12 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
messages: []
};
case SET_PRIVATE_MESSAGE_RECIPIENT:
return {
...state,
isOpen: Boolean(action.participant) || state.isOpen,
privateMessageRecipient: action.participant
};
case TOGGLE_CHAT:
return {
...state,
isOpen: !state.isOpen,
lastReadMessage: state.messages[
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1]
};
}

View File

@@ -16,7 +16,6 @@ import { TestConnectionInfo } from '../../../base/testing';
import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync';
import { Chat } from '../../../chat';
import { DisplayNameLabel } from '../../../display-name';
import { SharedDocument } from '../../../etherpad';
import {
FILMSTRIP_SIZE,
Filmstrip,
@@ -162,78 +161,6 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
return (
<Container style = { styles.conference }>
<StatusBar
barStyle = 'light-content'
hidden = { true }
translucent = { true } />
{ this._renderContent() }
</Container>
);
}
_onClick: () => void;
/**
* Changes the value of the toolboxVisible state, thus allowing us to switch
* between Toolbox and Filmstrip and change their visibility.
*
* @private
* @returns {void}
*/
_onClick() {
this._setToolboxVisible(!this.props._toolboxVisible);
}
_onHardwareBackPress: () => boolean;
/**
* Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
* (if supported) or leaves the associated {@code Conference} otherwise.
*
* @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
*/
_onHardwareBackPress() {
let p;
if (this.props._pictureInPictureEnabled) {
const { PictureInPicture } = NativeModules;
p = PictureInPicture.enterPictureInPicture();
} else {
p = Promise.reject(new Error('PiP not enabled'));
}
p.catch(() => {
this.props.dispatch(appNavigate(undefined));
});
return true;
}
/**
* Renders the conference notification badge if the feature is enabled.
*
* @private
* @returns {React$Node}
*/
_renderConferenceNotification() {
const { _calendarEnabled, _reducedUI } = this.props;
return (
_calendarEnabled && !_reducedUI
? <ConferenceNotification />
: undefined);
}
/**
* Renders the content for the Conference container.
*
* @private
* @returns {React$Element}
*/
_renderContent() {
const {
_connecting,
_filmstripVisible,
@@ -245,15 +172,15 @@ class Conference extends AbstractConference<Props, *> {
const showGradient = _toolboxVisible;
const applyGradientStretching = _filmstripVisible && isNarrowAspectRatio(this) && !_shouldDisplayTileView;
if (_reducedUI) {
return this._renderContentForReducedUi();
}
return (
<>
<AddPeopleDialog />
<Container style = { styles.conference }>
<StatusBar
barStyle = 'light-content'
hidden = { true }
translucent = { true } />
<Chat />
<SharedDocument />
<AddPeopleDialog />
{/*
* The LargeVideo is the lowermost stacking layer.
@@ -266,7 +193,7 @@ class Conference extends AbstractConference<Props, *> {
{/*
* If there is a ringing call, show the callee's info.
*/
<CalleeInfoContainer />
_reducedUI || <CalleeInfoContainer />
}
{/*
@@ -326,37 +253,71 @@ class Conference extends AbstractConference<Props, *> {
pointerEvents = 'box-none'
style = { styles.navBarSafeView }>
<NavigationBar />
{ this._renderNotificationsContainer() }
{ this.renderNotificationsContainer() }
</SafeAreaView>
<TestConnectionInfo />
{ this._renderConferenceNotification() }
</>
{
this._renderConferenceNotification()
}
</Container>
);
}
_onClick: () => void;
/**
* Renders the content for the Conference container when in "reduced UI" mode.
* Changes the value of the toolboxVisible state, thus allowing us to switch
* between Toolbox and Filmstrip and change their visibility.
*
* @private
* @returns {React$Element}
* @returns {void}
*/
_renderContentForReducedUi() {
const { _connecting } = this.props;
_onClick() {
this._setToolboxVisible(!this.props._toolboxVisible);
}
_onHardwareBackPress: () => boolean;
/**
* Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
* (if supported) or leaves the associated {@code Conference} otherwise.
*
* @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
*/
_onHardwareBackPress() {
let p;
if (this.props._pictureInPictureEnabled) {
const { PictureInPicture } = NativeModules;
p = PictureInPicture.enterPictureInPicture();
} else {
p = Promise.reject(new Error('PiP not enabled'));
}
p.catch(() => {
this.props.dispatch(appNavigate(undefined));
});
return true;
}
/**
* Renders the conference notification badge if the feature is enabled.
*
* @private
* @returns {React$Node}
*/
_renderConferenceNotification() {
const { _calendarEnabled, _reducedUI } = this.props;
return (
<>
<LargeVideo onClick = { this._onClick } />
{
_connecting
&& <TintedView>
<LoadingIndicator />
</TintedView>
}
</>
);
_calendarEnabled && !_reducedUI
? <ConferenceNotification />
: undefined);
}
/**
@@ -366,7 +327,7 @@ class Conference extends AbstractConference<Props, *> {
* @private
* @returns {React$Element}
*/
_renderNotificationsContainer() {
renderNotificationsContainer() {
const notificationsStyle = {};
// In the landscape mode (wide) there's problem with notifications being

View File

@@ -5,6 +5,7 @@ import React from 'react';
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
import { obtainConfig } from '../../../base/config';
import { connect, disconnect } from '../../../base/connection';
import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
@@ -22,6 +23,7 @@ import {
} from '../../../toolbox';
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
import logger from '../../logger';
import Labels from './Labels';
import { default as Notice } from './Notice';
@@ -121,7 +123,31 @@ class Conference extends AbstractConference<Props, *> {
*/
componentDidMount() {
document.title = interfaceConfig.APP_NAME;
this._start();
const { configLocation } = config;
if (configLocation) {
obtainConfig(configLocation, this.props._room)
.then(() => {
const now = window.performance.now();
APP.connectionTimes['configuration.fetched'] = now;
logger.log('(TIME) configuration fetched:\t', now);
this._start();
})
.catch(err => {
logger.log(err);
// Show obtain config error.
APP.UI.messageHandler.showError({
descriptionKey: 'dialog.connectError',
titleKey: 'connection.CONNFAIL'
});
});
} else {
this._start();
}
}
/**

View File

@@ -1,3 +1,13 @@
/**
* The type of the action which signals document editing has been enabled.
*
* {
* type: ETHERPAD_INITIALIZED
* }
*/
export const ETHERPAD_INITIALIZED = 'ETHERPAD_INITIALIZED';
/**
* The type of the action which signals document editing has stopped or started.
*
@@ -5,16 +15,8 @@
* type: SET_DOCUMENT_EDITING_STATUS
* }
*/
export const SET_DOCUMENT_EDITING_STATUS = 'SET_DOCUMENT_EDITING_STATUS';
/**
* The type of the action which updates the shared document URL.
*
* {
* type: SET_DOCUMENT_URL
* }
*/
export const SET_DOCUMENT_URL = 'SET_DOCUMENT_URL';
export const SET_DOCUMENT_EDITING_STATUS
= 'SET_DOCUMENT_EDITING_STATUS';
/**
* The type of the action which signals to start or stop editing a shared

View File

@@ -1,8 +1,8 @@
// @flow
import {
ETHERPAD_INITIALIZED,
SET_DOCUMENT_EDITING_STATUS,
SET_DOCUMENT_URL,
TOGGLE_DOCUMENT_EDITING
} from './actionTypes';
@@ -24,18 +24,15 @@ export function setDocumentEditingState(editing: boolean) {
}
/**
* Dispatches an action to set the shared document URL.
* Dispatches an action to set Etherpad as having been initialized.
*
* @param {string} documentUrl - The shared document URL.
* @returns {{
* type: SET_DOCUMENT_URL,
* documentUrl: string
* type: ETHERPAD_INITIALIZED
* }}
*/
export function setDocumentUrl(documentUrl: ?string) {
export function setEtherpadHasInitialzied() {
return {
type: SET_DOCUMENT_URL,
documentUrl
type: ETHERPAD_INITIALIZED
};
}

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