Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6f906b9ca | ||
|
|
d74e43ddcc | ||
|
|
23ddce122b | ||
|
|
814bd26c07 | ||
|
|
2e4b39c19c | ||
|
|
3ee65748bb | ||
|
|
4fa800b87a | ||
|
|
9338b3cf94 | ||
|
|
45e09af692 | ||
|
|
9d32f48ab8 | ||
|
|
290e7baead | ||
|
|
e780ae00d0 | ||
|
|
9ea224412d | ||
|
|
cd8ae07698 | ||
|
|
433a73e13d | ||
|
|
1e558f4da6 | ||
|
|
aef6e33c91 | ||
|
|
acd83ede2f | ||
|
|
bd51613e62 | ||
|
|
246cb39003 | ||
|
|
3b54c527b6 | ||
|
|
18368fefaa | ||
|
|
c361e1e31a | ||
|
|
e3d4152e32 | ||
|
|
d861ba1876 | ||
|
|
c942017b73 | ||
|
|
743d12e326 | ||
|
|
d371a3d5fd | ||
|
|
e1056126e6 | ||
|
|
72c267fbf3 | ||
|
|
0ed85b9d25 | ||
|
|
a8877d82b6 | ||
|
|
8896b0adf3 | ||
|
|
60b14e9b45 | ||
|
|
631e853b40 | ||
|
|
b409c8cc2f | ||
|
|
905212b109 | ||
|
|
05b7df26e6 | ||
|
|
1268afd3f8 | ||
|
|
c1b9b7a623 | ||
|
|
0b9160fb75 | ||
|
|
93c9419392 | ||
|
|
6121bcf171 | ||
|
|
702144180c | ||
|
|
d2b2f98941 | ||
|
|
ec95956e25 | ||
|
|
d6d7ce1b67 | ||
|
|
4cb36b0a5d | ||
|
|
2b3aea76a9 | ||
|
|
f50a31b4e8 | ||
|
|
b226c3aca3 | ||
|
|
4979666a89 | ||
|
|
c9636f85b9 | ||
|
|
436bc87a86 | ||
|
|
e89c2b242d | ||
|
|
02b26a65bb | ||
|
|
6a3e4bb59f | ||
|
|
b01ad360da | ||
|
|
c7f3740099 | ||
|
|
554595acd7 | ||
|
|
ee4ddd5446 | ||
|
|
ebab617a12 | ||
|
|
bc5d92a452 | ||
|
|
2f388dfb6a | ||
|
|
73a0197eb2 | ||
|
|
b6990e9e5d | ||
|
|
26e119bfc2 | ||
|
|
9f866ae608 | ||
|
|
023359b9d2 | ||
|
|
2128d047e1 | ||
|
|
a89349c5b9 | ||
|
|
d109b8beb6 | ||
|
|
9b40572921 | ||
|
|
aaf7a38cce | ||
|
|
1ed0759a50 | ||
|
|
213b73da6e | ||
|
|
5b6985fc5c | ||
|
|
538af01bf5 | ||
|
|
92d0589a37 | ||
|
|
f3269070b2 | ||
|
|
d93bd3eda7 | ||
|
|
0dbbc5d8b6 | ||
|
|
08efd5ecab | ||
|
|
dba1bcb0e3 | ||
|
|
348403abff | ||
|
|
c290cf45b7 | ||
|
|
175c8e6e50 | ||
|
|
f90667b23c | ||
|
|
cf69d591e4 | ||
|
|
e599491583 | ||
|
|
d1520773cf | ||
|
|
573ca97b6c | ||
|
|
0d97f14a1a | ||
|
|
b8f28abfdf | ||
|
|
9ac7c97e67 | ||
|
|
52b3eaacb5 | ||
|
|
9b01ae6db9 | ||
|
|
469487ad36 | ||
|
|
176c3c1601 | ||
|
|
94391234cc | ||
|
|
d84901f196 | ||
|
|
c6b117565d | ||
|
|
2a9124acb5 | ||
|
|
401a783d6a | ||
|
|
39483a30b6 | ||
|
|
0e2a07f8d7 | ||
|
|
36f5b0218d | ||
|
|
a1b3c56de7 | ||
|
|
6d664f133e | ||
|
|
732a433ec1 | ||
|
|
f7dcd1ba2c | ||
|
|
55a8b44224 | ||
|
|
e29db31d91 | ||
|
|
183d3c3ca4 | ||
|
|
c57e713696 | ||
|
|
4519f26adf | ||
|
|
c26f9cc01f | ||
|
|
f6f730b994 |
@@ -28,6 +28,8 @@ node_modules/react-native/flow
|
||||
flow/
|
||||
|
||||
[options]
|
||||
emoji=true
|
||||
|
||||
module.system=haste
|
||||
|
||||
experimental.strict_type_args=true
|
||||
@@ -40,11 +42,11 @@ suppress_type=$FlowIssue
|
||||
suppress_type=$FlowFixMe
|
||||
suppress_type=$FixMe
|
||||
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-7]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-7]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-8]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-8]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
[version]
|
||||
^0.37.0
|
||||
^0.38.0
|
||||
|
||||
1
.gitignore
vendored
@@ -53,7 +53,6 @@ yarn-error.log
|
||||
#
|
||||
buck-out/
|
||||
\.buckd/
|
||||
android/app/libs
|
||||
*.keystore
|
||||
|
||||
# fastlane
|
||||
|
||||
@@ -5,8 +5,14 @@ debian/
|
||||
libs/
|
||||
node_modules/
|
||||
|
||||
# The following are checked by ESLint which supersedes JSHint.
|
||||
# The following are checked by ESLint with the maximum configuration which
|
||||
# supersedes JSHint.
|
||||
flow-typed/
|
||||
react/
|
||||
|
||||
# The following are checked by ESLint with the minimum configuration which does
|
||||
# not supersede JSHint but take advantage of advanced language features such as
|
||||
# Facebook Flow which are not supported by JSHint.
|
||||
modules/translation/translation.js
|
||||
|
||||
analytics.js
|
||||
|
||||
@@ -19,6 +19,10 @@ You can download Debian/Ubuntu binaries:
|
||||
* [testing](https://download.jitsi.org/testing/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianTestingRepository))
|
||||
* [nightly](https://download.jitsi.org/unstable/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianNightlyRepository))
|
||||
|
||||
You can get our mobile versoins from here:
|
||||
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
|
||||
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
## Building the sources
|
||||
|
||||
Jitsi Meet uses [Browserify](http://browserify.org). If you want to make changes in the code you need to [install Browserify](http://browserify.org/#install). Browserify requires [nodejs](http://nodejs.org).
|
||||
|
||||
@@ -91,7 +91,7 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode Integer.parseInt("${version}")
|
||||
versionName "1.2.${version}"
|
||||
versionName "1.3.${version}"
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'x86'
|
||||
}
|
||||
@@ -139,6 +139,7 @@ if (project.hasProperty('JITSI_SIGNING')
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':react-native-background-timer')
|
||||
compile project(':react-native-immersive')
|
||||
compile project(':react-native-keep-awake')
|
||||
compile project(':react-native-vector-icons')
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:name=".MainActivity">
|
||||
android:name=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
||||
@@ -29,6 +29,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
new com.corbt.keepawake.KCKeepAwakePackage(),
|
||||
new com.facebook.react.shell.MainReactPackage(),
|
||||
new com.oblador.vectoricons.VectorIconsPackage(),
|
||||
new com.ocetnik.timer.BackgroundTimerPackage(),
|
||||
new com.oney.WebRTCModule.WebRTCModulePackage(),
|
||||
new com.rnimmersive.RNImmersivePackage(),
|
||||
new org.jitsi.meet.audiomode.AudioModePackage()
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 38 KiB |
@@ -5,7 +5,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.3.1'
|
||||
classpath 'com.android.tools.build:gradle:2.2.3'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
rootProject.name = 'jitsi-meet-react'
|
||||
|
||||
include ':app'
|
||||
include ':react-native-background-timer'
|
||||
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
|
||||
include ':react-native-immersive'
|
||||
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
|
||||
include ':react-native-keep-awake'
|
||||
|
||||
176
conference.js
@@ -20,6 +20,30 @@ import analytics from './modules/analytics/analytics';
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
conferenceFailed,
|
||||
conferenceJoined,
|
||||
conferenceLeft,
|
||||
EMAIL_COMMAND
|
||||
} from './react/features/base/conference';
|
||||
import {
|
||||
isFatalJitsiConnectionError
|
||||
} from './react/features/base/lib-jitsi-meet';
|
||||
import {
|
||||
changeParticipantAvatarID,
|
||||
changeParticipantAvatarURL,
|
||||
changeParticipantEmail,
|
||||
participantJoined,
|
||||
participantLeft,
|
||||
participantRoleChanged
|
||||
} from './react/features/base/participants';
|
||||
import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
} from './react/features/overlay';
|
||||
|
||||
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
|
||||
@@ -46,12 +70,12 @@ import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
|
||||
* Known custom conference commands.
|
||||
*/
|
||||
const commands = {
|
||||
EMAIL: "email",
|
||||
AVATAR_URL: "avatar-url",
|
||||
AVATAR_ID: "avatar-id",
|
||||
AVATAR_ID: AVATAR_ID_COMMAND,
|
||||
AVATAR_URL: AVATAR_URL_COMMAND,
|
||||
CUSTOM_ROLE: "custom-role",
|
||||
EMAIL: EMAIL_COMMAND,
|
||||
ETHERPAD: "etherpad",
|
||||
SHARED_VIDEO: "shared-video",
|
||||
CUSTOM_ROLE: "custom-role"
|
||||
SHARED_VIDEO: "shared-video"
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,7 +115,10 @@ function createInitialLocalTracksAndConnect(roomName) {
|
||||
|
||||
JitsiMeetJS.mediaDevices.addEventListener(
|
||||
JitsiMeetJS.events.mediaDevices.PERMISSION_PROMPT_IS_SHOWN,
|
||||
browser => APP.UI.showUserMediaPermissionsGuidanceOverlay(browser));
|
||||
browser =>
|
||||
APP.store.dispatch(
|
||||
mediaPermissionPromptVisibilityChanged(true, browser))
|
||||
);
|
||||
|
||||
// First try to retrieve both audio and video.
|
||||
let tryCreateLocalTracks = createLocalTracks(
|
||||
@@ -109,8 +136,7 @@ function createInitialLocalTracksAndConnect(roomName) {
|
||||
|
||||
return Promise.all([ tryCreateLocalTracks, connect(roomName) ])
|
||||
.then(([tracks, con]) => {
|
||||
APP.UI.hideUserMediaPermissionsGuidanceOverlay();
|
||||
|
||||
APP.store.dispatch(mediaPermissionPromptVisibilityChanged(false));
|
||||
if (audioAndVideoError) {
|
||||
if (audioOnlyError) {
|
||||
// If both requests for 'audio' + 'video' and 'audio' only
|
||||
@@ -139,6 +165,29 @@ function sendData (command, value) {
|
||||
room.sendCommand(command, {value: value});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up initially the properties of the local participant - email, avatarID,
|
||||
* avatarURL, displayName, etc.
|
||||
*/
|
||||
function _setupLocalParticipantProperties() {
|
||||
const email = APP.settings.getEmail();
|
||||
email && sendData(commands.EMAIL, email);
|
||||
|
||||
const avatarUrl = APP.settings.getAvatarUrl();
|
||||
avatarUrl && sendData(commands.AVATAR_URL, avatarUrl);
|
||||
|
||||
if (!email && !avatarUrl) {
|
||||
sendData(commands.AVATAR_ID, APP.settings.getAvatarId());
|
||||
}
|
||||
|
||||
let nick = APP.settings.getDisplayName();
|
||||
if (config.useNicks && !nick) {
|
||||
nick = APP.UI.askForNickname();
|
||||
APP.settings.setDisplayName(nick);
|
||||
}
|
||||
nick && room.setDisplayName(nick);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user nickname by user id.
|
||||
* @param {string} id user id
|
||||
@@ -334,6 +383,7 @@ class ConferenceConnector {
|
||||
this._reject(err);
|
||||
}
|
||||
_onConferenceFailed(err, ...params) {
|
||||
APP.store.dispatch(conferenceFailed(room, err, ...params));
|
||||
logger.error('CONFERENCE FAILED:', err, ...params);
|
||||
APP.UI.hideRingOverLay();
|
||||
switch (err) {
|
||||
@@ -408,8 +458,6 @@ class ConferenceConnector {
|
||||
// the app. Both the errors above are unrecoverable from the library
|
||||
// perspective.
|
||||
room.leave().then(() => connection.disconnect());
|
||||
APP.UI.showPageReloadOverlay(
|
||||
false /* not a network type of failure */, err);
|
||||
break;
|
||||
|
||||
case ConferenceErrors.CONFERENCE_MAX_USERS:
|
||||
@@ -466,6 +514,23 @@ function disconnect() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles CONNECTION_FAILED events from lib-jitsi-meet.
|
||||
*
|
||||
* @param {JitsiMeetJS.connection.error} error - The reported error.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function _connectionFailedHandler(error) {
|
||||
if (isFatalJitsiConnectionError(error)) {
|
||||
APP.connection.removeEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED,
|
||||
_connectionFailedHandler);
|
||||
if (room)
|
||||
room.leave();
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
isModerator: false,
|
||||
audioMuted: false,
|
||||
@@ -518,11 +583,13 @@ export default {
|
||||
return createInitialLocalTracksAndConnect(options.roomName);
|
||||
}).then(([tracks, con]) => {
|
||||
logger.log('initialized with %s local tracks', tracks.length);
|
||||
con.addEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED,
|
||||
_connectionFailedHandler);
|
||||
APP.connection = connection = con;
|
||||
this.isDesktopSharingEnabled =
|
||||
JitsiMeetJS.isDesktopSharingEnabled();
|
||||
APP.remoteControl.init();
|
||||
this._bindConnectionFailedHandler(con);
|
||||
this._createRoom(tracks);
|
||||
|
||||
if (UIUtil.isButtonEnabled('contacts')
|
||||
@@ -561,47 +628,6 @@ export default {
|
||||
isLocalId (id) {
|
||||
return this.getMyUserId() === id;
|
||||
},
|
||||
/**
|
||||
* Binds a handler that will handle the case when the connection is dropped
|
||||
* in the middle of the conference.
|
||||
* @param {JitsiConnection} connection the connection to which the handler
|
||||
* will be bound to.
|
||||
* @private
|
||||
*/
|
||||
_bindConnectionFailedHandler (connection) {
|
||||
const handler = function (error, errMsg) {
|
||||
/* eslint-disable no-case-declarations */
|
||||
switch (error) {
|
||||
case ConnectionErrors.CONNECTION_DROPPED_ERROR:
|
||||
case ConnectionErrors.OTHER_ERROR:
|
||||
case ConnectionErrors.SERVER_ERROR:
|
||||
|
||||
logger.error("XMPP connection error: " + errMsg);
|
||||
|
||||
// From all of the cases above only CONNECTION_DROPPED_ERROR
|
||||
// is considered a network type of failure
|
||||
const isNetworkFailure
|
||||
= error === ConnectionErrors.CONNECTION_DROPPED_ERROR;
|
||||
|
||||
APP.UI.showPageReloadOverlay(
|
||||
isNetworkFailure,
|
||||
"xmpp-conn-dropped:" + errMsg);
|
||||
|
||||
connection.removeEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED, handler);
|
||||
|
||||
// FIXME it feels like the conference should be stopped
|
||||
// by lib-jitsi-meet
|
||||
if (room)
|
||||
room.leave();
|
||||
|
||||
break;
|
||||
}
|
||||
/* eslint-enable no-case-declarations */
|
||||
};
|
||||
connection.addEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED, handler);
|
||||
},
|
||||
/**
|
||||
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
|
||||
* @param mute true for mute and false for unmute.
|
||||
@@ -676,7 +702,7 @@ export default {
|
||||
* false.
|
||||
*/
|
||||
isCallstatsEnabled () {
|
||||
return room.isCallstatsEnabled();
|
||||
return room && room.isCallstatsEnabled();
|
||||
},
|
||||
/**
|
||||
* Sends the given feedback through CallStats if enabled.
|
||||
@@ -887,21 +913,7 @@ export default {
|
||||
this.invite = new Invite(room);
|
||||
this._room = room; // FIXME do not use this
|
||||
|
||||
let email = APP.settings.getEmail();
|
||||
email && sendData(this.commands.defaults.EMAIL, email);
|
||||
|
||||
let avatarUrl = APP.settings.getAvatarUrl();
|
||||
avatarUrl && sendData(this.commands.defaults.AVATAR_URL,
|
||||
avatarUrl);
|
||||
!email && sendData(
|
||||
this.commands.defaults.AVATAR_ID, APP.settings.getAvatarId());
|
||||
|
||||
let nick = APP.settings.getDisplayName();
|
||||
if (config.useNicks && !nick) {
|
||||
nick = APP.UI.askForNickname();
|
||||
APP.settings.setDisplayName(nick);
|
||||
}
|
||||
nick && room.setDisplayName(nick);
|
||||
_setupLocalParticipantProperties();
|
||||
|
||||
this._setupListeners();
|
||||
},
|
||||
@@ -1128,11 +1140,17 @@ export default {
|
||||
_setupListeners () {
|
||||
// add local streams when joined to the conference
|
||||
room.on(ConferenceEvents.CONFERENCE_JOINED, () => {
|
||||
APP.store.dispatch(conferenceJoined(room));
|
||||
|
||||
APP.UI.mucJoined();
|
||||
APP.API.notifyConferenceJoined(APP.conference.roomName);
|
||||
APP.UI.markVideoInterrupted(false);
|
||||
});
|
||||
|
||||
room.on(
|
||||
ConferenceEvents.CONFERENCE_LEFT,
|
||||
(...args) => APP.store.dispatch(conferenceLeft(room, ...args)));
|
||||
|
||||
room.on(
|
||||
ConferenceEvents.AUTH_STATUS_CHANGED,
|
||||
function (authEnabled, authLogin) {
|
||||
@@ -1146,6 +1164,12 @@ export default {
|
||||
if (user.isHidden())
|
||||
return;
|
||||
|
||||
APP.store.dispatch(participantJoined({
|
||||
id,
|
||||
name: user.getDisplayName(),
|
||||
role: user.getRole()
|
||||
}));
|
||||
|
||||
logger.log('USER %s connnected', id, user);
|
||||
APP.API.notifyUserJoined(id);
|
||||
APP.UI.addUser(user);
|
||||
@@ -1154,6 +1178,7 @@ export default {
|
||||
APP.UI.updateUserRole(user);
|
||||
});
|
||||
room.on(ConferenceEvents.USER_LEFT, (id, user) => {
|
||||
APP.store.dispatch(participantLeft(id, user));
|
||||
logger.log('USER %s LEFT', id, user);
|
||||
APP.API.notifyUserLeft(id);
|
||||
APP.UI.removeUser(id, user.getDisplayName());
|
||||
@@ -1162,6 +1187,7 @@ export default {
|
||||
|
||||
|
||||
room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
|
||||
APP.store.dispatch(participantRoleChanged(id, role));
|
||||
if (this.isLocalId(id)) {
|
||||
logger.info(`My role changed, new role: ${role}`);
|
||||
if (this.isModerator !== room.isModerator()) {
|
||||
@@ -1229,7 +1255,8 @@ export default {
|
||||
|
||||
room.on(ConferenceEvents.TALK_WHILE_MUTED, () => {
|
||||
APP.UI.showToolbar(6000);
|
||||
UIUtil.animateShowElement($("#talkWhileMutedPopup"), true, 5000);
|
||||
|
||||
APP.UI.showCustomToolbarPopup('#talkWhileMutedPopup', true, 5000);
|
||||
});
|
||||
|
||||
/*
|
||||
@@ -1364,6 +1391,7 @@ export default {
|
||||
});
|
||||
|
||||
room.on(ConferenceEvents.SUSPEND_DETECTED, () => {
|
||||
APP.store.dispatch(suspendDetected());
|
||||
// After wake up, we will be in a state where conference is left
|
||||
// there will be dialog shown to user.
|
||||
// We do not want video/audio as we show an overlay and after it
|
||||
@@ -1384,9 +1412,6 @@ export default {
|
||||
if (localAudio) {
|
||||
localAudio.dispose();
|
||||
}
|
||||
|
||||
// show overlay
|
||||
APP.UI.showSuspendedOverlay();
|
||||
});
|
||||
|
||||
room.on(ConferenceEvents.DTMF_SUPPORT_CHANGED, (isDTMFSupported) => {
|
||||
@@ -1424,17 +1449,22 @@ export default {
|
||||
|
||||
APP.UI.addListener(UIEvents.EMAIL_CHANGED, this.changeLocalEmail);
|
||||
room.addCommandListener(this.commands.defaults.EMAIL, (data, from) => {
|
||||
APP.store.dispatch(changeParticipantEmail(from, data.value));
|
||||
APP.UI.setUserEmail(from, data.value);
|
||||
});
|
||||
|
||||
room.addCommandListener(
|
||||
this.commands.defaults.AVATAR_URL,
|
||||
(data, from) => {
|
||||
APP.store.dispatch(
|
||||
changeParticipantAvatarURL(from, data.value));
|
||||
APP.UI.setUserAvatarUrl(from, data.value);
|
||||
});
|
||||
|
||||
room.addCommandListener(this.commands.defaults.AVATAR_ID,
|
||||
(data, from) => {
|
||||
APP.store.dispatch(
|
||||
changeParticipantAvatarID(from, data.value));
|
||||
APP.UI.setUserAvatarID(from, data.value);
|
||||
});
|
||||
|
||||
@@ -1845,6 +1875,7 @@ export default {
|
||||
if (email === APP.settings.getEmail()) {
|
||||
return;
|
||||
}
|
||||
APP.store.dispatch(changeParticipantEmail(room.myUserId(), email));
|
||||
|
||||
APP.settings.setEmail(email);
|
||||
APP.UI.setUserEmail(room.myUserId(), email);
|
||||
@@ -1861,6 +1892,7 @@ export default {
|
||||
if (url === APP.settings.getAvatarUrl()) {
|
||||
return;
|
||||
}
|
||||
APP.store.dispatch(changeParticipantAvatarURL(room.myUserId(), url));
|
||||
|
||||
APP.settings.setAvatarUrl(url);
|
||||
APP.UI.setUserAvatarUrl(room.myUserId(), url);
|
||||
|
||||
10
config.js
@@ -40,7 +40,7 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
// up to and including 41. On Firefox 42 and higher, we will run without the
|
||||
// extension.
|
||||
// If set to -1, an extension will be required for all versions of Firefox.
|
||||
desktopSharingFirefoxMaxVersionExtRequired: -1,
|
||||
desktopSharingFirefoxMaxVersionExtRequired: 51,
|
||||
// The URL to the Firefox extension for desktop sharing.
|
||||
desktopSharingFirefoxExtensionURL: null,
|
||||
|
||||
@@ -53,8 +53,6 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
disableStats: false,
|
||||
disableAudioLevels: false,
|
||||
channelLastN: -1, // The default value of the channel attribute last-n.
|
||||
adaptiveLastN: false,
|
||||
//disableAdaptiveSimulcast: false,
|
||||
enableRecording: false,
|
||||
enableWelcomePage: true,
|
||||
//enableClosePage: false, // enabling the close page will ignore the welcome
|
||||
@@ -79,6 +77,8 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
enableUserRolesBasedOnToken: false,
|
||||
// Suspending video might cause problems with audio playback. Disabling until these are fixed.
|
||||
disableSuspendVideo: true,
|
||||
// disables or enables RTX (RFC 4588).
|
||||
disableRtx: true
|
||||
// disables or enables RTX (RFC 4588) (defaults to false).
|
||||
disableRtx: false,
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 360.
|
||||
resolution: 720
|
||||
};
|
||||
|
||||
@@ -4,6 +4,14 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||
import jitsiLocalStorage from './modules/util/JitsiLocalStorage';
|
||||
|
||||
import {
|
||||
connectionEstablished,
|
||||
connectionFailed
|
||||
} from './react/features/base/connection';
|
||||
import {
|
||||
isFatalJitsiConnectionError
|
||||
} from './react/features/base/lib-jitsi-meet';
|
||||
|
||||
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
|
||||
@@ -67,6 +75,18 @@ function connect(id, password, roomName) {
|
||||
connection.addEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED, handleConnectionFailed
|
||||
);
|
||||
connection.addEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED, connectionFailedHandler);
|
||||
|
||||
function connectionFailedHandler(error, errMsg) {
|
||||
APP.store.dispatch(connectionFailed(connection, error, errMsg));
|
||||
|
||||
if (isFatalJitsiConnectionError(error)) {
|
||||
connection.removeEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED,
|
||||
connectionFailedHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribe() {
|
||||
connection.removeEventListener(
|
||||
@@ -80,6 +100,7 @@ function connect(id, password, roomName) {
|
||||
}
|
||||
|
||||
function handleConnectionEstablished() {
|
||||
APP.store.dispatch(connectionEstablished(connection));
|
||||
unsubscribe();
|
||||
resolve(connection);
|
||||
}
|
||||
|
||||
@@ -3,21 +3,25 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html, body{
|
||||
margin:0px;
|
||||
height:100%;
|
||||
color: $defaultColor;
|
||||
body {
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
background: #000000;
|
||||
overflow: hidden;
|
||||
color: $defaultColor;
|
||||
background: $defaultBackground;
|
||||
&.filmstrip-only {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html, body, input, textarea, keygen, select, button {
|
||||
body, input, textarea, keygen, select, button {
|
||||
font-family: $baseFontFamily !important;
|
||||
}
|
||||
|
||||
@@ -122,6 +126,7 @@ form {
|
||||
z-index: $tooltipsZ;
|
||||
&-inner {
|
||||
background-color: $tooltipBg;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
|
||||
@@ -1,34 +1,10 @@
|
||||
/*Initialize*/
|
||||
ul.loginmenu {
|
||||
font-family: $baseFontFamily;
|
||||
line-height: normal;
|
||||
display:none;
|
||||
div.loginmenu {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
padding-bottom: 7px;
|
||||
top: 45px;
|
||||
left: -5px;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
border: 1px solid rgba(256, 256, 256, 0.2);
|
||||
border-radius:8px;
|
||||
}
|
||||
|
||||
ul.loginmenu li {
|
||||
list-style-type: none;
|
||||
padding: 7px;
|
||||
color: #fff;
|
||||
font-size: 11pt;
|
||||
cursor: default;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
ul.loginmenu:after {
|
||||
content: url('../images/dropdownPointer.png');
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 18px;
|
||||
top: 40px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
a.disabled {
|
||||
@@ -36,23 +12,7 @@ a.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loginmenuPadding {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.loginmenu.extendedToolbarPopup {
|
||||
left: 55px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
ul.loginmenu.extendedToolbarPopup:after {
|
||||
content: url('../images/leftDropdownPointer.png');
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: -7px;
|
||||
top: 20px;
|
||||
left: 40px;
|
||||
}
|
||||
200
css/_mixins.scss
@@ -2,52 +2,52 @@
|
||||
* Animation mixin.
|
||||
*/
|
||||
@mixin animation($animate...) {
|
||||
$max: length($animate);
|
||||
$animations: '';
|
||||
$max: length($animate);
|
||||
$animations: '';
|
||||
|
||||
@for $i from 1 through $max {
|
||||
$animations: #{$animations + nth($animate, $i)};
|
||||
@for $i from 1 through $max {
|
||||
$animations: #{$animations + nth($animate, $i)};
|
||||
|
||||
@if $i < $max {
|
||||
$animations: #{$animations + ", "};
|
||||
@if $i < $max {
|
||||
$animations: #{$animations + ", "};
|
||||
}
|
||||
}
|
||||
}
|
||||
-webkit-animation: $animations;
|
||||
-moz-animation: $animations;
|
||||
-o-animation: $animations;
|
||||
animation: $animations;
|
||||
-webkit-animation: $animations;
|
||||
-moz-animation: $animations;
|
||||
-o-animation: $animations;
|
||||
animation: $animations;
|
||||
}
|
||||
|
||||
@mixin flex() {
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyframes mixin.
|
||||
*/
|
||||
@mixin keyframes($animationName) {
|
||||
@-webkit-keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@-moz-keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@-o-keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@-webkit-keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@-moz-keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@-o-keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
@keyframes #{$animationName} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin circle($diameter) {
|
||||
width: $diameter;
|
||||
height: $diameter;
|
||||
border-radius: 50%;
|
||||
width: $diameter;
|
||||
height: $diameter;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,10 +60,10 @@
|
||||
}
|
||||
|
||||
@mixin absoluteAligning() {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
@include transform(translate(-50%, -50%));
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
@include transform(translate(-50%, -50%));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,81 +75,115 @@
|
||||
}
|
||||
|
||||
@mixin transform($func) {
|
||||
-moz-transform: $func;
|
||||
-ms-transform: $func;
|
||||
-webkit-transform: $func;
|
||||
-o-transform: $func;
|
||||
transform: $func;
|
||||
-moz-transform: $func;
|
||||
-ms-transform: $func;
|
||||
-webkit-transform: $func;
|
||||
-o-transform: $func;
|
||||
transform: $func;
|
||||
}
|
||||
|
||||
@mixin transition($transition...) {
|
||||
-moz-transition: $transition;
|
||||
-o-transition: $transition;
|
||||
-webkit-transition: $transition;
|
||||
transition: $transition;
|
||||
-moz-transition: $transition;
|
||||
-o-transition: $transition;
|
||||
-webkit-transition: $transition;
|
||||
transition: $transition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin styling placeholder
|
||||
**/
|
||||
* Mixin styling a placeholder.
|
||||
**/
|
||||
@mixin placeholder() {
|
||||
$selectors: (
|
||||
"::-webkit-input-placeholder",
|
||||
"::-moz-placeholder",
|
||||
":-moz-placeholder",
|
||||
":-ms-input-placeholder"
|
||||
);
|
||||
$selectors: (
|
||||
"::-webkit-input-placeholder",
|
||||
"::-moz-placeholder",
|
||||
":-moz-placeholder",
|
||||
":-ms-input-placeholder"
|
||||
);
|
||||
|
||||
@each $selector in $selectors {
|
||||
#{$selector} {
|
||||
@content;
|
||||
@each $selector in $selectors {
|
||||
#{$selector} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin styling a slider track for different browsers.
|
||||
**/
|
||||
@mixin slider() {
|
||||
$selectors: (
|
||||
"input[type=range]::-webkit-slider-runnable-track",
|
||||
"input[type=range]::-moz-range-track",
|
||||
"input[type=range]::-ms-track"
|
||||
);
|
||||
|
||||
@each $selector in $selectors {
|
||||
#{$selector} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixin styling a slider thumb for different browsers.
|
||||
**/
|
||||
@mixin slider-thumb() {
|
||||
$selectors: (
|
||||
"input[type=range]::-webkit-slider-thumb",
|
||||
"input[type=range]::-moz-range-thumb",
|
||||
"input[type=range]::-ms-thumb"
|
||||
);
|
||||
|
||||
@each $selector in $selectors {
|
||||
#{$selector} {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin box-shadow($h, $y, $blur, $color, $inset: false) {
|
||||
@if $inset {
|
||||
-webkit-box-shadow: inset $h $y $blur $color;
|
||||
-moz-box-shadow: inset $h $y $blur $color;
|
||||
box-shadow: inset $h $y $blur $color;
|
||||
} @else {
|
||||
-webkit-box-shadow: $h $y $blur $color;
|
||||
-moz-box-shadow: $h $y $blur $color;
|
||||
box-shadow: $h $y $blur $color;
|
||||
}
|
||||
@if $inset {
|
||||
-webkit-box-shadow: inset $h $y $blur $color;
|
||||
-moz-box-shadow: inset $h $y $blur $color;
|
||||
box-shadow: inset $h $y $blur $color;
|
||||
} @else {
|
||||
-webkit-box-shadow: $h $y $blur $color;
|
||||
-moz-box-shadow: $h $y $blur $color;
|
||||
box-shadow: $h $y $blur $color;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin no-box-shadow {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@mixin box-sizing($box-model) {
|
||||
-webkit-box-sizing: $box-model; // Safari <= 5
|
||||
-moz-box-sizing: $box-model; // Firefox <= 19
|
||||
box-sizing: $box-model;
|
||||
-webkit-box-sizing: $box-model; // Safari <= 5
|
||||
-moz-box-sizing: $box-model; // Firefox <= 19
|
||||
box-sizing: $box-model;
|
||||
}
|
||||
|
||||
@mixin border-radius($radius) {
|
||||
-webkit-border-radius: $radius;
|
||||
border-radius: $radius;
|
||||
/* stops bg color from leaking outside the border: */
|
||||
background-clip: padding-box;
|
||||
-webkit-border-radius: $radius;
|
||||
border-radius: $radius;
|
||||
/* stops bg color from leaking outside the border: */
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
@mixin opacity($opacity) {
|
||||
opacity: $opacity;
|
||||
$opacity-ie: $opacity * 100;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=$opacity-ie);
|
||||
filter: alpha(opacity=$opacity-ie); //IE8
|
||||
opacity: $opacity;
|
||||
$opacity-ie: $opacity * 100;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=$opacity-ie);
|
||||
filter: alpha(opacity=$opacity-ie); //IE8
|
||||
}
|
||||
|
||||
@mixin text-truncate {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,5 +191,5 @@
|
||||
* (opacity) value.
|
||||
*/
|
||||
@mixin transparentBg($color, $alpha) {
|
||||
background-color: rgba(red($color), green($color), blue($color), $alpha);
|
||||
background-color: rgba(red($color), green($color), blue($color), $alpha);
|
||||
}
|
||||
@@ -23,7 +23,8 @@
|
||||
}
|
||||
|
||||
// Link Appearance
|
||||
&__link {
|
||||
&__link,
|
||||
&__contents {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
@@ -40,11 +41,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
&__text,
|
||||
&__slider {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__slider {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
html, body {
|
||||
width: 100%;
|
||||
height:100%;
|
||||
color: $defaultColor;
|
||||
background: $defaultBackground;
|
||||
}
|
||||
|
||||
.redirectPageMessage {
|
||||
width: 30%;
|
||||
margin: 20% auto;
|
||||
|
||||
@@ -16,7 +16,7 @@ $defaultToolbarSize: 50px;
|
||||
$thumbnailToolbarHeight: 22px;
|
||||
$thumbnailIndicatorBorder: 2px;
|
||||
$thumbnailIndicatorSize: $thumbnailToolbarHeight;
|
||||
$thumbnailVideoMargin: 5px;
|
||||
$thumbnailVideoMargin: 2px;
|
||||
$thumbnailsBorder: 2px;
|
||||
$thumbnailVideoBorder: 2px;
|
||||
$hideFilmstripButtonWidth: 17px;
|
||||
@@ -129,8 +129,13 @@ $linkFontColor: #489afe;
|
||||
$linkHoverFontColor: #287ade;
|
||||
|
||||
/**
|
||||
* Landing
|
||||
* Unsupported browser
|
||||
*/
|
||||
$primaryUnsupportedBrowserButtonBgColor: #17a0db;
|
||||
$unsupportedBrowserButtonBgColor: #ff9a00;
|
||||
$unsupportedBrowserTextColor: #4a4a4a;
|
||||
$unsupportedBrowserTextSmallFontSize: 17px;
|
||||
$unsupportedBrowserTitleColor: #fff;
|
||||
$unsupportedBrowserTitleFontSize: 24px;
|
||||
$unsupportedDesktopBrowserTextColor: rgba(255, 255, 255, 0.7);
|
||||
$unsupportedDesktopBrowserTextFontSize: 21px;
|
||||
|
||||
41
css/components/_input-slider.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Disable the default webkit styles for range inputs (sliders).
|
||||
*/
|
||||
input[type=range]{
|
||||
-webkit-appearance: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the default focus styles for webkit range inputs (sliders).
|
||||
*/
|
||||
input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the mixin for a range input style.
|
||||
*/
|
||||
@include slider {
|
||||
background: $sliderTrackBackground;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the mixin for a range input thumb style.
|
||||
*/
|
||||
@include slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
background: white;
|
||||
border: 1px solid $sliderThumbBackground;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0px 1px $sliderThumbBackground;
|
||||
cursor: pointer;
|
||||
height: 14px;
|
||||
margin-top: -4px;
|
||||
width: 14px;
|
||||
}
|
||||
@@ -8,4 +8,13 @@
|
||||
text-decoration: underline;
|
||||
@include transition(color .1s ease-in);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper links are links that are meant to open a documentation page or more
|
||||
* detailed info.
|
||||
*/
|
||||
.helper-link {
|
||||
@extend .link;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -60,14 +60,14 @@
|
||||
@import 'components/link';
|
||||
@import 'shortcuts/main';
|
||||
@import 'components/button-control';
|
||||
@import 'components/_input-control.scss';
|
||||
@import 'components/input-control';
|
||||
@import 'components/input-slider';
|
||||
@import "modals/invite/invite";
|
||||
@import "connection-info";
|
||||
@import 'aui-components/dropdown';
|
||||
@import '404';
|
||||
@import 'policy';
|
||||
@import 'filmstrip';
|
||||
@import 'unsupported-browser/unsupported-desktop-browser';
|
||||
@import 'unsupported-browser/unsupported-mobile-browser';
|
||||
@import 'unsupported-browser/main';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.reload_overlay_msg {
|
||||
.reload_overlay_text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 30px;
|
||||
@@ -13,4 +13,4 @@
|
||||
#reloadProgressBar {
|
||||
width: 180px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ $baseLight: #FFFFFF;
|
||||
*/
|
||||
$controlBackground: $baseLight;
|
||||
$controlColor: #333333;
|
||||
$sliderTrackBackground: #474747;
|
||||
$sliderThumbBackground: #3572b0;
|
||||
|
||||
/**
|
||||
* Buttons
|
||||
|
||||
3
css/unsupported-browser/_main.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
@import 'no-mobile-app';
|
||||
@import 'unsupported-desktop-browser';
|
||||
@import 'unsupported-mobile-browser';
|
||||
21
css/unsupported-browser/_no-mobile-app.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
.no-mobile-app {
|
||||
margin: 30% auto 0;
|
||||
max-width: 25em;
|
||||
text-align: center;
|
||||
width: auto;
|
||||
|
||||
&__title {
|
||||
border-bottom: 1px solid $auiBorderColor;
|
||||
color: $unsupportedBrowserTitleColor;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
padding-bottom: em(17, 24);
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $unsupportedBrowserTextSmallFontSize;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
@@ -1,132 +1,39 @@
|
||||
.supported-browser {
|
||||
color: #929391;
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
margin: 1em 7px;
|
||||
vertical-align: middle;
|
||||
width: 138px;
|
||||
.unsupported-desktop-browser {
|
||||
@include absoluteAligning();
|
||||
|
||||
&__button {
|
||||
background-color: #62c82a;
|
||||
border: 1px solid #3c8117;
|
||||
border-radius: 10px;
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
height: 26px;
|
||||
margin: 15px auto 0px auto;
|
||||
padding-top: 13px;
|
||||
text-align: center;
|
||||
width: 115px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
&__title {
|
||||
color: $unsupportedBrowserTitleColor;
|
||||
font-weight: 300;
|
||||
font-size: $unsupportedBrowserTitleFontSize;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
color: $unsupportedDesktopBrowserTextColor;
|
||||
font-size: $unsupportedDesktopBrowserTextFontSize;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 16px;
|
||||
|
||||
&_small {
|
||||
@extend .unsupported-desktop-browser__description;
|
||||
font-size: $unsupportedBrowserTextSmallFontSize;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: #087dba;
|
||||
text-decoration: none;
|
||||
color: $linkFontColor;
|
||||
@include transition(color .1s ease-out);
|
||||
|
||||
&:hover {
|
||||
color: $linkHoverFontColor;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
@include transition(color .1s ease-in);
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-list
|
||||
{
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__logo {
|
||||
margin: 20px auto 0px auto;
|
||||
|
||||
&_chrome {
|
||||
background-image: url('../../images/chrome.png');
|
||||
height: 78px;
|
||||
width: 78px;
|
||||
}
|
||||
|
||||
&_chromium {
|
||||
background-image: url('../../images/chromium.png');
|
||||
height: 78px;
|
||||
width: 77px;
|
||||
}
|
||||
|
||||
&_firefox {
|
||||
background-image: url('../../images/firefox.png');
|
||||
height: 80px;
|
||||
width: 86px;
|
||||
}
|
||||
|
||||
&_opera {
|
||||
background-image: url('../../images/opera.png');
|
||||
height: 78px;
|
||||
width: 73px;
|
||||
}
|
||||
|
||||
&_ie {
|
||||
background-image: url('../../images/ie.png');
|
||||
height: 78px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
&_safari {
|
||||
background-image: url('../../images/safari.png');
|
||||
height: 79px;
|
||||
width: 78px;
|
||||
}
|
||||
}
|
||||
|
||||
&__text
|
||||
{
|
||||
line-height: 1.2em;
|
||||
|
||||
&_small {
|
||||
font-size: small;
|
||||
}
|
||||
}
|
||||
|
||||
&__tile {
|
||||
background-color: #e8e8e8;
|
||||
border: 1px solid #cfcfcf;
|
||||
border-radius: 10px;
|
||||
height: 163px;
|
||||
margin-top: 5px;
|
||||
width: 138px;
|
||||
}
|
||||
}
|
||||
|
||||
.unsupported-desktop-browser {
|
||||
display: block;
|
||||
height: 565px;
|
||||
margin: auto;
|
||||
overflow:hidden;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
width:500px;
|
||||
|
||||
&__page {
|
||||
display:inline-block;
|
||||
font-size: 28px;
|
||||
padding-top: 25px;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 auto;
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
background: #fff;
|
||||
display: block;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
2
debian/jitsi-meet-tokens.install
vendored
@@ -1 +1 @@
|
||||
prosody-plugins/ /usr/share/jitsi-meet/
|
||||
resources/prosody-plugins/ /usr/share/jitsi-meet/
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Overview
|
||||
Jitsi Meet supports logging to an [InfluxDB](http://influxdb.com/) database.
|
||||
|
||||
# Configuration
|
||||
The following needs to be done to enable this functionality.
|
||||
|
||||
## Install InfluxDB
|
||||
The details are outside the scope of the document, see http://influxdb.com/download/ .
|
||||
|
||||
## Create an InfluxDB database
|
||||
Use the InfluxDB admin interface (running on port 8083) and create a database. In this example we name it <code>jitsi_database</code>
|
||||
|
||||
## Enable logging for Jitsi Videobridge
|
||||
Add the following properties to <code>/usr/share/jitsi-videobridge/.sip-communicator/sip-communicator.properties</code>.
|
||||
|
||||
- org.jitsi.videobridge.log.INFLUX_DB_ENABLED=true
|
||||
- org.jitsi.videobridge.log.INFLUX_URL_BASE=http://influxdb.example.com:8086
|
||||
- org.jitsi.videobridge.log.INFLUX_DATABASE=jitsi_database
|
||||
- org.jitsi.videobridge.log.INFLUX_USER=user
|
||||
- org.jitsi.videobridge.log.INFLUX_PASS=pass
|
||||
|
||||
## Enable logging for Jicofo
|
||||
Add the same properties as above to <code>/usr/share/jicofo/.sip-communicator/sip-communicator.properties</code>.
|
||||
|
||||
## Enable logging for Jitsi Meet itself
|
||||
Change "logStats" to "true" in <code>/etc/jitsi/meet/you-domain.config.js</code> or the <code>config.js</code> file used in your installation.
|
||||
|
||||
# User interface
|
||||
You can explore the database using the [Jiloin](https://github.com/jitsi/jiloin) web interface.
|
||||
@@ -1,9 +1,9 @@
|
||||
# Jitsi Meet mobile apps
|
||||
|
||||
Jitsi Meet can also be built as a standalone mobile application for
|
||||
iOS and Android. It uses the [React Native]() framework.
|
||||
iOS and Android. It uses the [React Native] framework.
|
||||
|
||||
First make sure the [React Native dependencies]() are installed.
|
||||
First make sure the [React Native dependencies] are installed.
|
||||
|
||||
**NOTE**: This document assumes the app is being built on a macOS system.
|
||||
|
||||
@@ -22,6 +22,8 @@ work properly with the native plugins we require.
|
||||
npm install -g ios-deploy
|
||||
```
|
||||
|
||||
You may need to add ```--unsafe-perm=true``` if you are running on [Mac OS 10.11 or greater](https://github.com/phonegap/ios-deploy#os-x-1011-el-capitan-or-greater).
|
||||
|
||||
2. Build the app
|
||||
|
||||
There are 2 ways to build the app: using the CLI or using Xcode.
|
||||
@@ -62,8 +64,8 @@ work properly with the native plugins we require.
|
||||
|
||||
## Android
|
||||
|
||||
The [React Native dependencies]() page has very detailed information on how to
|
||||
setup [Android Studio]() and the required components for getting the necessary
|
||||
The [React Native dependencies] page has very detailed information on how to
|
||||
setup [Android Studio] and the required components for getting the necessary
|
||||
build environment. Make sure you follow it closely.
|
||||
|
||||
1. Building the app
|
||||
@@ -79,7 +81,7 @@ build environment. Make sure you follow it closely.
|
||||
|
||||
## Debugging
|
||||
|
||||
The official documentation on [debugging]() is quite extensive, it is the
|
||||
The official documentation on [debugging] is quite extensive, it is the
|
||||
preferred method for debugging.
|
||||
|
||||
**NOTE**: When using Chrome Developer Tools for debugging the JavaScript code
|
||||
|
||||
36
flow-typed/npm/react-i18next_v2.x.x.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
// flow-typed signature: 57cf34196930be78935a42e5c8ac3cb6
|
||||
// flow-typed version: ae6284e7b7/react-i18next_v2.x.x/flow_>=v0.36.x_<=v0.39.x
|
||||
|
||||
declare module 'react-i18next' {
|
||||
declare type TFunction = (key?: ?string, data?: ?Object) => string;
|
||||
declare type Locales = string | Array<string>;
|
||||
|
||||
declare type StatelessComponent<P> = (props: P) => ?React$Element<any>;
|
||||
|
||||
declare type Comp<P> = StatelessComponent<P> | Class<React$Component<*, P, *>>;
|
||||
|
||||
declare type Translator<OP, P> = {
|
||||
(component: StatelessComponent<P>): Class<React$Component<void, OP, void>>;
|
||||
<Def, St>(component: Class<React$Component<Def, P, St>>): Class<React$Component<Def, OP, St>>;
|
||||
}
|
||||
|
||||
declare function translate<OP, P>(locales: Locales): Translator<OP, P>;
|
||||
|
||||
declare type NamespacesProps = {
|
||||
components: Array<Comp<*>>,
|
||||
i18n: { loadNamespaces: Function },
|
||||
};
|
||||
|
||||
declare function loadNamespaces(props: NamespacesProps): Promise<void>;
|
||||
|
||||
declare type ProviderProps = { i18n: Object, children: React$Element<any> };
|
||||
|
||||
declare var I18nextProvider: Class<React$Component<void, ProviderProps, void>>;
|
||||
|
||||
declare type InterpolateProps = {
|
||||
children?: React$Element<any>,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
declare var Interpolate: Class<React$Component<void, InterpolateProps, void>>;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 234 B |
|
Before Width: | Height: | Size: 611 B |
@@ -113,6 +113,11 @@
|
||||
|
||||
window.setTimeout(
|
||||
function () { window.location.replace(href); }, delay);
|
||||
|
||||
// Call extra handler if defined.
|
||||
if (typeof postLoadErrorHandler === "function") {
|
||||
postLoadErrorHandler();
|
||||
}
|
||||
};
|
||||
window.removeEventListener(
|
||||
'error', loadErrHandler, true /* capture phase */);
|
||||
|
||||
@@ -70,5 +70,16 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
|
||||
AUDIO_LEVEL_SECONDARY_COLOR: "rgba(255,255,255,0.2)",
|
||||
POLICY_LOGO: null,
|
||||
LOCAL_THUMBNAIL_RATIO: 16/9, //16:9
|
||||
REMOTE_THUMBNAIL_RATIO: 1 //1:1
|
||||
REMOTE_THUMBNAIL_RATIO: 1, //1:1
|
||||
// Documentation reference for the live streaming feature.
|
||||
LIVE_STREAMING_HELP_LINK: "https://jitsi.org/live",
|
||||
|
||||
/**
|
||||
* Whether the mobile app Jitsi Meet is to be promoted to participants
|
||||
* attempting to join a conference in a mobile Web browser. If undefined,
|
||||
* default to true.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
MOBILE_APP_PROMO: true
|
||||
};
|
||||
|
||||
BIN
ios/app/Images.xcassets/AppIcon.appiconset/AppIcon-76@1x.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
ios/app/Images.xcassets/AppIcon.appiconset/AppIcon-76@2x.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
ios/app/Images.xcassets/AppIcon.appiconset/AppIcon-83.5@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
@@ -2,53 +2,103 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29@2x.png",
|
||||
"scale" : "2x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29@3x.png",
|
||||
"scale" : "3x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40@2x.png",
|
||||
"scale" : "2x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@2x.png",
|
||||
"scale" : "3x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@2x.png",
|
||||
"scale" : "2x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60@3x.png",
|
||||
"scale" : "3x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-29@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-40@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-76@1x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-76@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "AppIcon-83.5@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchScreen-480@1x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "LaunchScreen-480@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "LaunchScreen-480@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
ios/app/Images.xcassets/LaunchScreen.imageset/LaunchScreen-480@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 278 KiB |
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2</string>
|
||||
<string>1.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -80,9 +80,10 @@
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
2602576C1D0A7703001E3363 /* jitsi.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2602576B1D0A7703001E3363 /* jitsi.ttf */; };
|
||||
3847F11906B4479A9162628F /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 821D8ABD506944B4BDBB069B /* libRNVectorIcons.a */; };
|
||||
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
|
||||
7FAD39BE09A84D6AB0ABACA8 /* libRNBackgroundTimer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27A0018BBB2C4FD5A4F9CE71 /* libRNBackgroundTimer.a */; };
|
||||
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
|
||||
901FE90FA5744B5B94DCDC41 /* libKCKeepAwake.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EA8C046B2BF46279796F07D /* libKCKeepAwake.a */; };
|
||||
B30EF2311DC0ED7C00690F45 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B30EF2301DC0ED7C00690F45 /* WebRTC.framework */; };
|
||||
@@ -184,6 +185,13 @@
|
||||
remoteGlobalIDString = 58B5119B1A9E6C1200147676;
|
||||
remoteInfo = RCTText;
|
||||
};
|
||||
B332D04E1E54E3170086EA16 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 0965153BB98645B4A8B6AA10 /* RNBackgroundTimer.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 134814201AA4EA6300B7C361;
|
||||
remoteInfo = RNBackgroundTimer;
|
||||
};
|
||||
B3BA19D41DC6B37B00BCD481 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */;
|
||||
@@ -263,6 +271,7 @@
|
||||
00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = "<group>"; };
|
||||
00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = "<group>"; };
|
||||
00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = "<group>"; };
|
||||
0965153BB98645B4A8B6AA10 /* RNBackgroundTimer.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNBackgroundTimer.xcodeproj; path = "../node_modules/react-native-background-timer/ios/RNBackgroundTimer.xcodeproj"; sourceTree = "<group>"; };
|
||||
0B42DFAD1E2FD90700111B12 /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AudioMode.m; path = app/AudioMode.m; sourceTree = "<group>"; };
|
||||
0EA8C046B2BF46279796F07D /* libKCKeepAwake.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libKCKeepAwake.a; sourceTree = "<group>"; };
|
||||
139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = "<group>"; };
|
||||
@@ -277,6 +286,7 @@
|
||||
146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; };
|
||||
22418656B14845609F953A42 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = "<group>"; };
|
||||
2602576B1D0A7703001E3363 /* jitsi.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = jitsi.ttf; path = ../android/app/src/main/assets/fonts/jitsi.ttf; sourceTree = "<group>"; };
|
||||
27A0018BBB2C4FD5A4F9CE71 /* libRNBackgroundTimer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNBackgroundTimer.a; sourceTree = "<group>"; };
|
||||
5B09C20C78C74A548AAAC1FA /* KCKeepAwake.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = KCKeepAwake.xcodeproj; path = "../node_modules/react-native-keep-awake/ios/KCKeepAwake.xcodeproj"; sourceTree = "<group>"; };
|
||||
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
|
||||
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
|
||||
@@ -315,7 +325,7 @@
|
||||
BF9643881C34FBC800B0BBDF /* GLKit.framework in Frameworks */,
|
||||
B30EF2311DC0ED7C00690F45 /* WebRTC.framework in Frameworks */,
|
||||
BF96438E1C34FBE100B0BBDF /* VideoToolbox.framework in Frameworks */,
|
||||
146834051AC3E58100842450 /* libReact.a in Frameworks */,
|
||||
901FE90FA5744B5B94DCDC41 /* libKCKeepAwake.a in Frameworks */,
|
||||
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */,
|
||||
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */,
|
||||
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */,
|
||||
@@ -327,8 +337,9 @@
|
||||
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
|
||||
BFC745141CB829B300673F38 /* libRCTWebRTC.a in Frameworks */,
|
||||
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
|
||||
146834051AC3E58100842450 /* libReact.a in Frameworks */,
|
||||
7FAD39BE09A84D6AB0ABACA8 /* libRNBackgroundTimer.a in Frameworks */,
|
||||
3847F11906B4479A9162628F /* libRNVectorIcons.a in Frameworks */,
|
||||
901FE90FA5744B5B94DCDC41 /* libKCKeepAwake.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -488,6 +499,7 @@
|
||||
BFC7450D1CB829A700673F38 /* RCTWebRTC.xcodeproj */,
|
||||
139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */,
|
||||
146833FF1AC3E56700842450 /* React.xcodeproj */,
|
||||
0965153BB98645B4A8B6AA10 /* RNBackgroundTimer.xcodeproj */,
|
||||
22418656B14845609F953A42 /* RNVectorIcons.xcodeproj */,
|
||||
);
|
||||
name = Libraries;
|
||||
@@ -524,6 +536,14 @@
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B332D0301E54E3170086EA16 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B332D04F1E54E3170086EA16 /* libRNBackgroundTimer.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B3BA19B71DC6B02F00BCD481 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -653,6 +673,10 @@
|
||||
ProductGroup = 146834001AC3E56700842450 /* Products */;
|
||||
ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = B332D0301E54E3170086EA16 /* Products */;
|
||||
ProjectRef = 0965153BB98645B4A8B6AA10 /* RNBackgroundTimer.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 26D589F81D0B42EE00FC396B /* Products */;
|
||||
ProjectRef = 22418656B14845609F953A42 /* RNVectorIcons.xcodeproj */;
|
||||
@@ -806,6 +830,13 @@
|
||||
remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
B332D04F1E54E3170086EA16 /* libRNBackgroundTimer.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = libRNBackgroundTimer.a;
|
||||
remoteRef = B332D04E1E54E3170086EA16 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
B3BA19D51DC6B37B00BCD481 /* libRCTImage-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
@@ -952,6 +983,7 @@
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-background-timer/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-keep-awake/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
|
||||
);
|
||||
@@ -969,6 +1001,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios;
|
||||
PRODUCT_NAME = "jitsi-meet-react";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -988,6 +1021,7 @@
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native/Libraries/LinkingIOS/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-background-timer/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-keep-awake/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
|
||||
);
|
||||
@@ -1005,6 +1039,7 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios;
|
||||
PRODUCT_NAME = "jitsi-meet-react";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1050,6 +1085,7 @@
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-background-timer/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-keep-awake/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
|
||||
);
|
||||
@@ -1095,6 +1131,7 @@
|
||||
"$(inherited)",
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
|
||||
"$(SRCROOT)/../node_modules/react-native/React/**",
|
||||
"$(SRCROOT)/../node_modules/react-native-background-timer/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-keep-awake/ios",
|
||||
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
|
||||
);
|
||||
|
||||
@@ -13,5 +13,7 @@
|
||||
"sk": "Slowakisch",
|
||||
"sl": "Slowenisch",
|
||||
"sv": "Schwedisch",
|
||||
"tr": "Türkisch"
|
||||
"tr": "Türkisch",
|
||||
"zhCN": "Chinesisch (China)",
|
||||
"nb": "Norwegisch (Bokmal)"
|
||||
}
|
||||
18
lang/languages-nb.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": "",
|
||||
"zhCN": ""
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"en": "English (английский)",
|
||||
"en": "Английский",
|
||||
"bg": "Болгарский",
|
||||
"de": "Немецкий",
|
||||
"es": "Испанский",
|
||||
@@ -7,11 +7,12 @@
|
||||
"hy": "Армянский",
|
||||
"it": "Итальянский",
|
||||
"oc": "Окситанский",
|
||||
"pl": "",
|
||||
"pl": "Польский",
|
||||
"ptBR": "Португальский (Бразилия)",
|
||||
"ru": "",
|
||||
"ru": "Русский",
|
||||
"sk": "Словацкий",
|
||||
"sl": "Словенский",
|
||||
"sv": "Шведский",
|
||||
"tr": "Турецкий"
|
||||
"tr": "Турецкий",
|
||||
"zhCN": "Китайский (Китай)"
|
||||
}
|
||||
17
lang/languages-zhCN.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": ""
|
||||
}
|
||||
@@ -13,5 +13,7 @@
|
||||
"sk": "Slovak",
|
||||
"sl": "Slovenian",
|
||||
"sv": "Swedish",
|
||||
"tr": "Turkish"
|
||||
"tr": "Turkish",
|
||||
"zhCN": "Chinese (China)",
|
||||
"nb": "Norwegian Bokmal"
|
||||
}
|
||||
|
||||
@@ -39,41 +39,46 @@
|
||||
"videoMute": "Kamera starten oder stoppen"
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "Los",
|
||||
"roomname": "Konferenzname eingeben",
|
||||
"disable": "Diesen Hinweis nicht mehr anzeigen",
|
||||
"feature1": {
|
||||
"title": "Einfach zu benutzen",
|
||||
"content": "Kein Download nötig. __app__ läuft direkt im Browser. Einfach die Konferenzadresse teilen und los geht's."
|
||||
"content": "Kein Download nötig. __app__ läuft direkt im Browser. Einfach die Konferenzadresse teilen und los geht's.",
|
||||
"title": "Einfach zu benutzen"
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Niedrige Bandbreite",
|
||||
"content": "Videokonferenzen mit mehreren Teilnehmen mit weniger als 128Kpbs. Bildschirmfreigaben und Telefonkonferenzen kommen sogar mit noch weniger Bandbreite aus."
|
||||
"content": "Videokonferenzen mit mehreren Teilnehmen mit weniger als 128Kpbs. Bildschirmfreigaben und Telefonkonferenzen kommen sogar mit noch weniger Bandbreite aus.",
|
||||
"title": "Niedrige Bandbreite"
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Open Source",
|
||||
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
|
||||
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten.",
|
||||
"title": "Open Source"
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Unbegrenzte Anzahl Benutzer",
|
||||
"content": "Es gibt keine künstliche Beschränkung der Anzahl der Benutzer oder Konferenzteilnehmer. Die Leistung des Servers und die Bandbreite sind die einzigen limitierenden Faktoren."
|
||||
"content": "Es gibt keine künstliche Beschränkung der Anzahl der Benutzer oder Konferenzteilnehmer. Die Leistung des Servers und die Bandbreite sind die einzigen limitierenden Faktoren.",
|
||||
"title": "Unbegrenzte Anzahl Benutzer"
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Bildschirmfreigabe",
|
||||
"content": "Es ist ganz einfach den Bildschirm zu teilen. __app__ ist ideal für Online-Präsentationen, Vorlesungen und Fernwartungsanfragen."
|
||||
"content": "Es ist ganz einfach den Bildschirm zu teilen. __app__ ist ideal für Online-Präsentationen, Vorlesungen und Fernwartungsanfragen.",
|
||||
"title": "Bildschirmfreigabe"
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Sichere Konferenzen",
|
||||
"content": "Privatsphäre gewünscht? __app__ Konferenzen können mit einem Passwort geschützt werden um ungebetene Gäste fernzuhalten und Unterbrechungen zu vermeiden."
|
||||
"content": "Privatsphäre gewünscht? __app__ Konferenzen können mit einem Passwort geschützt werden um ungebetene Gäste fernzuhalten und Unterbrechungen zu vermeiden.",
|
||||
"title": "Sichere Konferenzen"
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Freigegebene Notizen",
|
||||
"content": "__app__ verwendet Etherpad, ein Editor zur kollaborativen Bearbeitung von Texten."
|
||||
"content": "__app__ verwendet Etherpad, ein Editor zur kollaborativen Bearbeitung von Texten.",
|
||||
"title": "Freigegebene Notizen"
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Benutzungsstatistiken",
|
||||
"content": "Die Verwendung kann durch die Integration mit Piwik, Google Analytics und anderen Überwachungs- und Statistikprogrammen protokolliert werden."
|
||||
}
|
||||
"content": "Die Verwendung kann durch die Integration mit Piwik, Google Analytics und anderen Überwachungs- und Statistikprogrammen protokolliert werden.",
|
||||
"title": "Benutzungsstatistiken"
|
||||
},
|
||||
"go": "Los",
|
||||
"join": "Beitreten",
|
||||
"privacy": "Privatsphäre",
|
||||
"roomname": "Konferenzname eingeben",
|
||||
"roomnamePlaceHolder": "Konferenzname",
|
||||
"sendFeedback": "Senden Sie uns Ihr Feedback",
|
||||
"terms": "Bedingungen"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -110,6 +115,13 @@
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "Hand erheben"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appInstalled": "oder wenn die App bereits installiert ist<br/>, <strong>dann</strong>",
|
||||
"appNotInstalled": "Sie benötigen <strong>__app__</strong> um der Konferenz mit dem Mobilgerät beizutreten",
|
||||
"downloadApp": "App herunterladen",
|
||||
"joinConversation": "Der Konferenz beitreten",
|
||||
"startConference": "Konferenz starten"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Chat öffnen / schliessen",
|
||||
"filmstrip": "Videos anzeigen / verbergen",
|
||||
@@ -154,7 +166,8 @@
|
||||
"kick": "Hinauswerfen",
|
||||
"muted": "Stummgeschaltet",
|
||||
"domute": "Stummschalten",
|
||||
"flip": "Spiegeln"
|
||||
"flip": "Spiegeln",
|
||||
"remoteControl": "Fernsteuerung"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "Verbindungsdaten",
|
||||
@@ -312,7 +325,12 @@
|
||||
"externalInstallationMsg": "Die Bildschirmfreigabeerweiterung muss installiert werden.",
|
||||
"muteParticipantTitle": "Teilnehmer stummschalten?",
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Teilnehmer nicht aufheben, aber ein Teilnehmer kann seine eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantButton": "Stummschalten"
|
||||
"muteParticipantButton": "Stummschalten",
|
||||
"remoteControlTitle": "Fernsteuerung",
|
||||
"remoteControlDeniedMessage": "__user__ hat die Anfrage zur Fernsteuerung verweigert.",
|
||||
"remoteControlAllowedMessage": "__user__ hat die Anfrage zur Fernsteuerung angenommen.",
|
||||
"remoteControlErrorMessage": "Beim Anfordern der Fernsteuerungsberechtigung von __user__ ist ein Fehler aufgetreten.",
|
||||
"remoteControlStopMessage": "Die Fernsteuerung wurde beendet."
|
||||
},
|
||||
"\u0005dialog": {},
|
||||
"email": {
|
||||
@@ -375,6 +393,7 @@
|
||||
"failedToStart": "Live-Streaming konnte nicht gestartet werden",
|
||||
"buttonTooltip": "Live-Stream starten / stoppen",
|
||||
"streamIdRequired": "Bitte Stream-ID eingeben um das Live-Streaming zu starten.",
|
||||
"streamIdHelp": "Wo ist die Stream-ID zu finden?",
|
||||
"error": "Das Live-Streaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"busy": "All Aufnahmegeräte sind momentan ausgelastet. Bitte versuchen Sie es später noch einmal."
|
||||
}
|
||||
|
||||
362
lang/main-nb.json
Normal file
@@ -0,0 +1,362 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"addParticipants": "",
|
||||
"roomLocked": "",
|
||||
"roomUnlocked": "",
|
||||
"passwordSetRemotely": "",
|
||||
"connectionsettings": "",
|
||||
"poweredby": "",
|
||||
"feedback": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"callingName": "",
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": ""
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": ""
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "",
|
||||
"roomname": "",
|
||||
"disable": "",
|
||||
"feature1": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature2": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature3": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature4": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature5": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature6": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature7": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature8": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
}
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"rejoinKeyTitle": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "",
|
||||
"videomute": "",
|
||||
"authenticate": "",
|
||||
"lock": "",
|
||||
"invite": "",
|
||||
"chat": "",
|
||||
"etherpad": "",
|
||||
"sharedvideo": "",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sip": "",
|
||||
"Settings": "",
|
||||
"hangup": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"dialpad": "",
|
||||
"sharedVideoMutedPopup": "",
|
||||
"micMutedPopup": "",
|
||||
"talkWhileMutedPopup": "",
|
||||
"unableToUnmutePopup": "",
|
||||
"cameraDisabled": "",
|
||||
"micDisabled": "",
|
||||
"filmstrip": "",
|
||||
"profile": "",
|
||||
"raiseHand": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
},
|
||||
"messagebox": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": "",
|
||||
"update": "",
|
||||
"name": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"noDevice": "",
|
||||
"noPermission": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": "",
|
||||
"setPasswordLabel": ""
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "",
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": "",
|
||||
"remoteControl": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural": "",
|
||||
"localport": "",
|
||||
"localport_plural": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural": "",
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural": "",
|
||||
"transport": "",
|
||||
"bandwidth": "",
|
||||
"na": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connected": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": ""
|
||||
},
|
||||
"dialog": {
|
||||
"add": "",
|
||||
"kickMessage": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"error": "",
|
||||
"roomLocked": "",
|
||||
"addPassword": "",
|
||||
"createPassword": "",
|
||||
"detectext": "",
|
||||
"failtoinstall": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"reconnectNow": "",
|
||||
"conferenceReloadTimeLeft": "",
|
||||
"maxUsersLimitReached": "",
|
||||
"lockTitle": "",
|
||||
"lockMessage": "",
|
||||
"warning": "",
|
||||
"passwordNotSupported": "",
|
||||
"internalErrorTitle": "",
|
||||
"internalError": "",
|
||||
"unableToSwitch": "",
|
||||
"SLDFailure": "",
|
||||
"SRDFailure": "",
|
||||
"oops": "",
|
||||
"currentPassword": "",
|
||||
"passwordLabel": "",
|
||||
"defaultError": "",
|
||||
"passwordRequired": "",
|
||||
"Ok": "",
|
||||
"done": "",
|
||||
"Remove": "",
|
||||
"removePassword": "",
|
||||
"shareVideoTitle": "",
|
||||
"shareVideoLinkError": "",
|
||||
"removeSharedVideoTitle": "",
|
||||
"removeSharedVideoMsg": "",
|
||||
"alreadySharedVideoMsg": "",
|
||||
"WaitingForHost": "",
|
||||
"WaitForHostMsg": "",
|
||||
"IamHost": "",
|
||||
"Cancel": "",
|
||||
"Submit": "",
|
||||
"retry": "",
|
||||
"logoutTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"sessTerminated": "",
|
||||
"hungUp": "",
|
||||
"joinAgain": "",
|
||||
"Share": "",
|
||||
"Save": "",
|
||||
"recording": "",
|
||||
"recordingToken": "",
|
||||
"Dial": "",
|
||||
"sipMsg": "",
|
||||
"passwordCheck": "",
|
||||
"passwordMsg": "",
|
||||
"shareLink": "",
|
||||
"settings1": "",
|
||||
"settings2": "",
|
||||
"settings3": "",
|
||||
"yourPassword": "",
|
||||
"Back": "",
|
||||
"serviceUnavailable": "",
|
||||
"gracefulShutdown": "",
|
||||
"Yes": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"password": "",
|
||||
"userPassword": "",
|
||||
"token": "",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"tokenAuthFailed": "",
|
||||
"displayNameRequired": "",
|
||||
"enterDisplayName": "",
|
||||
"extensionRequired": "",
|
||||
"firefoxExtensionPrompt": "",
|
||||
"rateExperience": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"thankYou": "",
|
||||
"sorryFeedback": "",
|
||||
"liveStreaming": "",
|
||||
"streamKey": "",
|
||||
"startLiveStreaming": "",
|
||||
"stopStreamingWarning": "",
|
||||
"stopRecordingWarning": "",
|
||||
"stopLiveStreaming": "",
|
||||
"stopRecording": "",
|
||||
"doNotShowWarningAgain": "",
|
||||
"doNotShowMessageAgain": "",
|
||||
"permissionDenied": "",
|
||||
"screenSharingPermissionDeniedError": "",
|
||||
"micErrorPresent": "",
|
||||
"cameraErrorPresent": "",
|
||||
"cameraUnsupportedResolutionError": "",
|
||||
"cameraUnknownError": "",
|
||||
"cameraPermissionDeniedError": "",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraConstraintFailedError": "",
|
||||
"micUnknownError": "",
|
||||
"micPermissionDeniedError": "",
|
||||
"micNotFoundError": "",
|
||||
"micConstraintFailedError": "",
|
||||
"micNotSendingData": "",
|
||||
"cameraNotSendingData": "",
|
||||
"goToStore": "",
|
||||
"externalInstallationTitle": "",
|
||||
"externalInstallationMsg": "",
|
||||
"muteParticipantTitle": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": "",
|
||||
"remoteControlTitle": "",
|
||||
"remoteControlDeniedMessage": "",
|
||||
"remoteControlAllowedMessage": "",
|
||||
"remoteControlErrorMessage": "",
|
||||
"remoteControlStopMessage": ""
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": "",
|
||||
"subject": "",
|
||||
"body": "",
|
||||
"and": ""
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "",
|
||||
"CONNECTING": "",
|
||||
"RECONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ATTACHED": ""
|
||||
},
|
||||
"recording": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"unavailable": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"unavailable": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"streamIdRequired": "",
|
||||
"streamIdHelp": "",
|
||||
"error": "",
|
||||
"busy": ""
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"contactlist": "Участники (__pcount__)",
|
||||
"addParticipants": "",
|
||||
"roomLocked": "",
|
||||
"roomUnlocked": "",
|
||||
"passwordSetRemotely": "",
|
||||
"roomLocked": "Вызывающие должны ввести пароль",
|
||||
"roomUnlocked": "Любой, владеющий ссылкой, может присоединиться",
|
||||
"passwordSetRemotely": "установлен другим участником",
|
||||
"connectionsettings": "Настройки подключения",
|
||||
"poweredby": "работает на",
|
||||
"feedback": "Оставьте нам свой отзыв",
|
||||
@@ -13,7 +13,7 @@
|
||||
"raisedHand": "Хочет говорить",
|
||||
"defaultNickname": "напр. Яна Цветочкина",
|
||||
"defaultLink": "напр. __url__",
|
||||
"callingName": "",
|
||||
"callingName": "__name__",
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "Пожалуйста, дайте разрешение на доступ к камере и микрофону нажатием на кнопку <b><i>Разрешить</i></b>",
|
||||
"chromeGrantPermissions": "Пожалуйста, дайте разрешение на доступ к камере и микрофону нажатием на кнопку <b><i>Разрешить</i></b>",
|
||||
@@ -25,18 +25,18 @@
|
||||
"nwjsGrantPermissions": "Пожалуйста дайте разрешение на доступ к камере и микрофону"
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": ""
|
||||
"keyboardShortcuts": "Комбинации клавиш",
|
||||
"raiseHand": "Поднять или опустить руку",
|
||||
"pushToTalk": "Нажмите, чтобы говорить",
|
||||
"toggleScreensharing": "Переключиться между камерой и совместным использованием экрана",
|
||||
"toggleFilmstrip": "Показать или скрыть видео",
|
||||
"toggleShortcuts": "Показать или скрыть это справочное меню",
|
||||
"focusLocal": "Фокус на ваше видео",
|
||||
"focusRemote": "Фокус на видео другого абонента",
|
||||
"toggleChat": "Открыть или закрыть чат",
|
||||
"mute": "Заглушить или включить микрофон",
|
||||
"fullScreen": "Войти или выйти из полноэкранного режима",
|
||||
"videoMute": "Включить или выключить вашу камеру"
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "Вперед!",
|
||||
@@ -51,11 +51,11 @@
|
||||
"content": "Многопользовательским видеоконференциям достаточно скорости передачи данных в 128 Кбит/с. Демонстрация экрана или аудиоконференции требуют и того меньше."
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Открытый исходный код",
|
||||
"title": "Исходный код открыт",
|
||||
"content": "__app__ лицензирован под Apache License. Вы можете свободно скачивать, использовать, изменять это ПО в соответствии с условиями лицензии."
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Неограниченное количество пользовательниц",
|
||||
"title": "Количество пользовательниц не ограничено",
|
||||
"content": "Нет никаких искусственных ограничений по количеству пользовательниц или участников конференций. Вас отграничивают только мощность сервера и качество соединения."
|
||||
},
|
||||
"feature5": {
|
||||
@@ -76,44 +76,44 @@
|
||||
}
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
"policyText": " ",
|
||||
"title": "__app__ нуждается в использовании вашего микрофона и камеры."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"rejoinKeyTitle": ""
|
||||
"title": "Ваш видеозвонок был прерван, потому что этот компьютер пошёл спать.",
|
||||
"rejoinKeyTitle": "Присоединиться повторно"
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "Вкл. / Выкл. звук",
|
||||
"videomute": "",
|
||||
"videomute": "Вкл / Выкл камеру",
|
||||
"authenticate": "Аутентифицировать",
|
||||
"lock": "",
|
||||
"invite": "",
|
||||
"chat": "",
|
||||
"etherpad": "",
|
||||
"lock": "Заблокировать / разблокировать комнату",
|
||||
"invite": "Поделиться ссылкой",
|
||||
"chat": "Открыть / Закрыть чат",
|
||||
"etherpad": "Открыть / Закрыть общий документ",
|
||||
"sharedvideo": "Поделиться YouTube видео",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sharescreen": "Начать / Завершить совместное использование экрана",
|
||||
"fullscreen": "Вкл / Выкл полноэкранный режим",
|
||||
"sip": "Набрать SIP номер",
|
||||
"Settings": "Настройки",
|
||||
"hangup": "",
|
||||
"hangup": "Покинуть",
|
||||
"login": "Войти",
|
||||
"logout": "Завершить сеанс",
|
||||
"dialpad": "",
|
||||
"dialpad": "Открыть / Закрыть клавиатуру для набора номера",
|
||||
"sharedVideoMutedPopup": "У видео, которым Вы поделились, отключён звук, чтобы вы могли говорить с остальными.",
|
||||
"micMutedPopup": "Ваш микрофон отключён, чтобы вы могли сосредоточиться на видео, которым поделились.",
|
||||
"talkWhileMutedPopup": "",
|
||||
"talkWhileMutedPopup": "Пытаетесь говорить? Вы приглушены.",
|
||||
"unableToUnmutePopup": "Вы не можете включить звук, потому что включено видео.",
|
||||
"cameraDisabled": "Камера недоступна",
|
||||
"micDisabled": "Микрофон недоступен",
|
||||
"filmstrip": "",
|
||||
"profile": "",
|
||||
"raiseHand": ""
|
||||
"filmstrip": "Показать / Скрыть видео",
|
||||
"profile": "Редактировать ваш профиль",
|
||||
"raiseHand": "Поднять / Опустить вашу руку"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Открыть / Закрыть чат",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
"filmstrip": "Показать / Скрыть видео",
|
||||
"contactlist": "Просмотреть и пригласить участников"
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
@@ -126,38 +126,39 @@
|
||||
"title": "Настройки",
|
||||
"update": "Обновить",
|
||||
"name": "Имя",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"startAudioMuted": "Каждый начинает глушиться",
|
||||
"startVideoMuted": "Все начинают скрываться",
|
||||
"selectCamera": "Камера",
|
||||
"selectMic": "Микрофон",
|
||||
"selectAudioOutput": "Звуковой выход",
|
||||
"followMe": "Каждый следует за мной",
|
||||
"noDevice": "Нет",
|
||||
"noPermission": "Нет прав пользоваться устройством",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": "",
|
||||
"setPasswordLabel": ""
|
||||
"cameraAndMic": "Камера и микрофон",
|
||||
"moderator": "МОДЕРАТОР",
|
||||
"password": "УСТАНОВИТЬ ПАРОЛЬ",
|
||||
"audioVideo": "АУДИО И ВИДЕО",
|
||||
"setPasswordLabel": "Заблокировать вашу комнату с помощью пароля."
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
"title": "Профиль",
|
||||
"setDisplayNameLabel": "Установить ваше отображаемое имя",
|
||||
"setEmailLabel": "Установить электронную почту для gravatar",
|
||||
"setEmailInput": "Введите электронную почту"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "Нажми, чтобы<br/>поменять имя экрана",
|
||||
"moderator": "Хозяйка конференции.",
|
||||
"videomute": "",
|
||||
"videomute": "Участник<br/>остановил камеру",
|
||||
"mute": "Без звука",
|
||||
"kick": "Прогнать",
|
||||
"muted": "Звук выключен",
|
||||
"domute": "Выключить звук",
|
||||
"flip": "Отразить"
|
||||
"flip": "Отразить",
|
||||
"remoteControl": "Дистанционное управление"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"header": "Данные соединения",
|
||||
"bitrate": "Битрейт",
|
||||
"packetloss": "Потеря пакетов:",
|
||||
"resolution": "Разрешение:",
|
||||
@@ -195,49 +196,49 @@
|
||||
"raisedHand": "Хочу высказаться."
|
||||
},
|
||||
"dialog": {
|
||||
"add": "",
|
||||
"add": "Добавить",
|
||||
"kickMessage": "Фигасе! Вас прогнали со встречи!",
|
||||
"popupError": "Ваш браузер блокирует всплывающие окна на этом сайте. Пожалуйста разрешите всплывающие окна в настройках безопасности и попробуйте снова.",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordErrorTitle": "Ошибка пароля",
|
||||
"passwordError": "Этот разговор сейчас защищён паролем. Только хозяйка конференции может устанавливать пароль.",
|
||||
"passwordError2": "Эта конференция защищена паролем. Только хозяйка конференции может устанавливать пароль.",
|
||||
"connectError": "Ёпрст! Что-то пошло не так и мы не можем связаться с конференцией.",
|
||||
"connectErrorWithMsg": "Ёпрст! Что-то пошло не так и мы не можем связаться с конференцией: __msg__",
|
||||
"incorrectPassword": "",
|
||||
"incorrectPassword": "Неверный пароль",
|
||||
"connecting": "Идёт подключение",
|
||||
"copy": "",
|
||||
"copy": "Копировать",
|
||||
"error": "Ошибка",
|
||||
"roomLocked": "",
|
||||
"addPassword": "",
|
||||
"createPassword": "",
|
||||
"roomLocked": "Этот звонок блокируется. Новые звонящие должны иметь ссылку и ввести пароль, чтобы присоединиться",
|
||||
"addPassword": "Добавить пароль",
|
||||
"createPassword": "Создать пароль",
|
||||
"detectext": "Ошибка при попытке определить расширение для совместного использования экрана.",
|
||||
"failtoinstall": "Невозможно установить расширение для совместного использования рабочего стола",
|
||||
"failedpermissions": "Невозможно получить права на использование локального микрофона и/или камеры.",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"reconnectNow": "",
|
||||
"conferenceReloadTimeLeft": "",
|
||||
"conferenceReloadTitle": "К сожалению, что-то пошло не так",
|
||||
"conferenceReloadMsg": "Мы пытаемся исправить это",
|
||||
"conferenceDisconnectTitle": "Вы были отсоединены. Вы можете проверить подключение к сети.",
|
||||
"conferenceDisconnectMsg": "Переподключение в...",
|
||||
"reconnectNow": "Переподключиться сейчас",
|
||||
"conferenceReloadTimeLeft": "__seconds__ сек.",
|
||||
"maxUsersLimitReached": "Достигнут максимум количества участников конференции. Конференция заполнена. Пожалуйста попробуйте позже!",
|
||||
"lockTitle": "Блокировка не удалась",
|
||||
"lockMessage": "Не удалось запереть конференцию",
|
||||
"warning": "Внимание",
|
||||
"passwordNotSupported": "Пароли для комнат сейчас не поддерживаются.",
|
||||
"internalErrorTitle": "",
|
||||
"internalError": "",
|
||||
"internalErrorTitle": "Внутренняя ошибка",
|
||||
"internalError": "Ой! Что-то пошло не так. Возникла следующая ошибка: [setRemoteDescription]",
|
||||
"unableToSwitch": "Невозможно сменить видео трансляцию.",
|
||||
"SLDFailure": "Ёпрст! Что-то пошло не так и мы не можем отключить звук! (ошибка SLD)",
|
||||
"SRDFailure": "Ёпрст! Что-то пошло не так и мы не можем остановить видео! (ошибка SRD)",
|
||||
"oops": "Ёпрст!",
|
||||
"currentPassword": "",
|
||||
"passwordLabel": "",
|
||||
"currentPassword": "Текущим паролем является",
|
||||
"passwordLabel": "Пароль",
|
||||
"defaultError": "Какая-то ошибка",
|
||||
"passwordRequired": "Требуется пароль",
|
||||
"Ok": "Ok",
|
||||
"done": "",
|
||||
"done": "Готово",
|
||||
"Remove": "Удалить",
|
||||
"removePassword": "",
|
||||
"removePassword": "Удалить пароль",
|
||||
"shareVideoTitle": "Поделиться видео",
|
||||
"shareVideoLinkError": "Пожалуйста введите корректную youtube ссылку.",
|
||||
"removeSharedVideoTitle": "Удалить общее видео",
|
||||
@@ -247,7 +248,7 @@
|
||||
"WaitForHostMsg": "Конференция <b>__room__ </b> ещё не началась. Если вы её хост - аутентифицируйтесь. Или сидите ждите хоста.",
|
||||
"IamHost": "Я хост",
|
||||
"Cancel": "Отменить",
|
||||
"Submit": "",
|
||||
"Submit": "Принять",
|
||||
"retry": "Повторить",
|
||||
"logoutTitle": "Завершить сеанс",
|
||||
"logoutQuestion": "Вы уверены, что хотите выйти и остановить конференцию?",
|
||||
@@ -262,29 +263,29 @@
|
||||
"sipMsg": "Введите SIP-номер",
|
||||
"passwordCheck": "Вы уверены, что хотите удалить ваш пароль?",
|
||||
"passwordMsg": "Введите пароль для вашей комнаты",
|
||||
"shareLink": "",
|
||||
"shareLink": "Поделитесь ссылкой на звонок",
|
||||
"settings1": "Настройка Вашей конференции",
|
||||
"settings2": "Участница подключилась без звука",
|
||||
"settings3": "Нужны имена<br/><br/>Установите пароль, чтобы запереть Вашу комнату:",
|
||||
"yourPassword": "",
|
||||
"yourPassword": "Введите новый пароль",
|
||||
"Back": "Назад",
|
||||
"serviceUnavailable": "Служба недоступна",
|
||||
"gracefulShutdown": "Сервис закрыт на переучёт. Пожалуйста попробуйте позже.",
|
||||
"Yes": "Да",
|
||||
"reservationError": "Ошибка системы резервации",
|
||||
"reservationErrorMsg": "Код ошибки: __code__, сообщение: __msg__",
|
||||
"password": "",
|
||||
"password": "Введите пароль",
|
||||
"userPassword": "пароль пользователя",
|
||||
"token": "токен",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"tokenAuthFailed": "",
|
||||
"displayNameRequired": "",
|
||||
"tokenAuthFailedTitle": "Ошибка аутентификации",
|
||||
"tokenAuthFailed": "Извините, вам не разрешено присоединиться к этому звонку.",
|
||||
"displayNameRequired": "Требуется отображаемое имя",
|
||||
"enterDisplayName": "Пожалуйста, введите Ваше имя экрана",
|
||||
"extensionRequired": "Требуется расширение:",
|
||||
"firefoxExtensionPrompt": "Нужно установить расширение Firefox, чтобы совместно пользоваться экраном. Попробуйте позже, скачав его <a href='__url__'>отсюда</a>!",
|
||||
"rateExperience": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"rateExperience": "Пожалуйста, оцените ваш опыт встречи.",
|
||||
"feedbackHelp": "Ваша поддержка поможет нам улучшить опыт видео.",
|
||||
"feedbackQuestion": "Расскажите нам о вашем звонке!",
|
||||
"thankYou": "Спасибо за использование __appName__!",
|
||||
"sorryFeedback": "Мы удручены услышанным. Может расскажете поподробнее?",
|
||||
"liveStreaming": "Трансляция",
|
||||
@@ -295,7 +296,7 @@
|
||||
"stopLiveStreaming": "Остановить трансляцию",
|
||||
"stopRecording": "Остановить запись",
|
||||
"doNotShowWarningAgain": "Больше не показывать это предупреждение",
|
||||
"doNotShowMessageAgain": "",
|
||||
"doNotShowMessageAgain": "Не показывать больше это сообщение",
|
||||
"permissionDenied": "Доступ запрещён",
|
||||
"screenSharingPermissionDeniedError": "У Вас нет прав совместно использовать Ваш экран",
|
||||
"micErrorPresent": "Произошла ошибка при подключении к Вашему микрофону",
|
||||
@@ -303,20 +304,25 @@
|
||||
"cameraUnsupportedResolutionError": "Ваша камера не поддерживает необходимое разрешение.",
|
||||
"cameraUnknownError": "Не могу использовать камеру по неизвестной причине.",
|
||||
"cameraPermissionDeniedError": "У вас нет прав на использование камеры. Вы можете участвовать в конференции, но другие не будут Вас видеть. Используйте значок с камерой в строке адреса, чтобы устранить проблему.",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraNotFoundError": "Камера не была найдена.",
|
||||
"cameraConstraintFailedError": "Ваша камера не отвечает некоторым требованиям.",
|
||||
"micUnknownError": "Не могу пользоваться микрофоном по непонятным причинам.",
|
||||
"micPermissionDeniedError": "Вы не дали прав на использование микрофона. Вы все-равно можете присоединиться к конференции, но никто не будет Вас слышать. Используйте иконку с камерой в адресной строке браузера, чтобы исправить это.",
|
||||
"micNotFoundError": "",
|
||||
"micNotFoundError": "Микрофон не был найден.",
|
||||
"micConstraintFailedError": "Ваш микрофон не отвечает некоторым необходимым требованиям.",
|
||||
"micNotSendingData": "",
|
||||
"cameraNotSendingData": "",
|
||||
"goToStore": "",
|
||||
"externalInstallationTitle": "",
|
||||
"externalInstallationMsg": "",
|
||||
"muteParticipantTitle": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": "Выключить звук"
|
||||
"micNotSendingData": "Мы не можем получить доступ к вашему микрофону. Пожалуйста, выберите другое устройство из меню настроек или попробуйте перезапустить приложение.",
|
||||
"cameraNotSendingData": "Мы не можем получить доступ к вашей камере. Пожалуйста, проверьте, используется ли это устройство другим приложением, выберите другое устройство из меню настроек или же перезапустите приложение.",
|
||||
"goToStore": "Перейти к интернет-магазину",
|
||||
"externalInstallationTitle": "Требуется расширение",
|
||||
"externalInstallationMsg": "Вам необходимо установить наше дополнение для совместного использования рабочего стола.",
|
||||
"muteParticipantTitle": "Приглушить этого участника?",
|
||||
"muteParticipantBody": "Вы не сможете перестать глушить их, но они могут сделать это сами в любое время.",
|
||||
"muteParticipantButton": "Выключить звук",
|
||||
"remoteControlTitle": "Дистанционное управление",
|
||||
"remoteControlDeniedMessage": "__user__ отклонил ваш запрос на дистанционное управление!",
|
||||
"remoteControlAllowedMessage": "__user__ принял ваш запрос на дистанционное управление!",
|
||||
"remoteControlErrorMessage": "Произошла ошибка при попытке запросить разрешения удалённого управления от __user__!",
|
||||
"remoteControlStopMessage": "Сессия дистанционного управления завершена!"
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": [
|
||||
@@ -363,7 +369,7 @@
|
||||
"on": "Запись",
|
||||
"off": "Запись остановлена",
|
||||
"failedToStart": "Ошибка при начале записи",
|
||||
"buttonTooltip": "",
|
||||
"buttonTooltip": "Начать / Остановить запись",
|
||||
"error": "Ошибка записи. Попробуйте позже.",
|
||||
"unavailable": "Сервис записи сейчас недоступен. Попробуйте позже."
|
||||
},
|
||||
@@ -373,8 +379,9 @@
|
||||
"off": "Трансляция остановлена",
|
||||
"unavailable": "Служба трансляций сейчас недоступна. Попробуйте позже.",
|
||||
"failedToStart": "Трансляция видео не может быть начата",
|
||||
"buttonTooltip": "",
|
||||
"buttonTooltip": "Начать / Остановить прямую трансляцию",
|
||||
"streamIdRequired": "Пожалуйста введите идентификатор трансляции, чтобы запустить её.",
|
||||
"streamIdHelp": "Где я могу найти это?",
|
||||
"error": "Не удалось начать трансляцию. Попробуйте снова.",
|
||||
"busy": "Все рекордеры сейчас заняты. Попробуйте позже."
|
||||
}
|
||||
|
||||
355
lang/main-zhCN.json
Normal file
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"addParticipants": "",
|
||||
"roomLocked": "",
|
||||
"roomUnlocked": "",
|
||||
"passwordSetRemotely": "",
|
||||
"connectionsettings": "",
|
||||
"poweredby": "",
|
||||
"feedback": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"callingName": "",
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": ""
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": ""
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "",
|
||||
"roomname": "",
|
||||
"disable": "",
|
||||
"feature1": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature2": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature3": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature4": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature5": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature6": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature7": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature8": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
}
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"rejoinKeyTitle": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "",
|
||||
"videomute": "",
|
||||
"authenticate": "",
|
||||
"lock": "",
|
||||
"invite": "",
|
||||
"chat": "",
|
||||
"etherpad": "",
|
||||
"sharedvideo": "",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sip": "",
|
||||
"Settings": "",
|
||||
"hangup": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"dialpad": "",
|
||||
"sharedVideoMutedPopup": "",
|
||||
"micMutedPopup": "",
|
||||
"talkWhileMutedPopup": "",
|
||||
"unableToUnmutePopup": "",
|
||||
"cameraDisabled": "",
|
||||
"micDisabled": "",
|
||||
"filmstrip": "",
|
||||
"profile": "",
|
||||
"raiseHand": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
},
|
||||
"messagebox": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": "",
|
||||
"update": "",
|
||||
"name": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"noDevice": "",
|
||||
"noPermission": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": "",
|
||||
"setPasswordLabel": ""
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "",
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport_plural": "",
|
||||
"remoteport": "",
|
||||
"localport_plural": "",
|
||||
"localport": "",
|
||||
"localaddress_plural": "",
|
||||
"localaddress": "",
|
||||
"remoteaddress_plural": "",
|
||||
"remoteaddress": "",
|
||||
"transport": "",
|
||||
"bandwidth": "",
|
||||
"na": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connected": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": ""
|
||||
},
|
||||
"dialog": {
|
||||
"add": "",
|
||||
"kickMessage": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"error": "",
|
||||
"roomLocked": "",
|
||||
"addPassword": "",
|
||||
"createPassword": "",
|
||||
"detectext": "",
|
||||
"failtoinstall": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"reconnectNow": "",
|
||||
"conferenceReloadTimeLeft": "",
|
||||
"maxUsersLimitReached": "",
|
||||
"lockTitle": "",
|
||||
"lockMessage": "",
|
||||
"warning": "",
|
||||
"passwordNotSupported": "",
|
||||
"internalErrorTitle": "",
|
||||
"internalError": "",
|
||||
"unableToSwitch": "",
|
||||
"SLDFailure": "",
|
||||
"SRDFailure": "",
|
||||
"oops": "",
|
||||
"currentPassword": "",
|
||||
"passwordLabel": "",
|
||||
"defaultError": "",
|
||||
"passwordRequired": "",
|
||||
"Ok": "",
|
||||
"done": "",
|
||||
"Remove": "",
|
||||
"removePassword": "",
|
||||
"shareVideoTitle": "",
|
||||
"shareVideoLinkError": "",
|
||||
"removeSharedVideoTitle": "",
|
||||
"removeSharedVideoMsg": "",
|
||||
"alreadySharedVideoMsg": "",
|
||||
"WaitingForHost": "",
|
||||
"WaitForHostMsg": "",
|
||||
"IamHost": "",
|
||||
"Cancel": "",
|
||||
"Submit": "",
|
||||
"retry": "",
|
||||
"logoutTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"sessTerminated": "",
|
||||
"hungUp": "",
|
||||
"joinAgain": "",
|
||||
"Share": "",
|
||||
"Save": "",
|
||||
"recording": "",
|
||||
"recordingToken": "",
|
||||
"Dial": "",
|
||||
"sipMsg": "",
|
||||
"passwordCheck": "",
|
||||
"passwordMsg": "",
|
||||
"shareLink": "",
|
||||
"settings1": "",
|
||||
"settings2": "",
|
||||
"settings3": "",
|
||||
"yourPassword": "",
|
||||
"Back": "",
|
||||
"serviceUnavailable": "",
|
||||
"gracefulShutdown": "",
|
||||
"Yes": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"password": "",
|
||||
"userPassword": "",
|
||||
"token": "",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"tokenAuthFailed": "",
|
||||
"displayNameRequired": "",
|
||||
"enterDisplayName": "",
|
||||
"extensionRequired": "",
|
||||
"firefoxExtensionPrompt": "",
|
||||
"rateExperience": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"thankYou": "",
|
||||
"sorryFeedback": "",
|
||||
"liveStreaming": "",
|
||||
"streamKey": "",
|
||||
"startLiveStreaming": "",
|
||||
"stopStreamingWarning": "",
|
||||
"stopRecordingWarning": "",
|
||||
"stopLiveStreaming": "",
|
||||
"stopRecording": "",
|
||||
"doNotShowWarningAgain": "",
|
||||
"doNotShowMessageAgain": "",
|
||||
"permissionDenied": "",
|
||||
"screenSharingPermissionDeniedError": "",
|
||||
"micErrorPresent": "",
|
||||
"cameraErrorPresent": "",
|
||||
"cameraUnsupportedResolutionError": "",
|
||||
"cameraUnknownError": "",
|
||||
"cameraPermissionDeniedError": "",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraConstraintFailedError": "",
|
||||
"micUnknownError": "",
|
||||
"micPermissionDeniedError": "",
|
||||
"micNotFoundError": "",
|
||||
"micConstraintFailedError": "",
|
||||
"micNotSendingData": "",
|
||||
"cameraNotSendingData": "",
|
||||
"goToStore": "",
|
||||
"externalInstallationTitle": "",
|
||||
"externalInstallationMsg": "",
|
||||
"muteParticipantTitle": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": ""
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": "",
|
||||
"subject": "",
|
||||
"body": "",
|
||||
"and": ""
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "",
|
||||
"CONNECTING": "",
|
||||
"RECONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ATTACHED": ""
|
||||
},
|
||||
"recording": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"unavailable": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"unavailable": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"streamIdRequired": "",
|
||||
"error": "",
|
||||
"busy": ""
|
||||
}
|
||||
}
|
||||
@@ -39,41 +39,46 @@
|
||||
"videoMute": "Start or stop your camera"
|
||||
},
|
||||
"welcomepage":{
|
||||
"go": "GO",
|
||||
"roomname": "Enter room name",
|
||||
"disable": "Don't show this page again",
|
||||
"feature1": {
|
||||
"title": "Simple to use",
|
||||
"content": "No downloads required. __app__ works directly within your browser. Simply share your conference URL with others to get started."
|
||||
"content": "No downloads required. __app__ works directly within your browser. Simply share your conference URL with others to get started.",
|
||||
"title": "Simple to use"
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Low bandwidth",
|
||||
"content": "Multi-party video conferences work with as little as 128Kbps. Screen-sharing and audio-only conferences are possible with far less."
|
||||
"content": "Multi-party video conferences work with as little as 128Kbps. Screen-sharing and audio-only conferences are possible with far less.",
|
||||
"title": "Low bandwidth"
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Open source",
|
||||
"content": "__app__ is licensed under the Apache License. You are free to download, use, modify, and share it as per this license."
|
||||
"content": "__app__ is licensed under the Apache License. You are free to download, use, modify, and share it as per this license.",
|
||||
"title": "Open source"
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Unlimited users",
|
||||
"content": "There are no artificial restrictions on the number of users or conference participants. Server power and bandwidth are the only limiting factors."
|
||||
"content": "There are no artificial restrictions on the number of users or conference participants. Server power and bandwidth are the only limiting factors.",
|
||||
"title": "Unlimited users"
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Screen sharing",
|
||||
"content": "It's easy to share your screen with others. __app__ is ideal for on-line presentations, lectures, and tech support sessions."
|
||||
"content": "It's easy to share your screen with others. __app__ is ideal for on-line presentations, lectures, and tech support sessions.",
|
||||
"title": "Screen sharing"
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Secure rooms",
|
||||
"content": "Need some privacy? __app__ conference rooms can be secured with a password in order to exclude unwanted guests and prevent interruptions."
|
||||
"content": "Need some privacy? __app__ conference rooms can be secured with a password in order to exclude unwanted guests and prevent interruptions.",
|
||||
"title": "Secure rooms"
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Shared notes",
|
||||
"content": "__app__ features Etherpad, a real-time collaborative text editor that's great for meeting minutes, writing articles, and more."
|
||||
"content": "__app__ features Etherpad, a real-time collaborative text editor that's great for meeting minutes, writing articles, and more.",
|
||||
"title": "Shared notes"
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Usage statistics",
|
||||
"content": "Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems."
|
||||
}
|
||||
"content": "Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems.",
|
||||
"title": "Usage statistics"
|
||||
},
|
||||
"go": "GO",
|
||||
"join": "JOIN",
|
||||
"privacy": "Privacy",
|
||||
"roomname": "Enter room name",
|
||||
"roomnamePlaceHolder": "room name",
|
||||
"sendFeedback": "Send feedback",
|
||||
"terms": "Terms"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -110,6 +115,13 @@
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appInstalled": "or if you already have it<br /><strong>then</strong>",
|
||||
"appNotInstalled": "You need <strong>__app__</strong> to join a conversation on your mobile",
|
||||
"downloadApp": "Download the App",
|
||||
"joinConversation": "Join the conversation",
|
||||
"startConference": "Start a conference"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Open / close chat",
|
||||
"filmstrip": "Show / hide videos",
|
||||
@@ -387,6 +399,7 @@
|
||||
"failedToStart": "Live streaming failed to start",
|
||||
"buttonTooltip": "Start / Stop live stream",
|
||||
"streamIdRequired": "Please fill in the stream id in order to launch the live streaming.",
|
||||
"streamIdHelp": "Where do I find this?",
|
||||
"error": "Live streaming failed. Please try again.",
|
||||
"busy": "All recorders are currently busy. Please try again later."
|
||||
}
|
||||
|
||||
393
modules/UI/UI.js
@@ -15,18 +15,13 @@ import UIEvents from "../../service/UI/UIEvents";
|
||||
import EtherpadManager from './etherpad/Etherpad';
|
||||
import SharedVideoManager from './shared_video/SharedVideo';
|
||||
import Recording from "./recording/Recording";
|
||||
import GumPermissionsOverlay
|
||||
from './gum_overlay/UserMediaPermissionsGuidanceOverlay';
|
||||
|
||||
import * as PageReloadOverlay from './reload_overlay/PageReloadOverlay';
|
||||
import SuspendedOverlay from './suspended_overlay/SuspendedOverlay';
|
||||
import VideoLayout from "./videolayout/VideoLayout";
|
||||
import FilmStrip from "./videolayout/FilmStrip";
|
||||
import SettingsMenu from "./side_pannels/settings/SettingsMenu";
|
||||
import Profile from "./side_pannels/profile/Profile";
|
||||
import Settings from "./../settings/Settings";
|
||||
import RingOverlay from "./ring_overlay/RingOverlay";
|
||||
import { randomInt } from "../../react/features/base/util/randomUtil";
|
||||
import UIErrors from './UIErrors';
|
||||
import { debounce } from "../util/helpers";
|
||||
|
||||
@@ -40,6 +35,17 @@ import FollowMe from "../FollowMe";
|
||||
var eventEmitter = new EventEmitter();
|
||||
UI.eventEmitter = eventEmitter;
|
||||
|
||||
/**
|
||||
* Whether an overlay is visible or not.
|
||||
*
|
||||
* FIXME: This is temporary solution. Don't use this variable!
|
||||
* Should be removed when all the code is move to react.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @public
|
||||
*/
|
||||
UI.overlayVisible = false;
|
||||
|
||||
let etherpadManager;
|
||||
let sharedVideoManager;
|
||||
|
||||
@@ -77,57 +83,6 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE]
|
||||
= "dialog.micNotSendingData";
|
||||
|
||||
/**
|
||||
* Prompt user for nickname.
|
||||
*/
|
||||
function promptDisplayName() {
|
||||
let labelKey = 'dialog.enterDisplayName';
|
||||
let message = (
|
||||
`<div class="form-control">
|
||||
<label data-i18n="${labelKey}" class="form-control__label"></label>
|
||||
<input name="displayName" type="text"
|
||||
data-i18n="[placeholder]defaultNickname"
|
||||
class="input-control" autofocus>
|
||||
</div>`
|
||||
);
|
||||
|
||||
// Don't use a translation string, because we're too early in the call and
|
||||
// the translation may not be initialised.
|
||||
let buttons = {Ok:true};
|
||||
|
||||
let dialog = messageHandler.openDialog(
|
||||
'dialog.displayNameRequired',
|
||||
message,
|
||||
true,
|
||||
buttons,
|
||||
function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v) {
|
||||
let displayName = f.displayName;
|
||||
if (displayName) {
|
||||
UI.inputDisplayNameHandler(displayName);
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
function () {
|
||||
let form = $.prompt.getPrompt();
|
||||
let input = form.find("input[name='displayName']");
|
||||
input.focus();
|
||||
let button = form.find("button");
|
||||
button.attr("disabled", "disabled");
|
||||
input.keyup(function () {
|
||||
if (input.val()) {
|
||||
button.removeAttr("disabled");
|
||||
} else {
|
||||
button.attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize toolbars with side panels.
|
||||
*/
|
||||
@@ -298,9 +253,7 @@ UI.mucJoined = function () {
|
||||
/***
|
||||
* Handler for toggling filmstrip
|
||||
*/
|
||||
UI.handleToggleFilmStrip = () => {
|
||||
UI.toggleFilmStrip();
|
||||
};
|
||||
UI.handleToggleFilmStrip = () => UI.toggleFilmStrip();
|
||||
|
||||
/**
|
||||
* Sets tooltip defaults.
|
||||
@@ -318,69 +271,6 @@ function _setTooltipDefaults() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup some UI event listeners.
|
||||
*/
|
||||
function registerListeners() {
|
||||
|
||||
UI.addListener(UIEvents.ETHERPAD_CLICKED, function () {
|
||||
if (etherpadManager) {
|
||||
etherpadManager.toggleEtherpad();
|
||||
}
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.SHARED_VIDEO_CLICKED, function () {
|
||||
if (sharedVideoManager) {
|
||||
sharedVideoManager.toggleSharedVideo();
|
||||
}
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.TOGGLE_FULLSCREEN, UI.toggleFullScreen);
|
||||
|
||||
UI.addListener(UIEvents.TOGGLE_CHAT, UI.toggleChat);
|
||||
|
||||
UI.addListener(UIEvents.TOGGLE_SETTINGS, function () {
|
||||
UI.toggleSidePanel("settings_container");
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.TOGGLE_CONTACT_LIST, UI.toggleContactList);
|
||||
|
||||
UI.addListener( UIEvents.TOGGLE_PROFILE, function() {
|
||||
if(APP.tokenData.isGuest)
|
||||
UI.toggleSidePanel("profile_container");
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.TOGGLE_FILM_STRIP, UI.handleToggleFilmStrip);
|
||||
|
||||
UI.addListener(UIEvents.FOLLOW_ME_ENABLED, function (isEnabled) {
|
||||
if (followMeHandler)
|
||||
followMeHandler.enableFollowMe(isEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup some DOM event listeners.
|
||||
*/
|
||||
function bindEvents() {
|
||||
function onResize() {
|
||||
SideContainerToggler.resize();
|
||||
VideoLayout.resizeVideoArea();
|
||||
}
|
||||
|
||||
// Resize and reposition videos in full screen mode.
|
||||
$(document).on(
|
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
|
||||
() => {
|
||||
eventEmitter.emit( UIEvents.FULLSCREEN_TOGGLED,
|
||||
UIUtil.isFullScreen());
|
||||
|
||||
onResize();
|
||||
}
|
||||
);
|
||||
|
||||
$(window).resize(onResize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shared document manager object.
|
||||
* @return {EtherpadManager} the shared document manager object
|
||||
@@ -404,8 +294,6 @@ UI.start = function () {
|
||||
// Set the defaults for tooltips.
|
||||
_setTooltipDefaults();
|
||||
|
||||
registerListeners();
|
||||
|
||||
ToolbarToggler.init();
|
||||
SideContainerToggler.init(eventEmitter);
|
||||
FilmStrip.init(eventEmitter);
|
||||
@@ -416,7 +304,6 @@ UI.start = function () {
|
||||
}
|
||||
VideoLayout.resizeVideoArea(true, true);
|
||||
|
||||
bindEvents();
|
||||
sharedVideoManager = new SharedVideoManager(eventEmitter);
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
let debouncedShowToolbar = debounce(() => {
|
||||
@@ -435,6 +322,7 @@ UI.start = function () {
|
||||
UIUtil.setVisible('notice', true);
|
||||
}
|
||||
} else {
|
||||
$("body").addClass("filmstrip-only");
|
||||
UIUtil.setVisible('mainToolbarContainer', false);
|
||||
FilmStrip.setupFilmStripOnly();
|
||||
messageHandler.enableNotifications(false);
|
||||
@@ -443,12 +331,6 @@ UI.start = function () {
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
|
||||
if(config.requireDisplayName) {
|
||||
if (!APP.settings.getDisplayName()) {
|
||||
promptDisplayName();
|
||||
}
|
||||
}
|
||||
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
toastr.options = {
|
||||
"closeButton": true,
|
||||
@@ -473,17 +355,57 @@ UI.start = function () {
|
||||
if(APP.tokenData.callee) {
|
||||
UI.showRingOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
// Return true to indicate that the UI has been fully started and
|
||||
// conference ready.
|
||||
return true;
|
||||
/**
|
||||
* Setup some UI event listeners.
|
||||
*/
|
||||
UI.registerListeners
|
||||
= () => UIListeners.forEach((value, key) => UI.addListener(key, value));
|
||||
|
||||
/**
|
||||
* Unregister some UI event listeners.
|
||||
*/
|
||||
UI.unregisterListeners
|
||||
= () => UIListeners.forEach((value, key) => UI.removeListener(key, value));
|
||||
|
||||
/**
|
||||
* Setup some DOM event listeners.
|
||||
*/
|
||||
UI.bindEvents = () => {
|
||||
function onResize() {
|
||||
SideContainerToggler.resize();
|
||||
VideoLayout.resizeVideoArea();
|
||||
}
|
||||
|
||||
// Resize and reposition videos in full screen mode.
|
||||
$(document).on(
|
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
|
||||
() => {
|
||||
eventEmitter.emit(
|
||||
UIEvents.FULLSCREEN_TOGGLED,
|
||||
UIUtil.isFullScreen());
|
||||
onResize();
|
||||
});
|
||||
|
||||
$(window).resize(onResize);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbind some DOM event listeners.
|
||||
*/
|
||||
UI.unbindEvents = () => {
|
||||
$(document).off(
|
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange');
|
||||
|
||||
$(window).off('resize');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show local stream on UI.
|
||||
* @param {JitsiTrack} track stream to show
|
||||
*/
|
||||
UI.addLocalStream = function (track) {
|
||||
UI.addLocalStream = track => {
|
||||
switch (track.getType()) {
|
||||
case 'audio':
|
||||
VideoLayout.changeLocalAudio(track);
|
||||
@@ -502,31 +424,25 @@ UI.addLocalStream = function (track) {
|
||||
* Show remote stream on UI.
|
||||
* @param {JitsiTrack} track stream to show
|
||||
*/
|
||||
UI.addRemoteStream = function (track) {
|
||||
VideoLayout.onRemoteStreamAdded(track);
|
||||
};
|
||||
UI.addRemoteStream = track => VideoLayout.onRemoteStreamAdded(track);
|
||||
|
||||
/**
|
||||
* Removed remote stream from UI.
|
||||
* @param {JitsiTrack} track stream to remove
|
||||
*/
|
||||
UI.removeRemoteStream = function (track) {
|
||||
VideoLayout.onRemoteStreamRemoved(track);
|
||||
};
|
||||
UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
|
||||
|
||||
/**
|
||||
* Update chat subject.
|
||||
* @param {string} subject new chat subject
|
||||
*/
|
||||
UI.setSubject = function (subject) {
|
||||
Chat.setSubject(subject);
|
||||
};
|
||||
UI.setSubject = subject => Chat.setSubject(subject);
|
||||
|
||||
/**
|
||||
* Setup and show Etherpad.
|
||||
* @param {string} name etherpad id
|
||||
*/
|
||||
UI.initEtherpad = function (name) {
|
||||
UI.initEtherpad = name => {
|
||||
if (etherpadManager || !config.etherpad_base || !name) {
|
||||
return;
|
||||
}
|
||||
@@ -540,9 +456,7 @@ UI.initEtherpad = function (name) {
|
||||
* Returns the shared document manager object.
|
||||
* @return {EtherpadManager} the shared document manager object
|
||||
*/
|
||||
UI.getSharedDocumentManager = function () {
|
||||
return etherpadManager;
|
||||
};
|
||||
UI.getSharedDocumentManager = () => etherpadManager;
|
||||
|
||||
/**
|
||||
* Show user on UI.
|
||||
@@ -600,15 +514,14 @@ UI.removeUser = function (id, displayName) {
|
||||
* @param {string} id user id
|
||||
* @param {string} newVideoType new videotype
|
||||
*/
|
||||
UI.onPeerVideoTypeChanged = (id, newVideoType) => {
|
||||
VideoLayout.onVideoTypeChanged(id, newVideoType);
|
||||
};
|
||||
UI.onPeerVideoTypeChanged
|
||||
= (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
|
||||
|
||||
/**
|
||||
* Update local user role and show notification if user is moderator.
|
||||
* @param {boolean} isModerator if local user is moderator or not
|
||||
*/
|
||||
UI.updateLocalRole = function (isModerator) {
|
||||
UI.updateLocalRole = isModerator => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
Toolbar.showSipCallButton(isModerator);
|
||||
@@ -631,7 +544,7 @@ UI.updateLocalRole = function (isModerator) {
|
||||
* and notifies user who is the moderator
|
||||
* @param user to check for moderator
|
||||
*/
|
||||
UI.updateUserRole = function (user) {
|
||||
UI.updateUserRole = user => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
// We don't need to show moderator notifications when the focus (moderator)
|
||||
@@ -656,13 +569,10 @@ UI.updateUserRole = function (user) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Toggles smileys in the chat.
|
||||
*/
|
||||
UI.toggleSmileys = function () {
|
||||
Chat.toggleSmileys();
|
||||
};
|
||||
UI.toggleSmileys = () => Chat.toggleSmileys();
|
||||
|
||||
/**
|
||||
* Toggles film strip.
|
||||
@@ -677,32 +587,24 @@ UI.toggleFilmStrip = function () {
|
||||
* Indicates if the film strip is currently visible or not.
|
||||
* @returns {true} if the film strip is currently visible, otherwise
|
||||
*/
|
||||
UI.isFilmStripVisible = function () {
|
||||
return FilmStrip.isFilmStripVisible();
|
||||
};
|
||||
UI.isFilmStripVisible = () => FilmStrip.isFilmStripVisible();
|
||||
|
||||
/**
|
||||
* Toggles chat panel.
|
||||
*/
|
||||
UI.toggleChat = function () {
|
||||
UI.toggleSidePanel("chat_container");
|
||||
};
|
||||
UI.toggleChat = () => UI.toggleSidePanel("chat_container");
|
||||
|
||||
/**
|
||||
* Toggles contact list panel.
|
||||
*/
|
||||
UI.toggleContactList = function () {
|
||||
UI.toggleSidePanel("contacts_container");
|
||||
};
|
||||
UI.toggleContactList = () => UI.toggleSidePanel("contacts_container");
|
||||
|
||||
/**
|
||||
* Toggles the given side panel.
|
||||
*
|
||||
* @param {String} sidePanelId the identifier of the side panel to toggle
|
||||
*/
|
||||
UI.toggleSidePanel = function (sidePanelId) {
|
||||
SideContainerToggler.toggle(sidePanelId);
|
||||
};
|
||||
UI.toggleSidePanel = sidePanelId => SideContainerToggler.toggle(sidePanelId);
|
||||
|
||||
|
||||
/**
|
||||
@@ -712,6 +614,17 @@ UI.inputDisplayNameHandler = function (newDisplayName) {
|
||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show custom popup/tooltip for a specified button.
|
||||
* @param popupSelectorID the selector id of the popup to show
|
||||
* @param show true or false/show or hide the popup
|
||||
* @param timeout the time to show the popup
|
||||
*/
|
||||
UI.showCustomToolbarPopup = function (popupSelectorID, show, timeout) {
|
||||
eventEmitter.emit(UIEvents.SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP,
|
||||
popupSelectorID, show, timeout);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the type of the remote video.
|
||||
* @param jid the jid for the remote video
|
||||
@@ -946,6 +859,59 @@ UI.participantConnectionStatusChanged = function (id, isActive) {
|
||||
VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt user for nickname.
|
||||
*/
|
||||
UI.promptDisplayName = () => {
|
||||
const labelKey = 'dialog.enterDisplayName';
|
||||
const message = (
|
||||
`<div class="form-control">
|
||||
<label data-i18n="${labelKey}" class="form-control__label"></label>
|
||||
<input name="displayName" type="text"
|
||||
data-i18n="[placeholder]defaultNickname"
|
||||
class="input-control" autofocus>
|
||||
</div>`
|
||||
);
|
||||
|
||||
// Don't use a translation string, because we're too early in the call and
|
||||
// the translation may not be initialised.
|
||||
const buttons = { Ok: true };
|
||||
|
||||
const dialog = messageHandler.openDialog(
|
||||
'dialog.displayNameRequired',
|
||||
message,
|
||||
true,
|
||||
buttons,
|
||||
(e, v, m, f) => {
|
||||
e.preventDefault();
|
||||
if (v) {
|
||||
const displayName = f.displayName;
|
||||
|
||||
if (displayName) {
|
||||
UI.inputDisplayNameHandler(displayName);
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
() => {
|
||||
const form = $.prompt.getPrompt();
|
||||
const input = form.find("input[name='displayName']");
|
||||
const button = form.find("button");
|
||||
|
||||
input.focus();
|
||||
button.attr("disabled", "disabled");
|
||||
input.keyup(() => {
|
||||
if (input.val()) {
|
||||
button.removeAttr("disabled");
|
||||
} else {
|
||||
button.attr("disabled", "disabled");
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update audio level visualization for specified user.
|
||||
* @param {string} id user id
|
||||
@@ -1076,20 +1042,6 @@ UI.notifyFocusDisconnected = function (focus, retrySec) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify the user that the video conferencing service is badly broken and
|
||||
* the page should be reloaded.
|
||||
*
|
||||
* @param {boolean} isNetworkFailure <tt>true</tt> indicates that it's caused by
|
||||
* network related failure or <tt>false</tt> when it's the infrastructure.
|
||||
* @param {string} a label string identifying the reason for the page reload
|
||||
* which will be included in details of the log event.
|
||||
*/
|
||||
UI.showPageReloadOverlay = function (isNetworkFailure, reason) {
|
||||
// Reload the page after 10 - 30 seconds
|
||||
PageReloadOverlay.show(10 + randomInt(0, 20), isNetworkFailure, reason);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates auth info on the UI.
|
||||
* @param {boolean} isAuthEnabled if authentication is enabled
|
||||
@@ -1370,9 +1322,7 @@ UI.onSharedVideoStop = function (id, attributes) {
|
||||
* @param {boolean} enabled indicates if the camera button should be enabled
|
||||
* or disabled
|
||||
*/
|
||||
UI.setCameraButtonEnabled = function (enabled) {
|
||||
Toolbar.setVideoIconEnabled(enabled);
|
||||
};
|
||||
UI.setCameraButtonEnabled = enabled => Toolbar.setVideoIconEnabled(enabled);
|
||||
|
||||
/**
|
||||
* Enables / disables microphone toolbar button.
|
||||
@@ -1380,9 +1330,7 @@ UI.setCameraButtonEnabled = function (enabled) {
|
||||
* @param {boolean} enabled indicates if the microphone button should be
|
||||
* enabled or disabled
|
||||
*/
|
||||
UI.setMicrophoneButtonEnabled = function (enabled) {
|
||||
Toolbar.setAudioIconEnabled(enabled);
|
||||
};
|
||||
UI.setMicrophoneButtonEnabled = enabled => Toolbar.setAudioIconEnabled(enabled);
|
||||
|
||||
UI.showRingOverlay = function () {
|
||||
RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING);
|
||||
@@ -1403,10 +1351,7 @@ UI.hideRingOverLay = function () {
|
||||
* @returns {*|boolean} {true} if the overlay is visible, {false} otherwise
|
||||
*/
|
||||
UI.isOverlayVisible = function () {
|
||||
return RingOverlay.isVisible()
|
||||
|| SuspendedOverlay.isVisible()
|
||||
|| PageReloadOverlay.isVisible()
|
||||
|| GumPermissionsOverlay.isVisible();
|
||||
return RingOverlay.isVisible() || this.overlayVisible;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1414,38 +1359,42 @@ UI.isOverlayVisible = function () {
|
||||
*
|
||||
* @returns {*|boolean} {true} if the ring overlay is visible, {false} otherwise
|
||||
*/
|
||||
UI.isRingOverlayVisible = function () {
|
||||
return RingOverlay.isVisible();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows browser-specific overlay with guidance how to proceed with gUM prompt.
|
||||
* @param {string} browser - name of browser for which to show the guidance
|
||||
* overlay.
|
||||
*/
|
||||
UI.showUserMediaPermissionsGuidanceOverlay = function (browser) {
|
||||
GumPermissionsOverlay.show(browser);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows suspended overlay with a button to rejoin conference.
|
||||
*/
|
||||
UI.showSuspendedOverlay = function () {
|
||||
SuspendedOverlay.show();
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides browser-specific overlay with guidance how to proceed with gUM prompt.
|
||||
*/
|
||||
UI.hideUserMediaPermissionsGuidanceOverlay = function () {
|
||||
GumPermissionsOverlay.hide();
|
||||
};
|
||||
UI.isRingOverlayVisible = () => RingOverlay.isVisible();
|
||||
|
||||
/**
|
||||
* Handles user's features changes.
|
||||
*/
|
||||
UI.onUserFeaturesChanged = function (user) {
|
||||
VideoLayout.onUserFeaturesChanged(user);
|
||||
};
|
||||
UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
|
||||
|
||||
const UIListeners = new Map([
|
||||
[
|
||||
UIEvents.ETHERPAD_CLICKED,
|
||||
() => etherpadManager && etherpadManager.toggleEtherpad()
|
||||
], [
|
||||
UIEvents.SHARED_VIDEO_CLICKED,
|
||||
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
|
||||
], [
|
||||
UIEvents.TOGGLE_FULLSCREEN,
|
||||
UI.toggleFullScreen
|
||||
], [
|
||||
UIEvents.TOGGLE_CHAT,
|
||||
UI.toggleChat
|
||||
], [
|
||||
UIEvents.TOGGLE_SETTINGS,
|
||||
() => UI.toggleSidePanel("settings_container")
|
||||
], [
|
||||
UIEvents.TOGGLE_CONTACT_LIST,
|
||||
UI.toggleContactList
|
||||
], [
|
||||
UIEvents.TOGGLE_PROFILE,
|
||||
() => APP.tokenData.isGuest && UI.toggleSidePanel("profile_container")
|
||||
], [
|
||||
UIEvents.TOGGLE_FILM_STRIP,
|
||||
UI.handleToggleFilmStrip
|
||||
], [
|
||||
UIEvents.FOLLOW_ME_ENABLED,
|
||||
enabled => (followMeHandler && followMeHandler.enableFollowMe(enabled))
|
||||
]
|
||||
]);
|
||||
|
||||
module.exports = UI;
|
||||
|
||||
@@ -21,8 +21,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/* global MD5, config, interfaceConfig, APP */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
/* global APP */
|
||||
|
||||
import { getAvatarURL } from '../../../react/features/base/participants';
|
||||
|
||||
let users = {};
|
||||
|
||||
@@ -64,7 +65,7 @@ export default {
|
||||
* @param url the url for the avatar
|
||||
*/
|
||||
setUserAvatarUrl: function (id, url) {
|
||||
this._setUserProp(id, "url", url);
|
||||
this._setUserProp(id, "avatarUrl", url);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -82,57 +83,19 @@ export default {
|
||||
* @param {string} userId user id
|
||||
*/
|
||||
getAvatarUrl: function (userId) {
|
||||
if (config.disableThirdPartyRequests) {
|
||||
return 'images/avatar2.png';
|
||||
}
|
||||
|
||||
let user;
|
||||
if (!userId || APP.conference.isLocalId(userId)) {
|
||||
userId = "local";
|
||||
user = users.local;
|
||||
userId = APP.conference.getMyUserId();
|
||||
} else {
|
||||
user = users[userId];
|
||||
}
|
||||
|
||||
let avatarId = null;
|
||||
const user = users[userId];
|
||||
|
||||
// The priority is url, email and lowest is avatarId
|
||||
if(user) {
|
||||
if(user.url)
|
||||
return user.url;
|
||||
|
||||
if (user.email)
|
||||
avatarId = user.email;
|
||||
else {
|
||||
avatarId = user.avatarId;
|
||||
}
|
||||
}
|
||||
|
||||
// If the ID looks like an email, we'll use gravatar.
|
||||
// Otherwise, it's a random avatar, and we'll use the configured
|
||||
// URL.
|
||||
let random = !avatarId || avatarId.indexOf('@') < 0;
|
||||
|
||||
if (!avatarId) {
|
||||
logger.warn(
|
||||
`No avatar stored yet for ${userId} - using ID as avatar ID`);
|
||||
avatarId = userId;
|
||||
}
|
||||
avatarId = MD5.hexdigest(avatarId.trim().toLowerCase());
|
||||
|
||||
|
||||
let urlPref = null;
|
||||
let urlSuf = null;
|
||||
if (!random) {
|
||||
urlPref = 'https://www.gravatar.com/avatar/';
|
||||
urlSuf = "?d=wavatar&size=200";
|
||||
}
|
||||
else if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) {
|
||||
urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
|
||||
urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
|
||||
}
|
||||
else {
|
||||
urlPref = 'https://api.adorable.io/avatars/200/';
|
||||
urlSuf = ".png";
|
||||
}
|
||||
|
||||
return urlPref + avatarId + urlSuf;
|
||||
return getAvatarURL({
|
||||
avatarID: user ? user.avatarId : undefined,
|
||||
avatarURL: user ? user.avatarUrl : undefined,
|
||||
email: user ? user.email : undefined,
|
||||
id: userId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/* global interfaceConfig */
|
||||
|
||||
import Overlay from '../overlay/Overlay';
|
||||
|
||||
/**
|
||||
* An overlay with guidance how to proceed with gUM prompt.
|
||||
*/
|
||||
class GUMOverlayImpl extends Overlay {
|
||||
|
||||
/**
|
||||
* Constructs overlay with guidance how to proceed with gUM prompt.
|
||||
* @param {string} browser - name of browser for which to construct the
|
||||
* guidance overlay.
|
||||
* @override
|
||||
*/
|
||||
constructor(browser) {
|
||||
super();
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
_buildOverlayContent() {
|
||||
let textKey = `userMedia.${this.browser}GrantPermissions`;
|
||||
let titleKey = 'startupoverlay.title';
|
||||
let titleOptions = '{ "postProcess": "resolveAppName" }';
|
||||
let policyTextKey = 'startupoverlay.policyText';
|
||||
let policyLogo = '';
|
||||
let policyLogoSrc = interfaceConfig.POLICY_LOGO;
|
||||
if (policyLogoSrc) {
|
||||
policyLogo += (
|
||||
`<div class="policy__logo">
|
||||
<img src="${policyLogoSrc}"/>
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
`<div class="inlay">
|
||||
<span class="inlay__icon icon-microphone"></span>
|
||||
<span class="inlay__icon icon-camera"></span>
|
||||
<h3 class="inlay__title" data-i18n="${titleKey}"
|
||||
data-i18n-options='${titleOptions}'></h3>
|
||||
<span class='inlay__text'data-i18n='[html]${textKey}'></span>
|
||||
</div>
|
||||
<div class="policy overlay__policy">
|
||||
<p class="policy__text" data-i18n="[html]${policyTextKey}"></p>
|
||||
${policyLogo}
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores GUM overlay instance.
|
||||
* @type {GUMOverlayImpl}
|
||||
*/
|
||||
let overlay;
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Checks whether the overlay is currently visible.
|
||||
* @return {boolean} <tt>true</tt> if the overlay is visible
|
||||
* or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isVisible () {
|
||||
return overlay && overlay.isVisible();
|
||||
},
|
||||
/**
|
||||
* Shows browser-specific overlay with guidance how to proceed with
|
||||
* gUM prompt.
|
||||
* @param {string} browser - name of browser for which to show the
|
||||
* guidance overlay.
|
||||
*/
|
||||
show(browser) {
|
||||
if (!overlay) {
|
||||
overlay = new GUMOverlayImpl(browser);
|
||||
}
|
||||
overlay.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hides browser-specific overlay with guidance how to proceed with
|
||||
* gUM prompt.
|
||||
*/
|
||||
hide() {
|
||||
overlay && overlay.hide();
|
||||
}
|
||||
};
|
||||
@@ -1,94 +0,0 @@
|
||||
/* global $, APP */
|
||||
|
||||
/**
|
||||
* Base class for overlay components - the components which are displayed on
|
||||
* top of the application with semi-transparent background covering the whole
|
||||
* screen.
|
||||
*/
|
||||
export default class Overlay{
|
||||
/**
|
||||
* Creates new <tt>Overlay</tt> instance.
|
||||
*/
|
||||
constructor() {
|
||||
/**
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$overlay = null;
|
||||
|
||||
/**
|
||||
* Indicates if this overlay should use the light look & feel or the
|
||||
* standard one.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.isLightOverlay = false;
|
||||
}
|
||||
/**
|
||||
* Template method which should be used by subclasses to provide the overlay
|
||||
* content. The contents provided by this method are later subject to
|
||||
* the translation using {@link APP.translation.translateElement}.
|
||||
* @return {string} HTML representation of the overlay dialog contents.
|
||||
* @protected
|
||||
*/
|
||||
_buildOverlayContent() {
|
||||
return '';
|
||||
}
|
||||
/**
|
||||
* Constructs the HTML body of the overlay dialog.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_buildOverlayHtml() {
|
||||
|
||||
let overlayContent = this._buildOverlayContent();
|
||||
|
||||
let containerClass = this.isLightOverlay ? "overlay__container-light"
|
||||
: "overlay__container";
|
||||
|
||||
this.$overlay = $(`
|
||||
<div class=${containerClass}>
|
||||
<div class='overlay__content'>
|
||||
${overlayContent}
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
APP.translation.translateElement(this.$overlay);
|
||||
}
|
||||
/**
|
||||
* Checks whether the page reload overlay has been displayed.
|
||||
* @return {boolean} <tt>true</tt> if the page reload overlay is currently
|
||||
* visible or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isVisible() {
|
||||
return this.$overlay && this.$overlay.parents('body').length > 0;
|
||||
}
|
||||
/**
|
||||
* Template method called just after the overlay is displayed for the first
|
||||
* time.
|
||||
* @protected
|
||||
*/
|
||||
_onShow() {
|
||||
// To be overridden by subclasses.
|
||||
}
|
||||
/**
|
||||
* Shows the overlay dialog and attaches the underlying HTML representation
|
||||
* to the DOM.
|
||||
*/
|
||||
show() {
|
||||
|
||||
!this.$overlay && this._buildOverlayHtml();
|
||||
|
||||
if (!this.isVisible()) {
|
||||
this.$overlay.appendTo('body');
|
||||
this._onShow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the overlay dialog and detaches it's HTML representation from
|
||||
* the DOM.
|
||||
*/
|
||||
hide() {
|
||||
this.$overlay && this.$overlay.detach();
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,9 @@ function _requestLiveStreamId() {
|
||||
const streamIdRequired
|
||||
= APP.translation.generateTranslationHTML(
|
||||
"liveStreaming.streamIdRequired");
|
||||
const streamIdHelp
|
||||
= APP.translation.generateTranslationHTML(
|
||||
"liveStreaming.streamIdHelp");
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
dialog = APP.UI.messageHandler.openDialogWithStates({
|
||||
@@ -60,7 +63,11 @@ function _requestLiveStreamId() {
|
||||
`<input class="input-control"
|
||||
name="streamId" type="text"
|
||||
data-i18n="[placeholder]dialog.streamKey"
|
||||
autofocus>`,
|
||||
autofocus><div style="text-align: right">
|
||||
<a class="helper-link" target="_new"
|
||||
href="${interfaceConfig.LIVE_STREAMING_HELP_LINK}">`
|
||||
+ streamIdHelp
|
||||
+ `</a></div>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
/* global $, APP, AJS */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import Overlay from "../overlay/Overlay";
|
||||
|
||||
/**
|
||||
* An overlay dialog which is shown before the conference is reloaded. Shows
|
||||
* a warning message and counts down towards the reload.
|
||||
*/
|
||||
class PageReloadOverlayImpl extends Overlay{
|
||||
/**
|
||||
* Creates new <tt>PageReloadOverlayImpl</tt>
|
||||
* @param {number} timeoutSeconds how long the overlay dialog will be
|
||||
* displayed, before the conference will be reloaded.
|
||||
* @param {string} title the title of the overlay message
|
||||
* @param {string} message the message of the overlay
|
||||
* @param {string} buttonHtml the button html or an empty string if there's
|
||||
* no button
|
||||
* @param {boolean} isLightOverlay indicates if the overlay should be a
|
||||
* light overlay or a standard one
|
||||
*/
|
||||
constructor(timeoutSeconds, title, message, buttonHtml, isLightOverlay) {
|
||||
super();
|
||||
/**
|
||||
* Conference reload counter in seconds.
|
||||
* @type {number}
|
||||
*/
|
||||
this.timeLeft = timeoutSeconds;
|
||||
/**
|
||||
* Conference reload timeout in seconds.
|
||||
* @type {number}
|
||||
*/
|
||||
this.timeout = timeoutSeconds;
|
||||
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
this.buttonHtml = buttonHtml;
|
||||
this.isLightOverlay = isLightOverlay;
|
||||
}
|
||||
/**
|
||||
* Constructs overlay body with the warning message and count down towards
|
||||
* the conference reload.
|
||||
* @override
|
||||
*/
|
||||
_buildOverlayContent() {
|
||||
return `<div class="inlay">
|
||||
<span data-i18n=${this.title}
|
||||
class='reload_overlay_title'></span>
|
||||
<span data-i18n=${this.message}
|
||||
class='reload_overlay_msg'></span>
|
||||
<div>
|
||||
<div id='reloadProgressBar'
|
||||
class="aui-progress-indicator">
|
||||
<span class="aui-progress-indicator-value"></span>
|
||||
</div>
|
||||
<span id='reloadSecRemaining'
|
||||
data-i18n="dialog.conferenceReloadTimeLeft"
|
||||
class='reload_overlay_msg'>
|
||||
</span>
|
||||
</div>
|
||||
${this.buttonHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progress indicator position and the label with the time left.
|
||||
*/
|
||||
updateDisplay() {
|
||||
|
||||
APP.translation.translateElement(
|
||||
$("#reloadSecRemaining"), { seconds: this.timeLeft });
|
||||
|
||||
const ratio = (this.timeout - this.timeLeft) / this.timeout;
|
||||
AJS.progressBars.update("#reloadProgressBar", ratio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the reload countdown with the animation.
|
||||
* @override
|
||||
*/
|
||||
_onShow() {
|
||||
$("#reconnectNow").click(() => {
|
||||
APP.ConferenceUrl.reload();
|
||||
});
|
||||
|
||||
// Initialize displays
|
||||
this.updateDisplay();
|
||||
|
||||
var intervalId = window.setInterval(function() {
|
||||
|
||||
if (this.timeLeft >= 1) {
|
||||
this.timeLeft -= 1;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
|
||||
if (this.timeLeft === 0) {
|
||||
window.clearInterval(intervalId);
|
||||
APP.ConferenceUrl.reload();
|
||||
}
|
||||
}.bind(this), 1000);
|
||||
|
||||
logger.info(
|
||||
"The conference will be reloaded after "
|
||||
+ this.timeLeft + " seconds.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the page reload overlay instance.
|
||||
*
|
||||
* {@type PageReloadOverlayImpl}
|
||||
*/
|
||||
let overlay;
|
||||
|
||||
/**
|
||||
* Checks whether the page reload overlay has been displayed.
|
||||
* @return {boolean} <tt>true</tt> if the page reload overlay is currently
|
||||
* visible or <tt>false</tt> otherwise.
|
||||
*/
|
||||
export function isVisible() {
|
||||
return overlay && overlay.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the page reload overlay which will do the conference reload after
|
||||
* the given amount of time.
|
||||
*
|
||||
* @param {number} timeoutSeconds how many seconds before the conference
|
||||
* reload will happen.
|
||||
* @param {boolean} isNetworkFailure <tt>true</tt> indicates that it's
|
||||
* caused by network related failure or <tt>false</tt> when it's
|
||||
* the infrastructure.
|
||||
* @param {string} reason a label string identifying the reason for the page
|
||||
* reload which will be included in details of the log event
|
||||
*/
|
||||
export function show(timeoutSeconds, isNetworkFailure, reason) {
|
||||
let title;
|
||||
let message;
|
||||
let buttonHtml;
|
||||
let isLightOverlay;
|
||||
|
||||
if (isNetworkFailure) {
|
||||
title = "dialog.conferenceDisconnectTitle";
|
||||
message = "dialog.conferenceDisconnectMsg";
|
||||
buttonHtml
|
||||
= `<button id="reconnectNow" data-i18n="dialog.reconnectNow"
|
||||
class="button-control button-control_primary
|
||||
button-control_center"></button>`;
|
||||
isLightOverlay = true;
|
||||
}
|
||||
else {
|
||||
title = "dialog.conferenceReloadTitle";
|
||||
message = "dialog.conferenceReloadMsg";
|
||||
buttonHtml = "";
|
||||
isLightOverlay = false;
|
||||
}
|
||||
|
||||
if (!overlay) {
|
||||
overlay = new PageReloadOverlayImpl(timeoutSeconds,
|
||||
title,
|
||||
message,
|
||||
buttonHtml,
|
||||
isLightOverlay);
|
||||
}
|
||||
// Log the page reload event
|
||||
if (!this.isVisible()) {
|
||||
// FIXME (CallStats - issue) this event will not make it to
|
||||
// the CallStats, because the log queue is not flushed, before
|
||||
// "fabric terminated" is sent to the backed
|
||||
APP.conference.logEvent(
|
||||
'page.reload', undefined /* value */, reason /* label */);
|
||||
}
|
||||
overlay.show();
|
||||
}
|
||||
@@ -541,7 +541,7 @@ export default class SharedVideoManager {
|
||||
if(show)
|
||||
this.showSharedVideoMutedPopup(false);
|
||||
|
||||
UIUtil.animateShowElement($("#micMutedPopup"), show, 5000);
|
||||
APP.UI.showCustomToolbarPopup('#micMutedPopup', show, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,7 +554,7 @@ export default class SharedVideoManager {
|
||||
if(show)
|
||||
this.showMicMutedPopup(false);
|
||||
|
||||
UIUtil.animateShowElement($("#sharedVideoMutedPopup"), show, 5000);
|
||||
APP.UI.showCustomToolbarPopup('#sharedVideoMutedPopup', show, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,9 +164,7 @@ function resizeChatConversation() {
|
||||
* @param id {string} input id
|
||||
*/
|
||||
function deferredFocus(id){
|
||||
setTimeout(function (){
|
||||
$(`#${id}`).focus();
|
||||
}, 400);
|
||||
setTimeout(() => $(`#${id}`).focus(), 400);
|
||||
}
|
||||
/**
|
||||
* Chat related user interface.
|
||||
@@ -353,10 +351,17 @@ var Chat = {
|
||||
* Scrolls chat to the bottom.
|
||||
*/
|
||||
scrollChatToBottom () {
|
||||
setTimeout(function () {
|
||||
$('#chatconversation').scrollTop(
|
||||
$('#chatconversation')[0].scrollHeight);
|
||||
}, 5);
|
||||
setTimeout(
|
||||
() => {
|
||||
const chatconversation = $('#chatconversation');
|
||||
|
||||
// XXX Prevent TypeError: undefined is not an object when the
|
||||
// Web browser does not support WebRTC (yet).
|
||||
chatconversation.length > 0
|
||||
&& chatconversation.scrollTop(
|
||||
chatconversation[0].scrollHeight);
|
||||
},
|
||||
5);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* global $, APP, AJS, interfaceConfig, JitsiMeetJS */
|
||||
|
||||
import { LANGUAGES } from "../../../../react/features/base/i18n";
|
||||
|
||||
import UIUtil from "../../util/UIUtil";
|
||||
import UIEvents from "../../../../service/UI/UIEvents";
|
||||
import languages from "../../../../service/translation/languages";
|
||||
import Settings from '../../../settings/Settings';
|
||||
|
||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||
@@ -145,7 +146,7 @@ export default {
|
||||
let selectInput;
|
||||
|
||||
selectEl.html(generateLanguagesOptions(
|
||||
languages.getLanguages(),
|
||||
LANGUAGES,
|
||||
APP.translation.getCurrentLanguage()
|
||||
));
|
||||
initSelect2(selectEl, () => {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/* global $, APP */
|
||||
|
||||
import Overlay from '../overlay/Overlay';
|
||||
|
||||
/**
|
||||
* An overlay dialog which is shown when a suspended event is detected.
|
||||
*/
|
||||
class SuspendedOverlayImpl extends Overlay{
|
||||
/**
|
||||
* Creates new <tt>SuspendedOverlayImpl</tt>
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
$(document).on('click', '#rejoin', () => {
|
||||
APP.ConferenceUrl.reload();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Constructs overlay body with the message and a button to rejoin.
|
||||
* @override
|
||||
*/
|
||||
_buildOverlayContent() {
|
||||
return (
|
||||
`<div class="inlay">
|
||||
<span class="inlay__icon icon-microphone"></span>
|
||||
<span class="inlay__icon icon-camera"></span>
|
||||
<h3 class="inlay__title" data-i18n="suspendedoverlay.title"></h3>
|
||||
<button id="rejoin"
|
||||
data-i18n="suspendedoverlay.rejoinKeyTitle"
|
||||
class="inlay__button button-control button-control_primary">
|
||||
</button>
|
||||
</div>`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the page suspended overlay instance.
|
||||
*
|
||||
* {@type SuspendedOverlayImpl}
|
||||
*/
|
||||
let overlay;
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Checks whether the page suspended overlay has been displayed.
|
||||
* @return {boolean} <tt>true</tt> if the page suspended overlay is
|
||||
* currently visible or <tt>false</tt> otherwise.
|
||||
*/
|
||||
isVisible() {
|
||||
return overlay && overlay.isVisible();
|
||||
},
|
||||
/**
|
||||
* Shows the page suspended overlay.
|
||||
*/
|
||||
show() {
|
||||
|
||||
if (!overlay) {
|
||||
overlay = new SuspendedOverlayImpl();
|
||||
}
|
||||
overlay.show();
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global APP, $, config, interfaceConfig, JitsiMeetJS */
|
||||
/* global AJS, APP, $, config, interfaceConfig, JitsiMeetJS */
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import SideContainerToggler from "../side_pannels/SideContainerToggler";
|
||||
@@ -26,8 +26,8 @@ const buttonHandlers = {
|
||||
if (sharedVideoManager
|
||||
&& sharedVideoManager.isSharedVideoVolumeOn()
|
||||
&& !sharedVideoManager.isSharedVideoOwner()) {
|
||||
UIUtil.animateShowElement(
|
||||
$("#unableToUnmutePopup"), true, 5000);
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'#unableToUnmutePopup', true, 5000);
|
||||
}
|
||||
else {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.audio.unmuted');
|
||||
@@ -120,19 +120,19 @@ const defaultToolbarButtons = {
|
||||
shortcutDescription: "keyboardShortcuts.mute",
|
||||
popups: [
|
||||
{
|
||||
id: "micMutedPopup",
|
||||
className: "loginmenu",
|
||||
dataAttr: "[html]toolbar.micMutedPopup"
|
||||
id: 'micMutedPopup',
|
||||
className: 'loginmenu',
|
||||
dataAttr: '[title]toolbar.micMutedPopup'
|
||||
},
|
||||
{
|
||||
id: "unableToUnmutePopup",
|
||||
className: "loginmenu",
|
||||
dataAttr: "[html]toolbar.unableToUnmutePopup"
|
||||
id: 'unableToUnmutePopup',
|
||||
className: 'loginmenu',
|
||||
dataAttr: '[title]toolbar.unableToUnmutePopup'
|
||||
},
|
||||
{
|
||||
id: "talkWhileMutedPopup",
|
||||
className: "loginmenu",
|
||||
dataAttr: "[html]toolbar.talkWhileMutedPopup"
|
||||
id: 'talkWhileMutedPopup',
|
||||
className: 'loginmenu',
|
||||
dataAttr: '[title]toolbar.talkWhileMutedPopup'
|
||||
}
|
||||
],
|
||||
content: "Mute / Unmute",
|
||||
@@ -263,11 +263,14 @@ const defaultToolbarButtons = {
|
||||
id: 'toolbar_button_sharedvideo',
|
||||
tooltipKey: 'toolbar.sharedvideo',
|
||||
className: 'button icon-shared-video',
|
||||
html: `<ul id="sharedVideoMutedPopup"
|
||||
class="loginmenu extendedToolbarPopup">
|
||||
<li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li>
|
||||
</ul>
|
||||
`
|
||||
popups: [
|
||||
{
|
||||
id: 'sharedVideoMutedPopup',
|
||||
className: 'loginmenu extendedToolbarPopup',
|
||||
dataAttr: '[title]toolbar.sharedVideoMutedPopup',
|
||||
dataAttrPosition: 'w'
|
||||
}
|
||||
]
|
||||
},
|
||||
'sip': {
|
||||
id: 'toolbar_button_sip',
|
||||
@@ -325,6 +328,39 @@ function getToolbarButtonPlace (btn) {
|
||||
'extended';
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for side toolbar container toggled event.
|
||||
*
|
||||
* @param {string} containerId - ID of the container.
|
||||
* @param {boolean} isVisible - Flag showing whether container
|
||||
* is visible.
|
||||
* @returns {void}
|
||||
*/
|
||||
function onSideToolbarContainerToggled(containerId, isVisible) {
|
||||
Toolbar._handleSideToolbarContainerToggled(containerId, isVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for local raise hand changed event.
|
||||
*
|
||||
* @param {boolean} isRaisedHand - Flag showing whether hand is raised.
|
||||
* @returns {void}
|
||||
*/
|
||||
function onLocalRaiseHandChanged(isRaisedHand) {
|
||||
Toolbar._setToggledState("toolbar_button_raisehand", isRaisedHand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for full screen toggled event.
|
||||
*
|
||||
* @param {boolean} isFullScreen - Flag showing whether app in full
|
||||
* screen mode.
|
||||
* @returns {void}
|
||||
*/
|
||||
function onFullScreenToggled(isFullScreen) {
|
||||
Toolbar._handleFullScreenToggled(isFullScreen);
|
||||
}
|
||||
|
||||
Toolbar = {
|
||||
init (eventEmitter) {
|
||||
emitter = eventEmitter;
|
||||
@@ -333,6 +369,9 @@ Toolbar = {
|
||||
this.toolbarSelector = $("#mainToolbarContainer");
|
||||
this.extendedToolbarSelector = $("#extendedToolbar");
|
||||
|
||||
// Unregister listeners in case of reinitialization.
|
||||
this.unregisterListeners();
|
||||
|
||||
// Initialise the toolbar buttons.
|
||||
// The main toolbar will only take into account
|
||||
// it's own configuration from interface_config.
|
||||
@@ -342,20 +381,11 @@ Toolbar = {
|
||||
|
||||
this._setButtonHandlers();
|
||||
|
||||
APP.UI.addListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
|
||||
(containerId, isVisible) => {
|
||||
Toolbar._handleSideToolbarContainerToggled( containerId,
|
||||
isVisible);
|
||||
});
|
||||
this.registerListeners();
|
||||
|
||||
APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
|
||||
(isRaisedHand) => {
|
||||
this._setToggledState("toolbar_button_raisehand", isRaisedHand);
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.FULLSCREEN_TOGGLED,
|
||||
(isFullScreen) => {
|
||||
Toolbar._handleFullScreenToggled(isFullScreen);
|
||||
APP.UI.addListener(UIEvents.SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP,
|
||||
(popupID, show, timeout) => {
|
||||
Toolbar._showCustomToolbarPopup(popupID, show, timeout);
|
||||
});
|
||||
|
||||
if(!APP.tokenData.isGuest) {
|
||||
@@ -364,6 +394,35 @@ Toolbar = {
|
||||
document.getElementById('toolbar_button_profile'));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Register listeners for UI events of toolbar component.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
registerListeners() {
|
||||
APP.UI.addListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
|
||||
onSideToolbarContainerToggled);
|
||||
|
||||
APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
|
||||
onLocalRaiseHandChanged);
|
||||
|
||||
APP.UI.addListener(UIEvents.FULLSCREEN_TOGGLED, onFullScreenToggled);
|
||||
},
|
||||
/**
|
||||
* Unregisters handlers for UI events of Toolbar component.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
unregisterListeners() {
|
||||
APP.UI.removeListener(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
|
||||
onSideToolbarContainerToggled);
|
||||
|
||||
APP.UI.removeListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
|
||||
onLocalRaiseHandChanged);
|
||||
|
||||
APP.UI.removeListener(UIEvents.FULLSCREEN_TOGGLED,
|
||||
onFullScreenToggled);
|
||||
},
|
||||
/**
|
||||
* Enables / disables the toolbar.
|
||||
* @param {e} set to {true} to enable the toolbar or {false}
|
||||
@@ -724,17 +783,52 @@ Toolbar = {
|
||||
|
||||
_addPopups(buttonElement, popups = []) {
|
||||
popups.forEach((popup) => {
|
||||
let popupElement = document.createElement("ul");
|
||||
const popupElement = document.createElement('div');
|
||||
popupElement.id = popup.id;
|
||||
popupElement.className = popup.className;
|
||||
let liElement = document.createElement("li");
|
||||
liElement.setAttribute("data-i18n", popup.dataAttr);
|
||||
popupElement.appendChild(liElement);
|
||||
popupElement.setAttribute('data-i18n', popup.dataAttr);
|
||||
|
||||
let gravity = 'n';
|
||||
if (popup.dataAttrPosition)
|
||||
gravity = popup.dataAttrPosition;
|
||||
// use custom attribute to save gravity option
|
||||
// we use 'data-tooltip' in UIUtil to activate all tooltips
|
||||
// but we want these to be manually triggered
|
||||
popupElement.setAttribute('tooltip-gravity', gravity);
|
||||
|
||||
APP.translation.translateElement($(popupElement));
|
||||
|
||||
buttonElement.appendChild(popupElement);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Show custom popup/tooltip for a specified button.
|
||||
* @param popupSelectorID the selector id of the popup to show
|
||||
* @param show true or false/show or hide the popup
|
||||
* @param timeout the time to show the popup
|
||||
*/
|
||||
_showCustomToolbarPopup(popupSelectorID, show, timeout) {
|
||||
|
||||
const gravity = $(popupSelectorID).attr('tooltip-gravity');
|
||||
AJS.$(popupSelectorID)
|
||||
.tooltip({
|
||||
trigger: 'manual',
|
||||
html: true,
|
||||
gravity: gravity,
|
||||
title: 'title'});
|
||||
if (show) {
|
||||
AJS.$(popupSelectorID).tooltip('show');
|
||||
setTimeout(function () {
|
||||
// hide the tooltip
|
||||
AJS.$(popupSelectorID).tooltip('hide');
|
||||
}, timeout);
|
||||
} else {
|
||||
AJS.$(popupSelectorID).tooltip('hide');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the toggled state of the given element depending on the isToggled
|
||||
* parameter.
|
||||
*
|
||||
|
||||
@@ -28,6 +28,7 @@ function RemoteVideo(user, VideoLayout, emitter) {
|
||||
this.emitter = emitter;
|
||||
this.videoSpanId = `participant_${this.id}`;
|
||||
SmallVideo.call(this, VideoLayout);
|
||||
this._audioStreamElement = null;
|
||||
this.hasRemoteVideoMenu = false;
|
||||
this._supportsRemoteControl = false;
|
||||
this.addRemoteVideoContainer();
|
||||
@@ -200,6 +201,18 @@ RemoteVideo.prototype._generatePopupContent = function () {
|
||||
popupmenuElement.appendChild(menuItem);
|
||||
});
|
||||
|
||||
// feature check for volume setting as temasys objects cannot adjust volume
|
||||
if (this._canSetAudioVolume()) {
|
||||
const volumeScale = 100;
|
||||
const volumeSlider = this._generatePopupMenuSliderItem({
|
||||
handler: this._setAudioVolume.bind(this, volumeScale),
|
||||
icon: 'icon-volume',
|
||||
initialValue: this._getAudioElement().volume * volumeScale,
|
||||
maxValue: volumeScale
|
||||
});
|
||||
popupmenuElement.appendChild(volumeSlider);
|
||||
}
|
||||
|
||||
APP.translation.translateElement($(popupmenuElement));
|
||||
|
||||
return popupmenuElement;
|
||||
@@ -343,6 +356,74 @@ RemoteVideo.prototype._generatePopupMenuItem = function (opts = {}) {
|
||||
return menuItem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a div element with a slider.
|
||||
*
|
||||
* @param {object} options - Configuration for the div's display and slider.
|
||||
* @param {string} options.icon - The classname for the icon to display.
|
||||
* @param {int} options.maxValue - The maximum value on the slider. The default
|
||||
* value is 100.
|
||||
* @param {int} options.initialValue - The value the slider should start at.
|
||||
* The default value is 0.
|
||||
* @param {function} options.handler - The callback for slider value changes.
|
||||
* @returns {Element} A div element with a slider.
|
||||
*/
|
||||
RemoteVideo.prototype._generatePopupMenuSliderItem = function (options) {
|
||||
const template = `<div class='popupmenu__contents'>
|
||||
<span class='popupmenu__icon'>
|
||||
<i class=${options.icon}></i>
|
||||
</span>
|
||||
<input class='popupmenu__slider'
|
||||
type='range'
|
||||
min='0'
|
||||
max=${options.maxValue || 100}
|
||||
value=${options.initialValue || 0}>
|
||||
</input>
|
||||
</div>`;
|
||||
|
||||
const menuItem = document.createElement('li');
|
||||
menuItem.className = 'popupmenu__item';
|
||||
menuItem.innerHTML = template;
|
||||
|
||||
const slider = menuItem.getElementsByClassName('popupmenu__slider')[0];
|
||||
slider.oninput = function () {
|
||||
options.handler(Number(slider.value));
|
||||
};
|
||||
|
||||
return menuItem;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the remote participant's audio element.
|
||||
*
|
||||
* @returns {Element} audio element
|
||||
*/
|
||||
RemoteVideo.prototype._getAudioElement = function () {
|
||||
return this._audioStreamElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the remote participant's audio can have its volume adjusted.
|
||||
*
|
||||
* @returns {boolean} true if the volume can be adjusted.
|
||||
*/
|
||||
RemoteVideo.prototype._canSetAudioVolume = function () {
|
||||
const audioElement = this._getAudioElement();
|
||||
return audioElement && audioElement.volume !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the remote participant's volume level.
|
||||
*
|
||||
* @param {int} scale - The maximum value the slider can go to.
|
||||
* @param {int} newVal - The value to set the slider to.
|
||||
*/
|
||||
RemoteVideo.prototype._setAudioVolume = function (scale, newVal) {
|
||||
if (this._canSetAudioVolume()) {
|
||||
this._getAudioElement().volume = newVal / scale;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the remote video menu.
|
||||
*
|
||||
@@ -613,6 +694,10 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
||||
}
|
||||
|
||||
$(streamElement).click(onClickHandler);
|
||||
|
||||
if (!isVideo) {
|
||||
this._audioStreamElement = streamElement;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -310,6 +310,12 @@ export class VideoContainer extends LargeContainer {
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight, animate = false) {
|
||||
// XXX Prevent TypeError: undefined is not an object when the Web
|
||||
// browser does not support WebRTC (yet).
|
||||
if (this.$video.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let [width, height]
|
||||
= this.getVideoSize(containerWidth, containerHeight);
|
||||
let { horizontalIndent, verticalIndent }
|
||||
|
||||
@@ -67,6 +67,17 @@ function onContactClicked (id) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for local flip X changed event.
|
||||
* @param {Object} val
|
||||
*/
|
||||
function onLocalFlipXChanged (val) {
|
||||
localFlipX = val;
|
||||
if(largeVideo) {
|
||||
largeVideo.onLocalFlipXChange(val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the corresponding resource id to the given peer container
|
||||
* DOM element.
|
||||
@@ -91,11 +102,10 @@ let largeVideo;
|
||||
var VideoLayout = {
|
||||
init (emitter) {
|
||||
eventEmitter = emitter;
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED, function (val) {
|
||||
localFlipX = val;
|
||||
if(largeVideo)
|
||||
largeVideo.onLocalFlipXChange(val);
|
||||
});
|
||||
|
||||
// Unregister listeners in case of reinitialization
|
||||
this.unregisterListeners();
|
||||
|
||||
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
|
||||
// sets default video type of local video
|
||||
// FIXME container type is totally different thing from the video type
|
||||
@@ -104,8 +114,29 @@ var VideoLayout = {
|
||||
// the local video thumb maybe one pixel
|
||||
this.resizeThumbnails(false, true);
|
||||
|
||||
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
||||
this.lastNCount = config.channelLastN;
|
||||
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* Registering listeners for UI events in Video layout component.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
registerListeners() {
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
||||
onLocalFlipXChanged);
|
||||
eventEmitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unregistering listeners for UI events in Video layout component.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
unregisterListeners() {
|
||||
eventEmitter.removeListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
||||
},
|
||||
|
||||
initLargeVideo () {
|
||||
|
||||
@@ -3,13 +3,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import UIUtil from '../UI/util/UIUtil';
|
||||
import jitsiLocalStorage from '../util/JitsiLocalStorage';
|
||||
|
||||
function generateUniqueId() {
|
||||
function _p8() {
|
||||
return (Math.random().toString(16) + "000000000").substr(2, 8);
|
||||
}
|
||||
return _p8() + _p8() + _p8() + _p8();
|
||||
}
|
||||
import { randomHexString } from '../../react/features/base/util';
|
||||
|
||||
let avatarUrl = '';
|
||||
|
||||
@@ -17,14 +11,13 @@ let email = UIUtil.unescapeHtml(jitsiLocalStorage.getItem("email") || '');
|
||||
let avatarId = UIUtil.unescapeHtml(jitsiLocalStorage.getItem("avatarId") || '');
|
||||
if (!avatarId) {
|
||||
// if there is no avatar id, we generate a unique one and use it forever
|
||||
avatarId = generateUniqueId();
|
||||
avatarId = randomHexString(32);
|
||||
jitsiLocalStorage.setItem("avatarId", avatarId);
|
||||
}
|
||||
|
||||
let localFlipX = JSON.parse(jitsiLocalStorage.getItem("localFlipX") || true);
|
||||
let displayName = UIUtil.unescapeHtml(
|
||||
jitsiLocalStorage.getItem("displayname") || '');
|
||||
let language = jitsiLocalStorage.getItem("language");
|
||||
let cameraDeviceId = jitsiLocalStorage.getItem("cameraDeviceId") || '';
|
||||
let micDeviceId = jitsiLocalStorage.getItem("micDeviceId") || '';
|
||||
let welcomePageDisabled = JSON.parse(
|
||||
@@ -113,14 +106,6 @@ export default {
|
||||
return avatarUrl;
|
||||
},
|
||||
|
||||
getLanguage () {
|
||||
return language;
|
||||
},
|
||||
setLanguage: function (lang) {
|
||||
language = lang;
|
||||
jitsiLocalStorage.setItem("language", lang);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets new flipX state of local video and saves it to the local storage.
|
||||
* @param {string} val flipX state of local video
|
||||
|
||||
@@ -1,108 +1,56 @@
|
||||
/* global $, require, config, interfaceConfig */
|
||||
import i18n from 'i18next';
|
||||
import XHR from 'i18next-xhr-backend';
|
||||
/* @flow */
|
||||
|
||||
import jqueryI18next from 'jquery-i18next';
|
||||
import languagesR from "../../lang/languages.json";
|
||||
import mainR from "../../lang/main.json";
|
||||
import languages from "../../service/translation/languages";
|
||||
|
||||
const DEFAULT_LANG = languages.EN;
|
||||
import { DEFAULT_LANGUAGE, i18next } from '../../react/features/base/i18n';
|
||||
|
||||
const defaultOptions = {
|
||||
compatibilityAPI: 'v1',
|
||||
compatibilityJSON: 'v1',
|
||||
fallbackLng: DEFAULT_LANG,
|
||||
load: "unspecific",
|
||||
resGetPath: 'lang/__ns__-__lng__.json',
|
||||
ns: {
|
||||
namespaces: ['main', 'languages'],
|
||||
defaultNs: 'main'
|
||||
},
|
||||
lngWhitelist : languages.getLanguages(),
|
||||
fallbackOnNull: true,
|
||||
fallbackOnEmpty: true,
|
||||
useDataAttrOptions: true,
|
||||
app: interfaceConfig.APP_NAME
|
||||
};
|
||||
declare var $: Function;
|
||||
|
||||
function initCompleted() {
|
||||
$("[data-i18n]").localize();
|
||||
}
|
||||
|
||||
function getLangFromQuery() {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
var pair = vars[i].split("=");
|
||||
if(pair[0] == "lang")
|
||||
{
|
||||
return pair[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
/**
|
||||
* Notifies that the {@link i18next} instance has finished its initialization.
|
||||
*
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function _onI18nInitialized() {
|
||||
$('[data-i18n]').localize();
|
||||
}
|
||||
|
||||
class Translation {
|
||||
init (settingsLang) {
|
||||
let options = defaultOptions;
|
||||
|
||||
let lang = getLangFromQuery() || settingsLang || config.defaultLanguage;
|
||||
// XXX If none of the above has been set then the 'lang' will be
|
||||
// 'undefined' and the i18n lib will try to auto detect user's
|
||||
// preferred language based on browser's locale.
|
||||
// The interface config option allows to disable this auto detection
|
||||
// by specifying the fallback language in that case.
|
||||
let langDetection = interfaceConfig.LANG_DETECTION;
|
||||
|
||||
if (!langDetection && !lang) {
|
||||
lang = DEFAULT_LANG;
|
||||
}
|
||||
|
||||
if (lang) {
|
||||
options.lng = lang;
|
||||
}
|
||||
|
||||
i18n.use(XHR)
|
||||
.use({
|
||||
type: 'postProcessor',
|
||||
name: "resolveAppName",
|
||||
process: (res, key) => {
|
||||
return i18n.t(key, {app: options.app});
|
||||
}
|
||||
})
|
||||
.init(options, initCompleted);
|
||||
// adds default language which is preloaded from code
|
||||
i18n.addResourceBundle(DEFAULT_LANG, 'main', mainR, true, true);
|
||||
i18n.addResourceBundle(
|
||||
DEFAULT_LANG, 'languages', languagesR, true, true);
|
||||
jqueryI18next.init(i18n, $, {useOptionsAttr: true});
|
||||
addLanguageChangedListener(listener: Function) {
|
||||
i18next.on('languageChanged', listener);
|
||||
}
|
||||
|
||||
setLanguage (lang) {
|
||||
if(!lang)
|
||||
lang = DEFAULT_LANG;
|
||||
i18n.setLng(lang, defaultOptions, initCompleted);
|
||||
}
|
||||
generateTranslationHTML(key: string, options: Object) {
|
||||
const optAttr
|
||||
= options ? ` data-i18n-options='${JSON.stringify(options)}'` : '';
|
||||
|
||||
getCurrentLanguage () {
|
||||
return i18n.lng();
|
||||
}
|
||||
// XXX i18next expects undefined if options are missing.
|
||||
const text = i18next.t(key, options ? options : undefined);
|
||||
|
||||
translateElement (selector, options) {
|
||||
// i18next expects undefined if options are missing, check if its null
|
||||
selector.localize(
|
||||
options === null ? undefined : options);
|
||||
}
|
||||
|
||||
generateTranslationHTML (key, options) {
|
||||
let optAttr = options
|
||||
? ` data-i18n-options='${JSON.stringify(options)}'` : "";
|
||||
let text = i18n.t(key, options === null ? undefined : options);
|
||||
return `<span data-i18n="${key}"${optAttr}>${text}</span>`;
|
||||
}
|
||||
|
||||
addLanguageChangedListener(listener) {
|
||||
i18n.on('languageChanged', listener);
|
||||
getCurrentLanguage() {
|
||||
return i18next.lng();
|
||||
}
|
||||
|
||||
init() {
|
||||
jqueryI18next.init(i18next, $, { useOptionsAttr: true });
|
||||
|
||||
if (i18next.isInitialized)
|
||||
_onI18nInitialized();
|
||||
else
|
||||
i18next.on('initialized', _onI18nInitialized);
|
||||
}
|
||||
|
||||
setLanguage(language: string = DEFAULT_LANGUAGE) {
|
||||
i18next.setLng(language, {}, _onI18nInitialized);
|
||||
}
|
||||
|
||||
translateElement(selector: Object, options: Object) {
|
||||
// XXX i18next expects undefined if options are missing.
|
||||
selector.localize(options ? options : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
90
package.json
@@ -16,69 +16,75 @@
|
||||
"readmeFilename": "README.md",
|
||||
"//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
|
||||
"dependencies": {
|
||||
"@atlassian/aui": "^6.0.0",
|
||||
"@atlassian/aui": "6.0.6",
|
||||
"async": "0.9.0",
|
||||
"autosize": "^1.18.13",
|
||||
"autosize": "1.18.13",
|
||||
"bootstrap": "3.1.1",
|
||||
"i18next": "3.4.4",
|
||||
"i18next-xhr-backend": "1.1.0",
|
||||
"es6-iterator": "2.0.0",
|
||||
"es6-symbol": "3.1.0",
|
||||
"i18next": "7.0.0",
|
||||
"i18next-browser-languagedetector": "1.0.1",
|
||||
"i18next-xhr-backend": "1.3.0",
|
||||
"jitsi-meet-logger": "jitsi/jitsi-meet-logger",
|
||||
"jquery": "~2.1.1",
|
||||
"jquery-contextmenu": "*",
|
||||
"jquery": "2.1.4",
|
||||
"jquery-contextmenu": "2.4.3",
|
||||
"jquery-i18next": "1.1.0",
|
||||
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
|
||||
"jquery-ui": "1.10.5",
|
||||
"jssha": "1.5.0",
|
||||
"jws": "*",
|
||||
"jws": "3.1.4",
|
||||
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
|
||||
"postis": "^2.2.0",
|
||||
"postis": "2.2.0",
|
||||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-native": "0.41.2",
|
||||
"react-i18next": "2.2.0",
|
||||
"react-native": "0.42.0",
|
||||
"react-native-background-timer": "1.0.0",
|
||||
"react-native-immersive": "0.0.4",
|
||||
"react-native-keep-awake": "^2.0.2",
|
||||
"react-native-prompt": "^1.0.0",
|
||||
"react-native-vector-icons": "^4.0.0",
|
||||
"react-native-keep-awake": "2.0.2",
|
||||
"react-native-locale-detector": "1.0.1",
|
||||
"react-native-prompt": "1.0.0",
|
||||
"react-native-vector-icons": "4.0.0",
|
||||
"react-native-webrtc": "jitsi/react-native-webrtc",
|
||||
"react-redux": "^5.0.2",
|
||||
"redux": "^3.5.2",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"react-redux": "5.0.3",
|
||||
"redux": "3.6.0",
|
||||
"redux-thunk": "2.2.0",
|
||||
"retry": "0.6.1",
|
||||
"strophe": "1.2.4",
|
||||
"strophejs-plugins": "0.0.7",
|
||||
"toastr": "^2.0.3",
|
||||
"toastr": "2.1.2",
|
||||
"url-polyfill": "github/url-polyfill",
|
||||
"xmldom": "^0.1.27"
|
||||
"xmldom": "0.1.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-loader": "^6.2.10",
|
||||
"babel-polyfill": "*",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"babel-preset-react": "^6.16.0",
|
||||
"babel-preset-stage-1": "^6.16.0",
|
||||
"clean-css": "^3.0.0",
|
||||
"css-loader": "*",
|
||||
"eslint": "^3.15.0",
|
||||
"eslint-plugin-flowtype": "^2.30.0",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-jsdoc": "*",
|
||||
"eslint-plugin-react": "*",
|
||||
"eslint-plugin-react-native": "^2.2.1",
|
||||
"expose-loader": "*",
|
||||
"file-loader": "^0.10.0",
|
||||
"flow-bin": "^0.37.0",
|
||||
"haste-resolver-webpack-plugin": "^0.2.2",
|
||||
"imports-loader": "*",
|
||||
"babel-core": "6.23.1",
|
||||
"babel-eslint": "7.1.1",
|
||||
"babel-loader": "6.3.2",
|
||||
"babel-polyfill": "6.23.0",
|
||||
"babel-preset-es2015": "6.22.0",
|
||||
"babel-preset-react": "6.23.0",
|
||||
"babel-preset-stage-1": "6.22.0",
|
||||
"clean-css": "3.4.25",
|
||||
"css-loader": "0.26.2",
|
||||
"eslint": "3.16.1",
|
||||
"eslint-plugin-flowtype": "2.30.0",
|
||||
"eslint-plugin-import": "2.2.0",
|
||||
"eslint-plugin-jsdoc": "2.4.0",
|
||||
"eslint-plugin-react": "6.10.0",
|
||||
"eslint-plugin-react-native": "2.2.1",
|
||||
"expose-loader": "0.7.1",
|
||||
"file-loader": "0.10.1",
|
||||
"flow-bin": "0.38.0",
|
||||
"haste-resolver-webpack-plugin": "0.2.2",
|
||||
"imports-loader": "0.7.1",
|
||||
"jshint": "2.9.4",
|
||||
"json-loader": "0.5.4",
|
||||
"node-sass": "^3.8.0",
|
||||
"node-sass": "3.13.1",
|
||||
"precommit-hook": "3.0.0",
|
||||
"string-replace-loader": "*",
|
||||
"style-loader": "*",
|
||||
"webpack": "^1.14.0",
|
||||
"webpack-dev-server": "^1.16.2"
|
||||
"string-replace-loader": "1.0.5",
|
||||
"style-loader": "0.13.2",
|
||||
"webpack": "1.14.0",
|
||||
"webpack-dev-server": "1.16.3"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
_parseURIString,
|
||||
init
|
||||
} from './functions';
|
||||
import './reducer';
|
||||
|
||||
/**
|
||||
* Temporary solution. Should dispatch actions related to initial settings of
|
||||
@@ -25,16 +24,26 @@ export function appInit() {
|
||||
* Triggers an in-app navigation to a different route. Allows navigation to be
|
||||
* abstracted between the mobile and web versions.
|
||||
*
|
||||
* @param {(string|undefined)} urlOrRoom - The URL or room name to which to
|
||||
* navigate.
|
||||
* @param {(string|undefined)} uri - The URI to which to navigate. It may be a
|
||||
* full URL with an http(s) scheme, a full or partial URI with the app-specific
|
||||
* sheme, or a mere room name.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function appNavigate(urlOrRoom) {
|
||||
export function appNavigate(uri) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const oldDomain = getDomain(state);
|
||||
|
||||
const { domain, room } = _parseURIString(urlOrRoom);
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { domain, room } = _parseURIString(uri);
|
||||
|
||||
// If the specified URI does not identify a domain, use the app's
|
||||
// default.
|
||||
if (typeof domain === 'undefined') {
|
||||
domain
|
||||
= _parseURIString(state['features/app'].app._getDefaultURL())
|
||||
.domain;
|
||||
}
|
||||
|
||||
// TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
|
||||
// currently in a conference and ask her if she wants to close the
|
||||
@@ -49,7 +58,7 @@ export function appNavigate(urlOrRoom) {
|
||||
dispatch(setDomain(domain));
|
||||
|
||||
// If domain has changed, we need to load the config of the new
|
||||
// domain and set it, and only after that we can navigate to
|
||||
// domain and set it, and only after that we can navigate to a
|
||||
// different route.
|
||||
loadConfig(`https://${domain}`)
|
||||
.then(
|
||||
@@ -93,7 +102,7 @@ export function appNavigate(urlOrRoom) {
|
||||
dispatch(
|
||||
_setRoomAndNavigate(
|
||||
typeof room === 'undefined' && typeof domain === 'undefined'
|
||||
? urlOrRoom
|
||||
? uri
|
||||
: room));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { compose, createStore } from 'redux';
|
||||
import Thunk from 'redux-thunk';
|
||||
|
||||
import { i18next } from '../../base/i18n';
|
||||
import {
|
||||
localParticipantJoined,
|
||||
localParticipantLeft
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
appWillUnmount
|
||||
} from '../actions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Base (abstract) class for main App component.
|
||||
*
|
||||
@@ -74,9 +78,24 @@ export class AbstractApp extends Component {
|
||||
|
||||
dispatch(appWillMount(this));
|
||||
|
||||
dispatch(localParticipantJoined());
|
||||
// FIXME I believe it makes more sense for a middleware to dispatch
|
||||
// localParticipantJoined on APP_WILL_MOUNT because the order of actions
|
||||
// is important, not the call site. Moreover, we've got localParticipant
|
||||
// business logic in the React Component (i.e. UI) AbstractApp now.
|
||||
let localParticipant;
|
||||
|
||||
this._openURL(this._getDefaultURL());
|
||||
if (typeof APP !== 'undefined') {
|
||||
localParticipant = {
|
||||
avatarID: APP.settings.getAvatarId(),
|
||||
avatarURL: APP.settings.getAvatarUrl(),
|
||||
email: APP.settings.getEmail()
|
||||
};
|
||||
}
|
||||
dispatch(localParticipantJoined(localParticipant));
|
||||
|
||||
// If a URL was explicitly specified to this React Component, then open
|
||||
// it; otherwise, use a default.
|
||||
this._openURL(this.props.url || this._getDefaultURL());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +138,21 @@ export class AbstractApp extends Component {
|
||||
dispatch(appWillUnmount(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Location object from the window with information about the current
|
||||
* location of the document. Explicitly defined to allow extenders to
|
||||
* override because React Native does not usually have a location property
|
||||
* on its window unless debugging remotely in which case the browser that is
|
||||
* the remote debugger will provide a location property on the window.
|
||||
*
|
||||
* @public
|
||||
* @returns {Location} A Location object with information about the current
|
||||
* location of the document.
|
||||
*/
|
||||
getWindowLocation() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -130,11 +164,13 @@ export class AbstractApp extends Component {
|
||||
|
||||
if (route) {
|
||||
return (
|
||||
<Provider store = { this._getStore() }>
|
||||
{
|
||||
this._createElement(route.component)
|
||||
}
|
||||
</Provider>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Provider store = { this._getStore() }>
|
||||
{
|
||||
this._createElement(route.component)
|
||||
}
|
||||
</Provider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -211,27 +247,20 @@ export class AbstractApp extends Component {
|
||||
/**
|
||||
* Gets the default URL to be opened when this App mounts.
|
||||
*
|
||||
* @private
|
||||
* @protected
|
||||
* @returns {string} The default URL to be opened when this App mounts.
|
||||
*/
|
||||
_getDefaultURL() {
|
||||
// If the URL was explicitly specified to the React Component, then open
|
||||
// it.
|
||||
let url = this.props.url;
|
||||
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// If the execution environment provides a Location abstraction, then
|
||||
// this App at already at that location but it must be made aware of the
|
||||
// fact.
|
||||
const windowLocation = this._getWindowLocation();
|
||||
const windowLocation = this.getWindowLocation();
|
||||
|
||||
if (windowLocation) {
|
||||
url = windowLocation.toString();
|
||||
if (url) {
|
||||
return url;
|
||||
const href = windowLocation.toString();
|
||||
|
||||
if (href) {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,21 +300,6 @@ export class AbstractApp extends Component {
|
||||
return store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a Location object from the window with information about the current
|
||||
* location of the document. Explicitly defined to allow extenders to
|
||||
* override because React Native does not usually have a location property
|
||||
* on its window unless debugging remotely in which case the browser that is
|
||||
* the remote debugger will provide a location property on the window.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Location} A Location object with information about the current
|
||||
* location of the document.
|
||||
*/
|
||||
_getWindowLocation() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Redux store to be used by this AbstractApp if such as store is
|
||||
* not defined by the consumer of this AbstractApp through its
|
||||
@@ -305,6 +319,14 @@ export class AbstractApp extends Component {
|
||||
|
||||
if (typeof store === 'undefined') {
|
||||
store = this._createStore();
|
||||
|
||||
// This is temporary workaround to be able to dispatch actions from
|
||||
// non-reactified parts of the code (conference.js for example).
|
||||
// Don't use in the react code!!!
|
||||
// FIXME: remove when the reactification is finished!
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.store = store;
|
||||
}
|
||||
}
|
||||
|
||||
return store;
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import { Platform } from '../../base/react';
|
||||
import '../../audio-mode';
|
||||
import '../../background';
|
||||
import { Platform } from '../../base/react';
|
||||
import '../../full-screen';
|
||||
import '../../wake-lock';
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class App extends AbstractApp {
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_getWindowLocation() {
|
||||
getWindowLocation() {
|
||||
return window.location;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export class App extends AbstractApp {
|
||||
* @returns {string} The context root of window.location i.e. this Web App.
|
||||
*/
|
||||
_getWindowLocationContextRoot() {
|
||||
const pathname = this._getWindowLocation().pathname;
|
||||
const pathname = this.getWindowLocation().pathname;
|
||||
const contextRootEndIndex = pathname.lastIndexOf('/');
|
||||
|
||||
return (
|
||||
@@ -93,7 +93,7 @@ export class App extends AbstractApp {
|
||||
path = this._routePath2WindowLocationPathname(path);
|
||||
|
||||
// Navigate to the specified Route.
|
||||
const windowLocation = this._getWindowLocation();
|
||||
const windowLocation = this.getWindowLocation();
|
||||
|
||||
if (windowLocation.pathname === path) {
|
||||
// The browser is at the specified path already and what remains is
|
||||
|
||||
@@ -126,7 +126,7 @@ function _getRoomAndDomainFromURLObject(url) {
|
||||
let room;
|
||||
|
||||
if (url) {
|
||||
domain = url.hostname;
|
||||
domain = url.host;
|
||||
|
||||
// The room (name) is the last component of pathname.
|
||||
room = url.pathname;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* global APP, JitsiMeetJS, loggingConfig */
|
||||
/* global APP, loggingConfig */
|
||||
|
||||
import { isRoomValid } from '../base/conference';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { RouteRegistry } from '../base/react';
|
||||
import { interceptComponent } from '../base/util';
|
||||
import { Conference } from '../conference';
|
||||
@@ -9,7 +10,6 @@ import { WelcomePage } from '../welcome';
|
||||
import URLProcessor from '../../../modules/config/URLProcessor';
|
||||
import KeyboardShortcut
|
||||
from '../../../modules/keyboardshortcut/keyboardshortcut';
|
||||
import settings from '../../../modules/settings/Settings';
|
||||
import getTokenData from '../../../modules/tokendata/TokenData';
|
||||
import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage';
|
||||
|
||||
@@ -61,7 +61,7 @@ export function init() {
|
||||
// with jitsi meet.
|
||||
APP.API.init(APP.tokenData.jwt ? { forceEnable: true } : undefined);
|
||||
|
||||
APP.translation.init(settings.getLanguage());
|
||||
APP.translation.init();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,3 +2,5 @@ export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
|
||||
43
react/features/background/actionTypes.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Symbol } from '../base/react';
|
||||
|
||||
/**
|
||||
* The type of redux action to set the AppState API change event listener.
|
||||
*
|
||||
* {
|
||||
* type: _SET_APP_STATE_LISTENER,
|
||||
* listener: Function
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_APP_STATE_LISTENER
|
||||
= Symbol('_SET_APP_STATE_LISTENER');
|
||||
|
||||
/**
|
||||
* The type of redux action which signals that video will be muted because the
|
||||
* app is going to the background.
|
||||
*
|
||||
* {
|
||||
* type: _SET_BACKGROUND_VIDEO_MUTED,
|
||||
* muted: boolean
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_BACKGROUND_VIDEO_MUTED
|
||||
= Symbol('_SET_BACKGROUND_VIDEO_MUTED');
|
||||
|
||||
/**
|
||||
* The type of redux action which signals that the app state has changed (in
|
||||
* terms of execution mode). The app state can be one of 'active', 'inactive',
|
||||
* or 'background'.
|
||||
*
|
||||
* {
|
||||
* type: APP_STATE_CHANGED,
|
||||
* appState: string
|
||||
* }
|
||||
*
|
||||
* @public
|
||||
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
|
||||
*/
|
||||
export const APP_STATE_CHANGED = Symbol('APP_STATE_CHANGED');
|
||||
81
react/features/background/actions.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { setVideoMuted } from '../base/media';
|
||||
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
_SET_BACKGROUND_VIDEO_MUTED,
|
||||
APP_STATE_CHANGED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Signals that the App state has changed (in terms of execution state). The
|
||||
* application can be in 3 states: 'active', 'inactive' and 'background'.
|
||||
*
|
||||
* @param {string} appState - The new App state.
|
||||
* @public
|
||||
* @returns {{
|
||||
* type: APP_STATE_CHANGED,
|
||||
* appState: string
|
||||
* }}
|
||||
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
|
||||
*/
|
||||
export function appStateChanged(appState: string) {
|
||||
return {
|
||||
type: APP_STATE_CHANGED,
|
||||
appState
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to be used with React Native's AppState API.
|
||||
*
|
||||
* @param {Function} listener - Function to be set as the change event listener.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: _SET_APP_STATE_LISTENER,
|
||||
* listener: Function
|
||||
* }}
|
||||
*/
|
||||
export function _setAppStateListener(listener: ?Function) {
|
||||
return {
|
||||
type: _SET_APP_STATE_LISTENER,
|
||||
listener
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the app should mute video because it's now running in the
|
||||
* background, or unmute it because it came back from the background. If video
|
||||
* was already muted nothing will happen; otherwise, it will be muted. When
|
||||
* coming back from the background the previous state will be restored.
|
||||
*
|
||||
* @param {boolean} muted - True if video should be muted; false, otherwise.
|
||||
* @protected
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function _setBackgroundVideoMuted(muted: boolean) {
|
||||
return (dispatch, getState) => {
|
||||
if (muted) {
|
||||
const mediaState = getState()['features/base/media'];
|
||||
|
||||
if (mediaState.video.muted) {
|
||||
// Video is already muted, do nothing.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const bgState = getState()['features/background'];
|
||||
|
||||
if (!bgState.videoMuted) {
|
||||
// We didn't mute video, do nothing.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remember that video was muted due to the app going to the background
|
||||
// vs user's choice.
|
||||
dispatch({
|
||||
type: _SET_BACKGROUND_VIDEO_MUTED,
|
||||
muted
|
||||
});
|
||||
dispatch(setVideoMuted(muted));
|
||||
};
|
||||
}
|
||||
5
react/features/background/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
109
react/features/background/middleware.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/* @flow */
|
||||
|
||||
import { AppState } from 'react-native';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
APP_WILL_MOUNT,
|
||||
APP_WILL_UNMOUNT
|
||||
} from '../app';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
_setAppStateListener,
|
||||
_setBackgroundVideoMuted,
|
||||
appStateChanged
|
||||
} from './actions';
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
APP_STATE_CHANGED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Middleware that captures App lifetime actions and subscribes to application
|
||||
* state changes. When the application state changes it will fire the action
|
||||
* required to mute or unmute the local video in case the application goes to
|
||||
* the background or comes back from it.
|
||||
*
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Function}
|
||||
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case _SET_APP_STATE_LISTENER: {
|
||||
// Remove the current/old AppState listener.
|
||||
const { appStateListener } = store.getState()['features/background'];
|
||||
|
||||
if (appStateListener) {
|
||||
AppState.removeEventListener('change', appStateListener);
|
||||
}
|
||||
|
||||
// Add the new AppState listener.
|
||||
if (action.listener) {
|
||||
AppState.addEventListener('change', action.listener);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case APP_STATE_CHANGED:
|
||||
_appStateChanged(store.dispatch, action.appState);
|
||||
break;
|
||||
|
||||
case APP_WILL_MOUNT:
|
||||
store.dispatch(
|
||||
_setAppStateListener(
|
||||
_onAppStateChange.bind(undefined, store.dispatch)));
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
store.dispatch(_setAppStateListener(null));
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles app state changes. Dispatches the necessary Redux actions for the
|
||||
* local video to be muted when the app goes to the background, and to be
|
||||
* unmuted when the app comes back.
|
||||
*
|
||||
* @param {Dispatch} dispatch - Redux dispatch function.
|
||||
* @param {string} appState - The current app state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _appStateChanged(dispatch: Dispatch<*>, appState: string) {
|
||||
let muted;
|
||||
|
||||
switch (appState) {
|
||||
case 'active':
|
||||
muted = false;
|
||||
break;
|
||||
|
||||
case 'background':
|
||||
muted = true;
|
||||
break;
|
||||
|
||||
case 'inactive':
|
||||
default:
|
||||
// XXX: We purposely don't handle the 'inactive' app state.
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(_setBackgroundVideoMuted(muted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by React Native's AppState API to notify that the application state
|
||||
* has changed. Dispatches the change within the (associated) Redux store.
|
||||
*
|
||||
* @param {Dispatch} dispatch - Redux dispatch function.
|
||||
* @param {string} appState - The current application execution state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onAppStateChange(dispatch: Dispatch<*>, appState: string) {
|
||||
dispatch(appStateChanged(appState));
|
||||
}
|
||||
31
react/features/background/reducer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
_SET_BACKGROUND_VIDEO_MUTED,
|
||||
APP_STATE_CHANGED
|
||||
} from './actionTypes';
|
||||
|
||||
ReducerRegistry.register('features/background', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case _SET_APP_STATE_LISTENER:
|
||||
return {
|
||||
...state,
|
||||
appStateListener: action.listener
|
||||
};
|
||||
|
||||
case _SET_BACKGROUND_VIDEO_MUTED:
|
||||
return {
|
||||
...state,
|
||||
videoMuted: action.muted
|
||||
};
|
||||
|
||||
case APP_STATE_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
appState: action.appState
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -1,7 +1,10 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
||||
import {
|
||||
changeParticipantAvatarID,
|
||||
changeParticipantAvatarURL,
|
||||
changeParticipantEmail,
|
||||
dominantSpeakerChanged,
|
||||
getLocalParticipant,
|
||||
participantJoined,
|
||||
participantLeft,
|
||||
participantRoleChanged
|
||||
@@ -18,10 +21,12 @@ import {
|
||||
SET_PASSWORD,
|
||||
SET_ROOM
|
||||
} from './actionTypes';
|
||||
import { EMAIL_COMMAND } from './constants';
|
||||
import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND
|
||||
} from './constants';
|
||||
import { _addLocalTracksToConference } from './functions';
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
/**
|
||||
* Adds conference (event) listeners.
|
||||
@@ -32,17 +37,15 @@ import './reducer';
|
||||
* @returns {void}
|
||||
*/
|
||||
function _addConferenceListeners(conference, dispatch) {
|
||||
const JitsiConferenceEvents = JitsiMeetJS.events.conference;
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_FAILED,
|
||||
(...args) => dispatch(_conferenceFailed(conference, ...args)));
|
||||
(...args) => dispatch(conferenceFailed(conference, ...args)));
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_JOINED,
|
||||
(...args) => dispatch(_conferenceJoined(conference, ...args)));
|
||||
(...args) => dispatch(conferenceJoined(conference, ...args)));
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_LEFT,
|
||||
(...args) => dispatch(_conferenceLeft(conference, ...args)));
|
||||
(...args) => dispatch(conferenceLeft(conference, ...args)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||
@@ -73,11 +76,34 @@ function _addConferenceListeners(conference, dispatch) {
|
||||
JitsiConferenceEvents.USER_ROLE_CHANGED,
|
||||
(...args) => dispatch(participantRoleChanged(...args)));
|
||||
|
||||
conference.addCommandListener(
|
||||
AVATAR_ID_COMMAND,
|
||||
(data, id) => dispatch(changeParticipantAvatarID(id, data.value)));
|
||||
conference.addCommandListener(
|
||||
AVATAR_URL_COMMAND,
|
||||
(data, id) => dispatch(changeParticipantAvatarURL(id, data.value)));
|
||||
conference.addCommandListener(
|
||||
EMAIL_COMMAND,
|
||||
(data, id) => dispatch(changeParticipantEmail(id, data.value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data for the local participant to the conference.
|
||||
*
|
||||
* @param {JitsiConference} conference - The JitsiConference instance.
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setLocalParticipantData(conference, state) {
|
||||
const localParticipant
|
||||
= getLocalParticipant(state['features/base/participants']);
|
||||
|
||||
conference.removeCommand(AVATAR_ID_COMMAND);
|
||||
conference.sendCommand(AVATAR_ID_COMMAND, {
|
||||
value: localParticipant.avatarID
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a specific conference has failed.
|
||||
*
|
||||
@@ -89,8 +115,9 @@ function _addConferenceListeners(conference, dispatch) {
|
||||
* conference: JitsiConference,
|
||||
* error: string
|
||||
* }}
|
||||
* @public
|
||||
*/
|
||||
function _conferenceFailed(conference, error) {
|
||||
export function conferenceFailed(conference, error) {
|
||||
return {
|
||||
type: CONFERENCE_FAILED,
|
||||
conference,
|
||||
@@ -106,7 +133,7 @@ function _conferenceFailed(conference, error) {
|
||||
* joined by the local participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
function _conferenceJoined(conference) {
|
||||
export function conferenceJoined(conference) {
|
||||
return (dispatch, getState) => {
|
||||
const localTracks
|
||||
= getState()['features/base/tracks']
|
||||
@@ -134,7 +161,7 @@ function _conferenceJoined(conference) {
|
||||
* conference: JitsiConference
|
||||
* }}
|
||||
*/
|
||||
function _conferenceLeft(conference) {
|
||||
export function conferenceLeft(conference) {
|
||||
return {
|
||||
type: CONFERENCE_LEFT,
|
||||
conference
|
||||
@@ -206,12 +233,22 @@ export function createConference() {
|
||||
const conference
|
||||
= connection.initJitsiConference(
|
||||
|
||||
// XXX Lib-jitsi-meet does not accept uppercase letters.
|
||||
room.toLowerCase(),
|
||||
{ openSctp: true });
|
||||
// XXX Lib-jitsi-meet does not accept uppercase letters.
|
||||
room.toLowerCase(),
|
||||
{
|
||||
openSctp: true
|
||||
|
||||
// FIXME I tested H.264 from iPhone 6S during a morning
|
||||
// standup but, unfortunately, the other participants who
|
||||
// happened to be running the Web app saw only black.
|
||||
//
|
||||
// preferH264: true
|
||||
});
|
||||
|
||||
_addConferenceListeners(conference, dispatch);
|
||||
|
||||
_setLocalParticipantData(conference, state);
|
||||
|
||||
conference.join(password);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
/**
|
||||
* The command type for updating a participant's email address.
|
||||
* The command type for updating a participant's avatar ID.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const AVATAR_ID_COMMAND = 'avatar-id';
|
||||
|
||||
/**
|
||||
* The command type for updating a participant's avatar URL.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const AVATAR_URL_COMMAND = 'avatar-url';
|
||||
|
||||
/**
|
||||
* The command type for updating a participant's e-mail address.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
|
||||
const JitsiTrackErrors = JitsiMeetJS.errors.track;
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
|
||||
/**
|
||||
* Attach a set of local tracks to a conference.
|
||||
@@ -19,8 +17,8 @@ export function _addLocalTracksToConference(conference, localTracks) {
|
||||
// XXX The library lib-jitsi-meet may be draconian, for example, when
|
||||
// adding one and the same video track multiple times.
|
||||
if (conferenceLocalTracks.indexOf(track) === -1) {
|
||||
promises.push(conference.addTrack(track)
|
||||
.catch(err => {
|
||||
promises.push(
|
||||
conference.addTrack(track).catch(err => {
|
||||
_reportError(
|
||||
'Failed to add local track to conference',
|
||||
err);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global APP */
|
||||
import { CONNECTION_ESTABLISHED } from '../connection';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -53,7 +54,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
function _connectionEstablished(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
store.dispatch(createConference());
|
||||
// FIXME: workaround for the web version. Currently the creation of the
|
||||
// conference is handled by /conference.js
|
||||
if (typeof APP === 'undefined') {
|
||||
store.dispatch(createConference());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import {
|
||||
ReducerRegistry,
|
||||
setStateProperties,
|
||||
@@ -64,7 +64,6 @@ function _conferenceFailed(state, action) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
|
||||
const passwordRequired
|
||||
= JitsiConferenceErrors.PASSWORD_REQUIRED === action.error
|
||||
? conference
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { conferenceWillLeave } from '../conference';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
|
||||
|
||||
import {
|
||||
CONNECTION_DISCONNECTED,
|
||||
@@ -11,9 +11,6 @@ import {
|
||||
CONNECTION_FAILED,
|
||||
SET_DOMAIN
|
||||
} from './actionTypes';
|
||||
import './reducer';
|
||||
|
||||
const JitsiConnectionEvents = JitsiMeetJS.events.connection;
|
||||
|
||||
/**
|
||||
* Opens new connection.
|
||||
@@ -23,17 +20,16 @@ const JitsiConnectionEvents = JitsiMeetJS.events.connection;
|
||||
export function connect() {
|
||||
return (dispatch: Dispatch<*>, getState: Function) => {
|
||||
const state = getState();
|
||||
const connectionOptions
|
||||
= state['features/base/connection'].connectionOptions;
|
||||
const room = state['features/base/conference'].room;
|
||||
const { options } = state['features/base/connection'];
|
||||
const { room } = state['features/base/conference'];
|
||||
const connection
|
||||
= new JitsiMeetJS.JitsiConnection(
|
||||
connectionOptions.appId,
|
||||
connectionOptions.token,
|
||||
options.appId,
|
||||
options.token,
|
||||
{
|
||||
...connectionOptions,
|
||||
...options,
|
||||
bosh:
|
||||
connectionOptions.bosh
|
||||
options.bosh
|
||||
|
||||
// XXX The Jitsi Meet deployments require the room
|
||||
// argument to be in lower case at the time of this
|
||||
@@ -44,13 +40,13 @@ export function connect() {
|
||||
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
|
||||
connectionDisconnected);
|
||||
_onConnectionDisconnected);
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
connectionEstablished);
|
||||
_onConnectionEstablished);
|
||||
connection.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
connectionFailed);
|
||||
_onConnectionFailed);
|
||||
|
||||
connection.connect();
|
||||
|
||||
@@ -60,11 +56,12 @@ export function connect() {
|
||||
*
|
||||
* @param {string} message - Disconnect reason.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function connectionDisconnected(message: string) {
|
||||
function _onConnectionDisconnected(message: string) {
|
||||
connection.removeEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
|
||||
connectionDisconnected);
|
||||
_onConnectionDisconnected);
|
||||
|
||||
dispatch(_connectionDisconnected(connection, message));
|
||||
}
|
||||
@@ -73,10 +70,11 @@ export function connect() {
|
||||
* Resolves external promise when connection is established.
|
||||
*
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function connectionEstablished() {
|
||||
function _onConnectionEstablished() {
|
||||
unsubscribe();
|
||||
dispatch(_connectionEstablished(connection));
|
||||
dispatch(connectionEstablished(connection));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,11 +82,12 @@ export function connect() {
|
||||
*
|
||||
* @param {JitsiConnectionErrors} err - Connection error.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function connectionFailed(err) {
|
||||
function _onConnectionFailed(err) {
|
||||
unsubscribe();
|
||||
console.error('CONNECTION FAILED:', err);
|
||||
dispatch(_connectionFailed(connection, err));
|
||||
dispatch(connectionFailed(connection, err, ''));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,10 +99,10 @@ export function connect() {
|
||||
function unsubscribe() {
|
||||
connection.removeEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
connectionEstablished);
|
||||
_onConnectionEstablished);
|
||||
connection.removeEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
connectionFailed);
|
||||
_onConnectionFailed);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -184,13 +183,13 @@ function _connectionDisconnected(connection, message: string) {
|
||||
*
|
||||
* @param {JitsiConnection} connection - The JitsiConnection which was
|
||||
* established.
|
||||
* @private
|
||||
* @returns {{
|
||||
* type: CONNECTION_ESTABLISHED,
|
||||
* connection: JitsiConnection
|
||||
* }}
|
||||
* @public
|
||||
*/
|
||||
function _connectionEstablished(connection) {
|
||||
export function connectionEstablished(connection: Object) {
|
||||
return {
|
||||
type: CONNECTION_ESTABLISHED,
|
||||
connection
|
||||
@@ -201,18 +200,22 @@ function _connectionEstablished(connection) {
|
||||
* Create an action for when the signaling connection could not be created.
|
||||
*
|
||||
* @param {JitsiConnection} connection - The JitsiConnection which failed.
|
||||
* @param {string} error - Error message.
|
||||
* @private
|
||||
* @param {string} error - Error.
|
||||
* @param {string} errorMessage - Error message.
|
||||
* @returns {{
|
||||
* type: CONNECTION_FAILED,
|
||||
* connection: JitsiConnection,
|
||||
* error: string
|
||||
* error: string,
|
||||
* errorMessage: string
|
||||
* }}
|
||||
* @public
|
||||
*/
|
||||
function _connectionFailed(connection, error: string) {
|
||||
export function connectionFailed(
|
||||
connection: Object, error: string, errorMessage: string) {
|
||||
return {
|
||||
type: CONNECTION_FAILED,
|
||||
connection,
|
||||
error
|
||||
error,
|
||||
errorMessage
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,17 +2,27 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
JitsiConferenceEvents,
|
||||
libInitError,
|
||||
WEBRTC_NOT_READY,
|
||||
WEBRTC_NOT_SUPPORTED
|
||||
} from '../lib-jitsi-meet';
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { SET_DOMAIN } from './actionTypes';
|
||||
import './reducer';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var JitsiMeetJS: Object;
|
||||
declare var config: Object;
|
||||
|
||||
const JitsiConferenceEvents = JitsiMeetJS.events.conference;
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
export {
|
||||
connectionEstablished,
|
||||
connectionFailed
|
||||
} from './actions.native.js';
|
||||
|
||||
/**
|
||||
* Opens new connection.
|
||||
*
|
||||
@@ -58,17 +68,30 @@ export function connect() {
|
||||
|
||||
APP.UI.initConference();
|
||||
|
||||
APP.UI.addListener(UIEvents.LANG_CHANGED, language => {
|
||||
APP.translation.setLanguage(language);
|
||||
APP.settings.setLanguage(language);
|
||||
});
|
||||
APP.UI.addListener(
|
||||
UIEvents.LANG_CHANGED,
|
||||
language => APP.translation.setLanguage(language));
|
||||
|
||||
APP.keyboardshortcut.init();
|
||||
|
||||
if (config.requireDisplayName && !APP.settings.getDisplayName()) {
|
||||
APP.UI.promptDisplayName();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
.catch(error => {
|
||||
APP.UI.hideRingOverLay();
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
logger.error(err);
|
||||
logger.error(error);
|
||||
|
||||
// TODO The following are in fact Errors raised by
|
||||
// JitsiMeetJS.init() which should be taken care of in
|
||||
// features/base/lib-jitsi-meet but we are not there yet on the
|
||||
// Web at the time of this writing.
|
||||
switch (error.name) {
|
||||
case WEBRTC_NOT_READY:
|
||||
case WEBRTC_NOT_SUPPORTED:
|
||||
dispatch(libInitError(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ export function getDomain(stateOrGetState: Function | Object) {
|
||||
= typeof stateOrGetState === 'function'
|
||||
? stateOrGetState()
|
||||
: stateOrGetState;
|
||||
const connection = state['features/base/connection'];
|
||||
const { options } = state['features/base/connection'];
|
||||
let domain;
|
||||
|
||||
try {
|
||||
domain = connection.connectionOptions.hosts.domain;
|
||||
domain = options.hosts.domain;
|
||||
} catch (e) {
|
||||
// XXX The value of connectionOptions or any of the properties
|
||||
// descending from it may be undefined at some point in the execution
|
||||
// (e.g. on start). Instead of multiple checks for the undefined value,
|
||||
// we just wrap it in a try-catch block.
|
||||
// XXX The value of options or any of the properties descending from it
|
||||
// may be undefined at some point in the execution (e.g. on start).
|
||||
// Instead of multiple checks for the undefined value, we just wrap it
|
||||
// in a try-catch block.
|
||||
}
|
||||
|
||||
return domain;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
|
||||
@@ -69,7 +69,7 @@ function _connectionEstablished(state: Object, action: Object) {
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _constructConnectionOptions(domain: string) {
|
||||
function _constructOptions(domain: string) {
|
||||
// FIXME The HTTPS scheme for the BOSH URL works with meet.jit.si on both
|
||||
// mobile & Web. It also works with beta.meet.jit.si on Web. Unfortunately,
|
||||
// it doesn't work with beta.meet.jit.si on mobile. Temporarily, use the
|
||||
@@ -96,7 +96,9 @@ function _constructConnectionOptions(domain: string) {
|
||||
bosh: `${String(boshProtocol)}//${domain}/http-bind`,
|
||||
hosts: {
|
||||
domain,
|
||||
focus: `focus.${domain}`,
|
||||
|
||||
// Required by:
|
||||
// - lib-jitsi-meet/modules/xmpp/xmpp.js
|
||||
muc: `conference.${domain}`
|
||||
}
|
||||
};
|
||||
@@ -114,9 +116,9 @@ function _constructConnectionOptions(domain: string) {
|
||||
function _setDomain(state: Object, action: Object) {
|
||||
return {
|
||||
...state,
|
||||
connectionOptions: {
|
||||
...state.connectionOptions,
|
||||
..._constructConnectionOptions(action.domain)
|
||||
options: {
|
||||
...state.options,
|
||||
..._constructOptions(action.domain)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
29
react/features/base/i18n/configLanguageDetector.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* @flow */
|
||||
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Custom language detection, just returns the config property if any.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Does not support caching.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
cacheUserLanguage: Function.prototype,
|
||||
|
||||
/**
|
||||
* Looks the language up in the config.
|
||||
*
|
||||
* @returns {string} The default language if any.
|
||||
*/
|
||||
lookup() {
|
||||
return config.defaultLanguage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Name of the language detector.
|
||||
*/
|
||||
name: 'configLanguageDetector'
|
||||
};
|
||||
36
react/features/base/i18n/constants.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* The available/supported languages.
|
||||
*
|
||||
* XXX The element at index zero is the default language.
|
||||
*
|
||||
* @public
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
export const LANGUAGES = [
|
||||
'en', // XXX The default language.
|
||||
|
||||
'bg',
|
||||
'de',
|
||||
'es',
|
||||
'fr',
|
||||
'hy',
|
||||
'it',
|
||||
'oc',
|
||||
'pl',
|
||||
'ptBR',
|
||||
'ru',
|
||||
'sk',
|
||||
'sl',
|
||||
'sv',
|
||||
'tr'
|
||||
];
|
||||
|
||||
/**
|
||||
* The default language.
|
||||
*
|
||||
* XXX The element at index zero of {@link LANGUAGES} is the default language.
|
||||
*
|
||||
* @public
|
||||
* @type {string} The default language.
|
||||
*/
|
||||
export const DEFAULT_LANGUAGE = LANGUAGES[0];
|
||||