Compare commits

...

118 Commits
1663 ... 1757

Author SHA1 Message Date
Lyubo Marinov
e6f906b9ca [RN] Fix undefined.avatarID in ParticipantView 2017-03-08 17:05:55 -06:00
hristoterezov
d74e43ddcc [RN] fix(Avatar): Match the implementation for web 2017-03-08 13:21:34 -06:00
Lyubo Marinov
23ddce122b Comply w/ coding style 2017-03-07 21:54:37 -06:00
hristoterezov
814bd26c07 feat(Avatar): Implement Avatar for web 2017-03-07 21:54:37 -06:00
damencho
2e4b39c19c Fixes loading jquery-i18next. 2017-03-07 17:30:20 -06:00
Дамян Минков
3ee65748bb Merge pull request #1382 from jitsi/load_error_handler_placeholder
Post load error handler
2017-03-07 17:29:30 -06:00
paweldomas
4fa800b87a feat(index.html): post load error handler
Adds a placeholder which allows to write a plugin for executing some
code after the "load error handler" is triggered. A function named
"postLoadErrorHandler" should be defined in one of
the "#include virtual" files.
2017-03-07 16:29:50 -06:00
Emil Ivov
9338b3cf94 Adds links to mobile builds 2017-03-07 15:44:10 -06:00
Lyubo Marinov
45e09af692 react-native 0.42.0 2017-03-07 15:09:39 -06:00
yanas
9d32f48ab8 [IOS Assets] Fixes launch screen 480x3 image 2017-03-07 13:12:26 -06:00
Lyubo Marinov
290e7baead Stick to the exact package versions for the direct dependencies 2017-03-02 21:46:43 -06:00
Любомир Маринов
e780ae00d0 Merge pull request #1360 from jitsi/move_avatar
ref(avatar): Move Avatar and ParticipantView to base/participants
2017-03-02 21:17:41 -06:00
hristoterezov
9ea224412d ref(avatar): Move Avatar and Participant view to base/participants 2017-03-02 16:57:43 -06:00
ibauersachs
cd8ae07698 Commit from translate.jitsi.org by user ibauersachs.: 317 of 317 strings translated (0 fuzzy). 2017-03-02 21:36:45 +00:00
Ingo Bauersachs
433a73e13d Add Norwegian Bokmal 2017-03-02 22:14:06 +01:00
jitsi-pootle
1e558f4da6 New files added from translate.jitsi.org based on templates 2017-03-02 21:21:41 +00:00
Lyubo Marinov
aef6e33c91 [RN] Fix remote JS debugging 2017-03-01 21:33:49 -06:00
Lyubo Marinov
acd83ede2f [RN] Third-party ES6 Symbol ponyfill 2017-03-01 21:31:43 -06:00
Lyubo Marinov
bd51613e62 [RN] Support the for...of statement in lib-jitsi-meet 2017-03-01 21:30:21 -06:00
ibauersachs
246cb39003 Commit from translate.jitsi.org by user ibauersachs.: 306 of 306 strings translated (0 fuzzy). 2017-03-01 21:07:11 +00:00
Lyubo Marinov
3b54c527b6 Remove obsolete source code 2017-02-28 23:22:03 -06:00
Lyubo Marinov
18368fefaa Comply w/ coding style 2017-02-28 23:22:02 -06:00
damencho
c361e1e31a Translate react strings.
Split language detectors to be web/native dependent. Take care of strings that contain html.
2017-02-28 13:16:42 -06:00
damencho
e3d4152e32 Adds react-i18next and its provider to react.
Adds translate function with default namespaces and options.
2017-02-28 13:13:47 -06:00
damencho
d861ba1876 Moves translation to react and use i18next language detectors. 2017-02-28 13:13:47 -06:00
George Politis
c942017b73 Merge pull request #1341 from saghul/doc-disableRtx
config: switch default disableRtx to false
2017-02-28 08:52:33 -06:00
Lyubo Marinov
743d12e326 Default to mobile app promotion 2017-02-28 00:18:52 -06:00
Lyubo Marinov
d371a3d5fd Fix TypeError: undefined is not an object 2017-02-28 00:18:52 -06:00
Lyubo Marinov
e1056126e6 Fix TypeError: undefined is not an object 2017-02-28 00:18:51 -06:00
Lyubo Marinov
72c267fbf3 Fix the human-readable text 2017-02-28 00:18:51 -06:00
Lyubo Marinov
0ed85b9d25 Replace features/unsupported-browser SET_UNSUPPORTED_BROWSER with features/base/lib-jitsi-meet SET_WEBRTC_READY
The error raised by JitsiMeetJS.init() is already in the state of
features/base/lib-jitsi-meet so it's not a good design to store the same
error in the state of features/unsupported-browser.
2017-02-28 00:18:51 -06:00
Ilya Daynatovich
a8877d82b6 Rename style component; Use of status codes instead of flags in conference init 2017-02-28 00:18:51 -06:00
Ilya Daynatovich
8896b0adf3 Fix problem with dialogs 2017-02-28 00:18:50 -06:00
Ilya Daynatovich
60b14e9b45 Some fixes mentioned in the PR 2017-02-28 00:18:50 -06:00
Ilya Daynatovich
631e853b40 Update register/unregister listeners logic of some components in the old app 2017-02-28 00:18:50 -06:00
Ilya Daynatovich
b409c8cc2f Fix reload regression 2017-02-27 21:50:21 -06:00
Ilya Daynatovich
905212b109 Enable flow for written code 2017-02-27 21:50:20 -06:00
Ilya Daynatovich
05b7df26e6 Add no mobile app component 2017-02-27 21:50:20 -06:00
Ilya Daynatovich
1268afd3f8 Added unsuported browser and plugin required pages 2017-02-27 21:50:20 -06:00
Lyubo Marinov
c1b9b7a623 Prevent undefined JitsiMeetJS 2017-02-27 21:35:34 -06:00
Lyubo Marinov
0b9160fb75 LIB_DID_DISPOSE, LIB_DID_INIT, LIB_WILL_DISPOSE, LIB_WILL_INIT 2017-02-27 16:45:53 -06:00
George Politis
93c9419392 Merge pull request #1362 from bgrozev/no-extension-on-ff-52
config: Don't require an extension for firefox >=52.
2017-02-27 16:36:31 -06:00
Boris Grozev
6121bcf171 config: Don't require an extension for firefox >=52.
Starting with firefox 52, no extension is required for screensharing.
See https://wiki.mozilla.org/Screensharing
2017-02-27 16:08:13 -06:00
Lyubo Marinov
702144180c Be consistent, simplify the source code 2017-02-25 19:00:35 -06:00
Lyubo Marinov
d2b2f98941 Fix typo 2017-02-25 18:58:23 -06:00
Lyubo Marinov
ec95956e25 [RN] Prepare for modifications to unsupported-browser 2017-02-24 13:08:49 -06:00
Lyubo Marinov
d6d7ce1b67 [RN] Move preferH264 where it will be in effect (in the future) 2017-02-24 12:59:30 -06:00
bgrozev
4cb36b0a5d Merge pull request #1337 from saghul/cleanup-adaptive-simulcast
cleanup: remove old adaptive simulcast config option
2017-02-24 11:43:22 -06:00
George Politis
2b3aea76a9 doc: Nukes influxdb.md because influx support has been nuked. 2017-02-23 17:22:06 -06:00
Lyubo Marinov
f50a31b4e8 [RN] Simplify the source code 2017-02-23 17:14:04 -06:00
Saúl Ibarra Corretgé
b226c3aca3 [RN] Fix loading config.js from URLs with a non-standard port
`host` contains the hostname:port portion, whereas `hostname` is just the
hostname, not including the port.
2017-02-23 17:14:04 -06:00
yanas
4979666a89 Merge pull request #1342 from jitsi/filmstriponly_transparent
fix(filmstriponly): Set the background to transparent
2017-02-23 16:11:35 -06:00
yanas
c9636f85b9 Merge pull request #1314 from virtuacoplenny/lenny/audio-slider
Volume slider for remote participant audio elements
2017-02-23 15:48:52 -06:00
hristoterezov
436bc87a86 fix(overlay): comments after review 2017-02-23 13:50:09 -06:00
Lyubo Marinov
e89c2b242d Android plugin for Gradle 2.2.3
Recent versions of the Android plugin for Gradle started to automatically
download the SDK build tools dependency if it is not installed already.
So it is no longer necessary to have the developer of the Android app
install the SDK build tools dependency in advance.
2017-02-23 13:14:19 -06:00
Leonard Kim
02b26a65bb Volume slider for remote participant audio elements 2017-02-23 09:01:40 -08:00
Дамян Минков
6a3e4bb59f Merge pull request #1311 from saghul/resources
Move miscellaneous files to resources
2017-02-23 06:42:15 -08:00
Saúl Ibarra Corretgé
b01ad360da Move miscellaneous files to resources 2017-02-23 10:01:19 +01:00
Ilya Daynatovich
c7f3740099 Fix IE redirect problem 2017-02-22 23:36:06 -06:00
Дамян Минков
554595acd7 Merge pull request #1348 from jitsi/restyle-range-inputs
Re-styles range inputs for Chrome and FF.
2017-02-22 21:44:46 -06:00
yanas
ee4ddd5446 Fixes indentation 2017-02-22 17:14:09 -06:00
yanas
ebab617a12 Re-styles range inputs for Chrome and FF. 2017-02-22 16:49:56 -06:00
Lyubo Marinov
bc5d92a452 [RN] Prefer H.264 2017-02-22 08:28:19 -06:00
Saúl Ibarra Corretgé
2f388dfb6a Fix warning about missing key prop
When rendering using a for loop each child whould have a key prop.
2017-02-22 12:08:12 +01:00
Boris Grozev
73a0197eb2 doc: Adds a note on installing ios-deploy on MacOS 10.11. 2017-02-21 14:43:59 -06:00
hristoterezov
b6990e9e5d fix(filmstriponly): Set the background to transparent 2017-02-21 13:45:46 -06:00
Lyubo Marinov
26e119bfc2 Comply w/ coding style 2017-02-21 13:33:25 -06:00
Saúl Ibarra Corretgé
9f866ae608 config: switch default disableRtx to false 2017-02-21 20:02:55 +01:00
Saúl Ibarra Corretgé
023359b9d2 [RN] Avoid rendering Container if not visible
This solves the issue of view clipping on Android, plus it seems to be the RN
convention unless the views are very large and memory hungry.
2017-02-21 11:13:36 -06:00
bgrozev
2128d047e1 Merge pull request #1339 from saghul/fix-doc-links
doc: fix markdown link syntax in mobile docs
2017-02-21 10:49:27 -06:00
Saúl Ibarra Corretgé
a89349c5b9 doc: fix markdown link syntax in mobile docs 2017-02-21 17:37:56 +01:00
Lyubo Marinov
d109b8beb6 Comply w/ coding style 2017-02-21 09:39:59 -06:00
Saúl Ibarra Corretgé
9b40572921 [RN] Fix Android immersive mode when coming from the background
Fixes an issue where immersive mode would be enabled when coming back from the
background on the welcome screen.

Re-fixes c57e713, which was not correct.
2017-02-21 09:13:30 -06:00
Lyubo Marinov
aaf7a38cce Detail comment 2017-02-21 09:09:07 -06:00
Saúl Ibarra Corretgé
1ed0759a50 [RN] Temporarily disable camera toggling button
It doesn't work properly and gives a very bad user experience. Disble it until
all underlying issues in react-native-webrtc are ironed out.
2017-02-21 08:33:31 -06:00
Saúl Ibarra Corretgé
213b73da6e cleanup: remove old adaptive simulcast config option 2017-02-20 15:46:41 +01:00
Saúl Ibarra Corretgé
5b6985fc5c [RN] Fix use of undefined APP
On RN we don't use the global APP object, so don't save the store there unless
it's defined, which is the case in the current web version. Also, check for
undefined explicitly, since a "if (!APP)" check will throw a ReferenceError.
2017-02-20 11:16:01 +01:00
Lyubomir Marinov
538af01bf5 Comply w/ coding style 2017-02-18 21:57:38 -06:00
hristoterezov
92d0589a37 ref(overlay): The overlays to use React 2017-02-18 17:03:50 -06:00
Lyubomir Marinov
f3269070b2 [iOS] iPad support 2017-02-18 11:36:14 -06:00
Lyubomir Marinov
d93bd3eda7 [RN] Use a default host when only a room name is specified
The mobile app remembers the domain which hosted the last conference. If
the user specified a full URL first and specified a room name only the
second time, it was not obvious that the second conference would be
hosted on the domain of the first conference.
2017-02-18 10:04:08 -06:00
Yana Stamcheva
0dbbc5d8b6 [Android] Circular app/launcher icon(s) 2017-02-17 13:54:01 -06:00
George Politis
08efd5ecab Merge pull request #1327 from saghul/doc-resolution
doc: document resolution config option and set it to 720 by default
2017-02-17 11:33:04 -06:00
Lyubomir Marinov
dba1bcb0e3 [RN] Increment short app version from 1.2 to 1.3
Now that Apple have approved build 1.2.199 for release in the App Store,
the short app version needs to be incremented; otherwise, no new builds
can be uploaded to TestFlight and, respectively, for release in the App
Store.
2017-02-17 09:32:24 -06:00
yanas
348403abff Merge pull request #1326 from jitsi/fix-manual-tooltips
Fixes manual triggered tooltips text and no hover.
2017-02-17 09:26:58 -06:00
Paweł Domas
c290cf45b7 Merge pull request #1328 from saghul/cleanup-adaptive-lastn
cleanup: remove adaptive las N config option
2017-02-17 09:14:47 -06:00
Saúl Ibarra Corretgé
175c8e6e50 cleanup: remove adaptive las N config option
The feature has been replaced so the option no longer applies.
2017-02-17 15:33:20 +01:00
Saúl Ibarra Corretgé
f90667b23c doc: document resolution config option and set it to 720 by default 2017-02-17 13:53:41 +01:00
damencho
cf69d591e4 Fixes manual triggered tooltips text and no hover. 2017-02-17 00:14:58 +02:00
Lyubomir Marinov
e599491583 Remove duplication 2017-02-16 15:17:05 -06:00
Lyubomir Marinov
d1520773cf Improve consistency 2017-02-16 15:16:17 -06:00
Saúl Ibarra Corretgé
573ca97b6c [RN] Add workaround for Android view clipping
Looks like Android gets confused as to what surface to blit when we hide or
show toolbars. Setting a border on the container, seems to force the entire
area to blit properly.

Other attempted approaches, with no success:
- zIndex of -100
- width and height of 0
- opacity of 0 and setting 'disabled' on touch containers

This patch applies the workaround in the welcome page and conference containers.
2017-02-16 14:37:42 -06:00
Lyubomir Marinov
0d97f14a1a flow 2017-02-16 13:59:28 -06:00
Lyubomir Marinov
b8f28abfdf [RN] Fix incorrect JitsiMeetJS.init error handling 2017-02-16 13:59:12 -06:00
Lyubomir Marinov
9ac7c97e67 [RN] Enforce mandatory mobile app-specific config 2017-02-16 13:58:39 -06:00
Lyubomir Marinov
52b3eaacb5 [RN] Fix passing config.js to JitsiMeetJS.init 2017-02-16 13:51:01 -06:00
ibauersachs
9b01ae6db9 Commit from translate.jitsi.org by user ibauersachs.: 306 of 306 strings translated (0 fuzzy). 2017-02-16 06:55:14 +00:00
Ingo Bauersachs
469487ad36 Add Chinese (China) 2017-02-16 07:39:57 +01:00
jitsi-pootle
176c3c1601 New files added from translate.jitsi.org based on templates 2017-02-16 06:46:38 +00:00
yanas
94391234cc Merge pull request #1322 from jitsi/livestream_help_link
livestream link help link
2017-02-15 22:35:57 -06:00
yanas
d84901f196 Font size adjustment and moving link to config param 2017-02-15 17:57:57 -06:00
yanas
c6b117565d Merge branch 'livestream_help_link' of https://github.com/jitsi/jitsi-meet into livestream_help_link 2017-02-15 16:41:49 -06:00
yanas
2a9124acb5 Merge pull request #1316 from jitsi/custom-button-tooltips
Uses tooltip to show custom popups for mute mic button.
2017-02-15 16:20:36 -06:00
Lyubomir Marinov
401a783d6a Coding style consistency
Includes automatic recommended Xcode project file fixes.
2017-02-15 13:48:56 -06:00
Lyubomir Marinov
39483a30b6 Polyfill Element.innerHTML
Lib-jitsi-meet uses jQuery's .append method to manipulate Jingle. The
method in question invokes the getter and setter of Element.innerHTML.
Unfortunately, xmldom which we use in React Native to polyfill DOM does
not polyfill Element.innerHTML. So polyfill it ourselves.
2017-02-15 13:18:21 -06:00
Lyubomir Marinov
0e2a07f8d7 Stick to react-native-background-timer 1.0.0
Recently expose-loader broke us when it updated from 0.7.1 to 0.7.2 and
we decided to stick to exact versions.
2017-02-15 13:18:20 -06:00
Saúl Ibarra Corretgé
36f5b0218d [RN] Fix running timers in the background
Turns out React Native's timers (setTimeout / setInterval) don't run while the
app is in the background: https://github.com/facebook/react-native/issues/167

This patch replaces the global timer functions with those from the
react-native-background-timer package, which work in the background.

These timers won't magically make an application work in the background, but
they will run if an application already happens to run in the background. That's
our case while in a conference, so these timers will run, allowing XMPP pings to
be sent and the conference to stay up as long as the user desires.
2017-02-15 11:50:54 -06:00
damencho
a1b3c56de7 Uses tooltip to show custom popups for mute mic button/shared video button. 2017-02-15 11:29:26 +02:00
Aaron van Meerten
6d664f133e Cosmetic fixes for column length lint errors 2017-02-14 17:11:31 -06:00
Aaron van Meerten
732a433ec1 livestream link to provide more context to users on where to retrieve their stream key 2017-02-14 16:07:12 -06:00
Lyubomir Marinov
f7dcd1ba2c Stick to expose-loader 0.7.1
0.7.2 causes a ReferenceError: jQuery is not defined in autosize.
2017-02-14 08:07:49 -06:00
Lyubomir Marinov
55a8b44224 Consistent middleware and reducer imports 2017-02-10 11:04:40 -06:00
Lyubomir Marinov
e29db31d91 Comply w/ coding style 2017-02-10 10:13:39 -06:00
Lyubomir Marinov
183d3c3ca4 Fix a possible undefined state usage 2017-02-10 00:47:55 -06:00
Saúl Ibarra Corretgé
c57e713696 [RN] Fix full-screen mode when coming back from the background
On Android the status and navigation bars are shown again after coming back from
the background, so enter full-screen mode again if needed.
2017-02-10 00:44:37 -06:00
Saúl Ibarra Corretgé
4519f26adf [RN] Mute local video when app is in the background 2017-02-10 00:44:37 -06:00
bgrozev
c26f9cc01f Merge pull request #1301 from jitsi/video-thumbnail-margin
Lower the margin between video thumbnails
2017-02-09 11:43:55 -06:00
yanas
f6f730b994 Lower the margin between video thumbnails 2017-02-06 15:34:05 -06:00
190 changed files with 5235 additions and 2201 deletions

View File

@@ -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
View File

@@ -53,7 +53,6 @@ yarn-error.log
#
buck-out/
\.buckd/
android/app/libs
*.keystore
# fastlane

View File

@@ -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

View File

@@ -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).

View File

@@ -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')

View File

@@ -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" />

View File

@@ -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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -1,10 +1,3 @@
html, body {
width: 100%;
height:100%;
color: $defaultColor;
background: $defaultBackground;
}
.redirectPageMessage {
width: 30%;
margin: 20% auto;

View File

@@ -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;

View 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;
}

View File

@@ -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;
}

View File

@@ -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 */

View File

@@ -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;
}
}

View File

@@ -8,6 +8,8 @@ $baseLight: #FFFFFF;
*/
$controlBackground: $baseLight;
$controlColor: #333333;
$sliderTrackBackground: #474747;
$sliderThumbBackground: #3572b0;
/**
* Buttons

View File

@@ -0,0 +1,3 @@
@import 'no-mobile-app';
@import 'unsupported-desktop-browser';
@import 'unsupported-mobile-browser';

View 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;
}
}

View File

@@ -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%;
}
}

View File

@@ -1 +1 @@
prosody-plugins/ /usr/share/jitsi-meet/
resources/prosody-plugins/ /usr/share/jitsi-meet/

View File

@@ -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.

View File

@@ -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
View 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>>;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 611 B

View File

@@ -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 */);

View File

@@ -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
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -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/>

View File

@@ -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",
);

View File

@@ -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
View File

@@ -0,0 +1,18 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"zhCN": ""
}

View File

@@ -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
View File

@@ -0,0 +1,17 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": ""
}

View File

@@ -13,5 +13,7 @@
"sk": "Slovak",
"sl": "Slovenian",
"sv": "Swedish",
"tr": "Turkish"
"tr": "Turkish",
"zhCN": "Chinese (China)",
"nb": "Norwegian Bokmal"
}

View File

@@ -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": "&nbsp;",
@@ -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
View 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": ""
}
}

View File

@@ -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": "&nbsp;",
"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
View 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": ""
}
}

View File

@@ -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": "&nbsp;",
@@ -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."
}

View File

@@ -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;

View File

@@ -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
});
}
};

View File

@@ -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();
}
};

View File

@@ -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();
}
}

View File

@@ -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},

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
};

View File

@@ -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, () => {

View File

@@ -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();
}
};

View File

@@ -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.
*

View File

@@ -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;
}
};
/**

View File

@@ -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 }

View File

@@ -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 () {

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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": {

View File

@@ -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));
}
};

View File

@@ -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;

View File

@@ -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';

View File

@@ -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

View File

@@ -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;

View File

@@ -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();
}
/**

View File

@@ -2,3 +2,5 @@ export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './functions';
import './reducer';

View 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');

View 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));
};
}

View File

@@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './middleware';
import './reducer';

View 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));
}

View 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;
});

View File

@@ -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);
};
}

View File

@@ -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}
*/

View File

@@ -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);

View File

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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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
};
}

View File

@@ -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));
}
});
};
}

View File

@@ -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;

View File

@@ -1,3 +1,5 @@
export * from './actions';
export * from './actionTypes';
export * from './functions';
import './reducer';

View File

@@ -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)
}
};
}

View 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'
};

View 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];

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