Compare commits

...

114 Commits
1622 ... 1663

Author SHA1 Message Date
yanas
53e784094a Merge pull request #1308 from jitsi/ss_resize_remote
Fix for the size of remote desktop sharing videos
2017-02-08 15:31:06 -06:00
hristoterezov
0e92e48376 fix(ss): resize for remote videos 2017-02-08 14:58:42 -06:00
Lyubomir Marinov
4c9943ac38 Fix an image path on the mobile landing page 2017-02-08 12:41:51 -06:00
Дамян Минков
4bd0fd145d Merge pull request #1293 from jitsi/prosody_plugin_muc_all_owners
prosody plugin to make all users owners/moderators
2017-02-08 13:17:42 +02:00
Lyubomir Marinov
01ae82eb28 No Temasys alert on mobile Web 2017-02-07 21:54:08 -06:00
Lyubomir Marinov
e21eae0933 Prepare for webpack 2 2017-02-07 15:44:37 -06:00
Lyubomir Marinov
2f047c50dc Revert "No Temasys alert on mobile Web"
This reverts commit b09e86352f.
2017-02-07 15:21:34 -06:00
yanas
e397e1a80c Merge pull request #1303 from jitsi/no-temasys-alert-on-mobile
No Temasys alert on mobile (Web)
2017-02-07 15:11:09 -06:00
Lyubomir Marinov
b09e86352f No Temasys alert on mobile Web 2017-02-07 15:08:38 -06:00
Lyubomir Marinov
8687b69167 Consistency
Be consistent about formatting within 1 and the same file.
2017-02-07 08:29:40 -06:00
Lyubomir Marinov
6c5468d904 Simplify the source code
If half the file is written in ES6, it is easier to read if the rest of
the file is in ES6 as well. If ES6 is used, then const is better than
let. If source code is shorter yet as readable as the long version, then
prefer the short version.
2017-02-07 08:29:40 -06:00
Lyubomir Marinov
d6b0f8d4c5 Use functions, do not re-implement them
We have the functions reload and redirect which modify window.location.
Use them and do not directly modify window.location so that we have
fewer places of direct window.location modifications and it is easier to
refactor them.
2017-02-07 08:29:40 -06:00
Lyubomir Marinov
a8cd4ff12c 1, not 2 names for 1 and the same abstraction
window.location calls it reload so util/helpers shouldn't call it
redirect because UI/util/UIUtil has it is own redirect which is the
assign of window.location.
2017-02-07 08:29:40 -06:00
Lyubomir Marinov
8509efc8af Make the Web app aware of its context root 2017-02-07 08:27:23 -06:00
Saúl Ibarra Corretgé
23a0053dad doc: add section about embedding to the README 2017-02-06 17:41:19 -06:00
Saúl Ibarra Corretgé
5849980092 external_api: fix jsdoc 2017-02-06 17:41:19 -06:00
Saúl Ibarra Corretgé
e81fc2b254 doc: fix external API documentation
- use proper punctuation
- fix markdown syntax
- always use syntax highlighting
- document missing commands
- miscellaneous grammar fixes
2017-02-06 17:41:19 -06:00
Lyubomir Marinov
2ad869a036 Comply w/ coding style
- Use 1 name for 1 abstraction. Instead of useFullScreen and enabled use
  fullScreen.
- Comments are correct English sentences so no double spaces between
  senteces, no capitalization of the work On midsentence.
- Write as little source code as possible if readability is preserved.
- Utilize Facebook's Flow.
- The name of a private function must start with _ and the jsdoc should
  state that the function is private.
2017-02-06 15:32:03 -06:00
Saúl Ibarra Corretgé
7a8c84e990 [RN] Implement full screen mode while in a conference
The implementation varies across platforms, with the same goal: allow the app to
use the entire screen real state while in a conference.

On Android we use immersive mode, which  will hide the status and navigation bars.

https://developer.android.com/training/system-ui/immersive.html

On iOS the status bar is hidden, with a slide effect.
2017-02-06 13:51:17 -06:00
Lyubomir Marinov
0de01e93dd react-native 0.41.2 2017-02-06 12:24:38 -06:00
Lyubomir Marinov
6fa93e5b44 file-loader 0.10.0 2017-02-06 12:24:38 -06:00
Lyubomir Marinov
2144ec1e3f eslint 3.15.0 2017-02-06 12:24:38 -06:00
bgrozev
68d2f60ace Merge pull request #1276 from jitsi/respect-disable-rtx
fix: Respect the disableRtx config option.
2017-02-06 11:52:03 -06:00
George Politis
3d671ae71f docs: Documents the disableRtx config option. 2017-02-06 11:19:33 -06:00
Lyubomir Marinov
8ed47f9d99 [flow] Lint with Flow (in addition to JSHint and ESLint) 2017-02-03 16:36:14 -06:00
Lyubomir Marinov
b50f858556 [flow] Expand the coverage of flow-monitored files 2017-02-03 16:36:14 -06:00
Lyubomir Marinov
5de1a74429 [flow] Take advantage of flow-typed 2017-02-03 16:36:14 -06:00
Lyubomir Marinov
2063ad467d flow-typed 2017-02-03 16:36:14 -06:00
Lyubomir Marinov
679acbae16 Use babel-eslint in the whole project 2017-02-03 16:36:14 -06:00
Aaron van Meerten
a5b706a99e Added a prosody plugin for making all users into muc owners in prosody
Included a patch to prosody-trunk which allows owners to kick each other
2017-02-03 11:41:08 -06:00
yanas
542e61357e Adds new combined camera and mic icon 2017-02-02 17:10:02 -06:00
Lyubomir Marinov
3743602c67 [RN] Fix the parsing of the domain out of a URL 2017-02-02 13:40:49 -06:00
Lyubomir Marinov
ee651840bf Fixes related to coding style 2017-02-02 10:54:24 -06:00
damencho
0765c60d77 Moves feedback button as a component.
When callstats is not configured hide the button.
2017-02-02 10:06:17 -06:00
Lyubomir Marinov
7fa17322a1 Consistency in naming and jsdocs 2017-02-02 09:46:09 -06:00
Lyubomir Marinov
cfa3047330 [flow] Type annotations 2017-02-02 09:45:34 -06:00
Lyubomir Marinov
9e033deb7b Remove unnecessary source code 2017-02-02 09:42:14 -06:00
Lyubomir Marinov
f6c914f6f0 [flow] A minimal demonstration of flow in action 2017-02-01 13:38:37 -06:00
Lyubomir Marinov
06ff02c2a5 [flow] Ignore packages in node_modules that cause errors and we do not want to fix 2017-02-01 13:38:37 -06:00
Lyubomir Marinov
63fd263890 flow 0.36.0
The first step towards enabling Facbook's flow in the project. The flow
configuration is pristine as generated by react-native upgrade.
2017-02-01 13:38:37 -06:00
Lyubomir Marinov
94f3d4b279 [RN] Expand domains supported by Universal Links 2017-01-31 22:47:47 -06:00
Lyubomir Marinov
fdc96044ad [RN] App-specific URL scheme 2017-01-31 22:47:47 -06:00
Lyubomir Marinov
91487ffc94 Fix a case of endless recursion 2017-01-31 22:47:47 -06:00
Saúl Ibarra Corretgé
7a57dcc08a doc: add documentation on how to build the mobile apps 2017-01-31 22:32:15 -06:00
Дамян Минков
913a54713d Adds testing repo to readme. 2017-01-31 21:42:45 -06:00
damencho
39a8681e8e Improves safe checks for missing stats. 2017-01-30 17:01:25 -06:00
George Politis
a7015b0d1a fix: Respect the disableRtx config option. 2017-01-30 16:20:23 +00:00
Lyubomir Marinov
5305f23332 Consistent naming of functions
Until we make a decision on access modifier hints and adopt a respective
coding style, consistency is king.
2017-01-28 20:15:10 -06:00
Lyubomir Marinov
acbf3adab7 Simplify: Remove react/features/base/react-native 2017-01-28 20:13:54 -06:00
Lyubomir Marinov
366b2f1374 Simplify: Remove react/features/base/navigator 2017-01-28 19:56:35 -06:00
Lyubomir Marinov
2189ab7ee6 Consistent naming of react-redux's mapStateToProps
Until we make a decision on access modifier hints and adopt a respective
coding style, consistency is king.
2017-01-28 17:34:57 -06:00
Lyubomir Marinov
349c04d8d1 Consistent naming of functions
Until we make a decision on access modifier hints and adopt a respective
coding style, consistency is king.
2017-01-28 17:28:13 -06:00
Lyubomir Marinov
c7c6249ad7 [RN] Fix room name case sensitivity
It turns out that it is not enough to give lib-jitsi-meet the room name
in lower case. BOSH also needs the room name in lower case.
2017-01-28 17:26:09 -06:00
Lyubomir Marinov
5319227a8f Fix jsdocs 2017-01-28 12:11:24 -06:00
Lyubomir Marinov
3aff812ee2 Consistent naming of Component props mapped from the Redux state
Until we make a decision on access modifier hints and adopt a respective
coding style, consistency is king.
2017-01-27 21:36:20 -06:00
Lyubomir Marinov
88eabf23f4 Remove obsolete UnsupportedMobileBrowser functionality
The desired behavior of the button 'Start a conference' / 'Join the
conversation' is to launch the mobile app if installed; otherwise, do
nothing i.e. continue to display UnsupportedMobileBrowser.

Anyway, we may change our minds about allowing the user to continue in a
supported mobile browser so preserve the source code that enables that
but give it more appropriate naming.
2017-01-27 21:29:09 -06:00
Любомир Маринов
a70beaf7b6 Merge pull request #1265 from jitsi/api
api/api.html -> examples/api.html && rm doc/api
2017-01-27 20:11:23 -06:00
Lyubomir Marinov
c91bffa73c Merge branch 'saghul-android-audiomode' 2017-01-26 23:48:42 -06:00
Lyubomir Marinov
ea163dbbba Merge branch 'deduplicate-index-js' 2017-01-26 23:48:09 -06:00
Lyubomir Marinov
18bc99d6b5 Split long methods into multiple shorter ones 2017-01-26 23:35:56 -06:00
Lyubomir Marinov
bab94a207d Remove unnecessary source code 2017-01-26 21:08:50 -06:00
Lyubomir Marinov
ef39baab47 Comply w/ coding style
- Maximum of 80 characters per line.
- Group first and then sort in alphabetical order.
- Fields should begin with a lowercase letter.
2017-01-26 21:07:35 -06:00
Saúl Ibarra Corretgé
2edaaac7bf [RN] Implement AudioMode module on Android
This module chooses the most appropriate audio default based on the specified
mode.
2017-01-26 19:18:10 -06:00
Saúl Ibarra Corretgé
113e50c074 [RN] Bump Andoroid minimum and target SDK versions
Use a minimum SDK version of 19, that is Anroid 4.4 (KitKat) and a target SDK of
23, that is, Android 6.0 (Marshmallow).
2017-01-26 19:18:10 -06:00
Дамян Минков
900a675864 Merge pull request #1269 from jitsi/border-radius-fix
Fixes border radius.
2017-01-26 17:55:01 -06:00
Lyubomir Marinov
49b3b49f3e Remove duplication
The files react/index.native.js and react/index.web.js ended up having
very similar source code related to initializing the Redux store. Remove
the duplication.

Additionally, I always wanted the App React Component to be consumed
without the need to provide a Redux store to it.
2017-01-26 17:24:11 -06:00
yanas
23935d3d39 Fixes border radius. 2017-01-26 17:04:29 -06:00
Дамян Минков
3fd33d0f50 Merge pull request #1262 from jitsi/ui-fixes
Ui fixes
2017-01-26 10:15:39 -06:00
hristoterezov
8c3317b8e9 doc(iframe_api): api/api.html -> examples/api.html && rm doc/api 2017-01-26 10:02:34 -06:00
Lyubomir Marinov
99a9fc054f eslint 3.14.1 2017-01-26 09:39:47 -06:00
Lyubomir Marinov
967dcfc3d2 react-native-keep-awake 2.0.2 2017-01-26 09:39:20 -06:00
Lyubomir Marinov
0051b3b79c Clean up obsolete file references 2017-01-26 07:58:56 -06:00
Lyubomir Marinov
cbcee201f0 Comply w/ coding style 2017-01-26 07:58:46 -06:00
Ilya Daynatovich
1fa4a53a48 Remove rule 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
6a0b92638c Introduce interceptComponent function 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
2e81b8493e Introduce unsupported browser page 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
57ba702dda Clean up routing logic 2017-01-26 07:27:31 -06:00
Lyubomir Marinov
62bafcaf63 Introduce Platform in React
React Native provides a Platform abstraction which React does not
provide.
2017-01-26 07:27:31 -06:00
Lyubomir Marinov
7de5c9c1d2 Comply w/ coding style 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
8248b14555 Integrate Mobile landing in the Redux app 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
23ef0c8d9d justify text on landing page 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
58a4f59fd8 Implement Landing component 2017-01-26 07:27:31 -06:00
Ilya Daynatovich
0c851934fb layout for mobile landing 2017-01-26 07:27:31 -06:00
Emil Ivov
8d58dbee38 Restores api.md to original location 2017-01-25 19:14:30 -06:00
yanas
cd5e84e4ef Fixes network problem message position 2017-01-25 16:54:51 -06:00
yanas
09ba14eb04 Fixes video thumbnail border and large video background color 2017-01-25 16:53:58 -06:00
Boris Grozev
b22f1965aa doc: Remove obsolete reference to browserify. 2017-01-25 15:48:32 -06:00
Samuel Liard
df6f920b44 Force jquery-ui version to fix pb with beta version we can get with npm
Use Maven in place of ant
Add --host parameter for jicofo
Add build for jitsi Meet

Revert "Force jquery-ui version to fix pb with beta version we can get with npm"

This reverts commit 35a8641e62.
2017-01-25 15:47:23 -06:00
Дамян Минков
f13b2462c8 Merge pull request #1257 from saghul/ds-options-cleanup
cleanup: Remove default Chrome extension ID
2017-01-23 17:04:59 -06:00
Paweł Domas
c0e80c14f8 Merge pull request #1192 from jitsi/remotecontrol
Implement remote control support
2017-01-23 17:00:11 -06:00
hristoterezov
2b1176df53 style(remotecontrol): getRequestedParticipant method comments 2017-01-23 16:06:51 -06:00
hristoterezov
05bfbf5620 fix(remotecontrol): Execute Reciever.enable only when the value is changed 2017-01-23 15:29:25 -06:00
hristoterezov
4af706bd83 style(keycode): Comment 2017-01-23 15:29:25 -06:00
hristoterezov
b62e4d5ee9 fix(remotecontrol): Log level 2017-01-23 15:29:25 -06:00
hristoterezov
bd98d661d3 ref(remotecontrol): Moves hangup logic to remote control module 2017-01-23 15:29:25 -06:00
hristoterezov
1f7c5529e9 fix(remotecontrol): Pin the controlled participant only on remote control permissions granted 2017-01-23 15:29:25 -06:00
hristoterezov
0453346cf4 ref(remotecontrol): Pass the largeVideoWrapper as parameter to remote control module 2017-01-23 15:29:25 -06:00
hristoterezov
15090243d0 fix(API): JS errors caused by remote control merge 2017-01-23 15:29:25 -06:00
hristoterezov
b22e3ee253 style(remotecontrol): Fix JSDoc for RemoteControlEvent 2017-01-23 15:29:25 -06:00
hristoterezov
e693554961 fix(remotecontrol): Logging 2017-01-23 15:29:25 -06:00
hristoterezov
0efca9a9a8 fix(remotecontrol): Fixing issues after peer review. 2017-01-23 15:29:25 -06:00
hristoterezov
5d22061c0a fix(remotecontrol): Handle on-stage participant changes 2017-01-23 15:29:25 -06:00
hristoterezov
84be7fd739 fix(remotecontrol): import of remote control 2017-01-23 15:29:25 -06:00
hristoterezov
5d269ad0aa fix(remotecontrol): Disable the keyboard shortcuts during remote control 2017-01-23 15:29:25 -06:00
hristoterezov
a4d5c41b3a feat(remotecontrol): UI for requesting permissions 2017-01-23 15:29:25 -06:00
hristoterezov
846fb9abb0 feat(remotecontrol): Implement requesting remote control permissions 2017-01-23 15:29:25 -06:00
hristoterezov
0f33e59e4d feat(remotecontrol): announce remotecontrol support 2017-01-23 15:29:25 -06:00
hristoterezov
896650d005 feat(remotecontrol): Implement basic remote control support 2017-01-23 15:29:25 -06:00
Saúl Ibarra Corretgé
6d6b5a7917 cleanup: Remove default Chrome extension ID
It makes for a bad first-time experience for users, since the desktop sharing
button will be visible, but it will never work.

Also get rid of the now deprecated `desktopSharingChromeMethod` option.
2017-01-23 15:21:28 -06:00
Любомир Маринов
db5010be9d Merge pull request #1261 from jitsi/clean-css-module-update
Stick to clean-css version 3.x.
2017-01-23 15:18:18 -06:00
damencho
be8860080c Fixes build.
Starting version 4.x clean-css is split into two packages and we should depend on clean-css-cli for versions 4 and above. Tested it and we have currently some problem with it like fonts and images got referenced under css folder. So sticking version to 3.x for now.
2017-01-23 14:35:26 -06:00
Lyubomir Marinov
42f124b0f6 Fix typo 2017-01-23 13:01:12 -06:00
Lyubomir Marinov
7249ababb7 Provide microphone and camera usage descriptions
iTunes Connect/TestFlight requires these now and is not satisfied with
empty values anymore.
2017-01-23 12:11:48 -06:00
Lyubomir Marinov
079be92468 Revert "[RN][iOS] Make universal builds"
This reverts commit b96ca538e4. While iPad
support is nice to have, we are not ready for a publish in App Store.
During the upload to iTunes Connect, a verification of the .ipa fails
because:

- app icons for iPad are missing:
  - 76x76 pixels in .png format for iOS versions >= 7.0,
  - 167x167 pixels in .png format for iOS versions supporting iPad Pro,
  - 152x152 pixels in .png format for iOS versions >= 7.0,
- orientations required for iPad Mutitasking are missing:
  - UIInterfaceOrientationPortraitUpsideDown.
2017-01-23 11:26:15 -06:00
143 changed files with 4632 additions and 1195 deletions

View File

@@ -3,6 +3,7 @@ build/*
# Third-party source code which we (1) do not want to modify or (2) try to
# modify as little as possible.
flow-typed/*
libs/*
# ESLint will by default ignore its own configuration file. However, there does

View File

@@ -4,7 +4,10 @@ module.exports = {
'commonjs': true,
'es6': true
},
'extends': 'eslint:recommended',
'extends': [
'eslint:recommended',
'plugin:flowtype/recommended'
],
'globals': {
// The globals that (1) are accessed but not defined within many of our
// files, (2) are certainly defined, and (3) we would like to use
@@ -12,12 +15,16 @@ module.exports = {
// files.
'__filename': false
},
'parser': 'babel-eslint',
'parserOptions': {
'ecmaFeatures': {
'experimentalObjectRestSpread': true
},
'sourceType': 'module'
},
'plugins': [
'flowtype'
],
'rules': {
'new-cap': [
'error',

50
.flowconfig Normal file
View File

@@ -0,0 +1,50 @@
[ignore]
; We fork some components by platform
.*/*[.]android.js
; Ignore "BUCK" generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
.*/Libraries/react-native/React.js
.*/Libraries/react-native/ReactNative.js
; Ignore packages in node_modules which we (i.e. the jitsi-meet project) have
; seen to cause errors and we have chosen not to fix.
.*/node_modules/babel-core/.*
.*/node_modules/bower/.*
.*/node_modules/jsonlint/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow
flow/
[options]
module.system=haste
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
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\\)*\\$FlowFixedInNextDeploy
unsafe.enable_getters_and_setters=true
[version]
^0.37.0

2
.gitignore vendored
View File

@@ -4,7 +4,6 @@ deploy-local.sh
libs/
all.css
*css.map
unsupported_browser.css
.remote-sync.json
.sync-config.cson
@@ -48,6 +47,7 @@ local.properties
#
node_modules/
npm-debug.log
yarn-error.log
# BUCK
#

View File

@@ -1,7 +1,12 @@
# The following do not need to be checked because they do not represent JS
# source code.
build/
debian/
libs/
node_modules/
# The following are checked by ESLint which supersedes JSHint.
flow-typed/
react/
analytics.js
webpack.config.babel.js

4
ConferenceEvents.js Normal file
View File

@@ -0,0 +1,4 @@
/**
* Notifies interested parties that hangup procedure will start.
*/
export const BEFORE_HANGUP = "conference.before_hangup";

View File

@@ -8,7 +8,6 @@ OUTPUT_DIR = .
STYLES_BUNDLE = css/all.bundle.css
STYLES_DESTINATION = css/all.css
STYLES_MAIN = css/main.scss
STYLES_UNSUPPORTED_BROWSER = css/unsupported_browser.scss
WEBPACK = ./node_modules/.bin/webpack
all: update-deps compile deploy clean
@@ -47,7 +46,6 @@ deploy-lib-jitsi-meet:
$(DEPLOY_DIR)
deploy-css:
$(NODE_SASS) css/unsupported_browser.scss css/unsupported_browser.css ; \
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
rm $(STYLES_BUNDLE)
@@ -59,6 +57,5 @@ source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html connection_optimization favicon.ico fonts images libs sounds LICENSE lang source_package/jitsi-meet && \
cp css/all.css source_package/jitsi-meet/css && \
cp css/unsupported_browser.css source_package/jitsi-meet/css && \
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
rm -rf source_package

View File

@@ -1,5 +1,5 @@
Jitsi Meet - Secure, Simple and Scalable Video Conferences
====
# Jitsi Meet - Secure, Simple and Scalable Video Conferences
Jitsi Meet is an open-source (Apache) WebRTC JavaScript application that uses [Jitsi Videobridge](https://jitsi.org/videobridge) to provide high quality, scalable video conferences. You can see [Jitsi Meet in action](http://youtu.be/7vFUVClsNh0) here at the session #482 of the VoIP Users Conference.
You can also try it out yourself at https://meet.jit.si .
@@ -16,6 +16,7 @@ For other systems, or if you wish to install all components manually, see the [d
You can download Debian/Ubuntu binaries:
* [stable](https://download.jitsi.org/stable/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianStableRepository))
* [testing](https://download.jitsi.org/testing/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianTestingRepository))
* [nightly](https://download.jitsi.org/unstable/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianNightlyRepository))
## Building the sources
@@ -34,7 +35,7 @@ To build the Jitsi Meet application, just type
make
```
## Working with the library sources(lib-jitsi-meet).
## Working with the library sources (lib-jitsi-meet)
By default the library is build from its git repository sources. The default dependency path in package.json is :
```json
@@ -81,6 +82,14 @@ npm unlink lib-jitsi-meet
npm install
```
## Embedding in external applications
Jitsi Meet provides a very flexible way of embedding it in external applications by using the [Jitsi Meet API](doc/api.md).
## Mobile app
Jitsi Meet is also available as a React Native application for Android and iOS.
Instructions on how to build it can be found [here](doc/mobile.md).
## Discuss
Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on Github.

View File

@@ -139,6 +139,7 @@ if (project.hasProperty('JITSI_SIGNING')
}
dependencies {
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-vector-icons')
compile project(':react-native-webrtc')

View File

@@ -3,9 +3,12 @@
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><!-- WebRTC -->
<!-- XXX: ACCESS_NETWORK_STATE is required by WebRTC. -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
@@ -13,8 +16,8 @@
<uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
android:minSdkVersion="19"
android:targetSdkVersion="23" />
<application
android:allowBackup="true"
@@ -35,11 +38,19 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:host="beta.hipchat.me" android:scheme="https" />
<data android:host="beta.meet.jit.si" android:scheme="https" />
<data android:host="chaos.hipchat.me" android:scheme="https" />
<data android:host="enso.me" android:scheme="https" />
<data android:host="hipchat.me" android:scheme="https" />
<data android:host="meet.jit.si" android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="org.jitsi.meet" />
</intent-filter>
</activity>
<activity
android:name="com.facebook.react.devsupport.DevSettingsActivity" />

View File

@@ -16,7 +16,7 @@ public class MainApplication extends Application implements ReactApplication {
* {@inheritDoc}
*/
@Override
protected boolean getUseDeveloperSupport() {
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@@ -29,7 +29,9 @@ 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.oney.WebRTCModule.WebRTCModulePackage()
new com.oney.WebRTCModule.WebRTCModulePackage(),
new com.rnimmersive.RNImmersivePackage(),
new org.jitsi.meet.audiomode.AudioModePackage()
);
}
};

View File

@@ -0,0 +1,322 @@
package org.jitsi.meet.audiomode;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import java.util.HashMap;
import java.util.Map;
/**
* Module implementing a simple API to select the appropriate audio device for a
* conference call.
*
* Audio calls should use <tt>AudioModeModule.AUDIO_CALL</tt>, which uses the
* builtin earpiece, wired headset or bluetooth headset. The builtin earpiece is
* the default audio device.
*
* Video calls should should use <tt>AudioModeModule.VIDEO_CALL</tt>, which uses
* the builtin speaker, earpiece, wired headset or bluetooth headset. The
* builtin speaker is the default audio device.
*
* Before a call has started and after it has ended the
* <tt>AudioModeModule.DEFAULT</tt> mode should be used.
*/
public class AudioModeModule extends ReactContextBaseJavaModule {
/**
* Constants representing the audio mode.
* - DEFAULT: Used before and after every call. It represents the default
* audio routing scheme.
* - AUDIO_CALL: Used for audio only calls. It will use the earpiece by
* default, unless a wired or Bluetooth headset is connected.
* - VIDEO_CALL: Used for video calls. It will use the speaker by default,
* unless a wired or Bluetooth headset is connected.
*/
private static final int DEFAULT = 0;
private static final int AUDIO_CALL = 1;
private static final int VIDEO_CALL = 2;
/**
*
*/
private static final String ACTION_HEADSET_PLUG
= (Build.VERSION.SDK_INT >= 21)
? AudioManager.ACTION_HEADSET_PLUG
: Intent.ACTION_HEADSET_PLUG;
/**
* React Native module name.
*/
private static final String MODULE_NAME = "AudioMode";
/**
* Tag used when logging messages.
*/
static final String TAG = MODULE_NAME;
/**
* {@link AudioManager} instance used to interact with the Android audio
* subsystem.
*/
private final AudioManager audioManager;
/**
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in
* old (< M) Android versions.
*/
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor;
/**
* {@link Handler} for running all operations on the main thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
/**
* {@link Runnable} for running update operation on the main thread.
*/
private final Runnable mainThreadRunner
= new Runnable() {
@Override
public void run() {
if (mode != -1) {
updateAudioRoute(mode);
}
}
};
/**
* Audio mode currently in use.
*/
private int mode = -1;
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the application.
*
* @param reactContext the {@link ReactApplicationContext} where this module
* is created.
*/
public AudioModeModule(ReactApplicationContext reactContext) {
super(reactContext);
audioManager
= (AudioManager)
reactContext.getSystemService(Context.AUDIO_SERVICE);
// Setup runtime device change detection.
setupAudioRouteChangeDetection();
}
/**
* Gets a mapping with the constants this module is exporting.
*
* @return a {@link Map} mapping the constants to be exported with their
* values.
*/
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
constants.put("AUDIO_CALL", AUDIO_CALL);
constants.put("DEFAULT", DEFAULT);
constants.put("VIDEO_CALL", VIDEO_CALL);
return constants;
}
/**
* Gets the name for this module, to be used in the React Native bridge.
*
* @return a string with the module name.
*/
@Override
public String getName() {
return MODULE_NAME;
}
/**
* Helper method to trigger an audio route update when devices change. It
* makes sure the operation is performed on the main thread.
*/
void onAudioDeviceChange() {
mainThreadHandler.post(mainThreadRunner);
}
/**
* Helper method to set the output route to a Bluetooth device.
*
* @param enabled true if Bluetooth should use used, false otherwise.
*/
private void setBluetoothAudioRoute(boolean enabled) {
if (enabled) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
} else {
audioManager.setBluetoothScoOn(false);
audioManager.stopBluetoothSco();
}
}
/**
* Public method to set the current audio mode.
*
* @param mode the desired audio mode.
* @param promise a {@link Promise} which will be resolved if the audio mode
* could be updated successfully, and it will be rejected otherwise.
*/
@ReactMethod
public void setMode(final int mode, final Promise promise) {
if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) {
promise.reject("setMode", "Invalid audio mode " + mode);
return;
}
Runnable r = new Runnable() {
@Override
public void run() {
if (updateAudioRoute(mode)) {
AudioModeModule.this.mode = mode;
promise.resolve(null);
} else {
promise.reject(
"setMode",
"Failed to set the requested audio mode");
}
}
};
mainThreadHandler.post(r);
}
/**
* Setup the audio route change detection mechanism. We use the
* {@link android.media.AudioDeviceCallback} API on Android >= 23 only.
*/
private void setupAudioRouteChangeDetection() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setupAudioRouteChangeDetectionM();
} else {
setupAudioRouteChangeDetectionPreM();
}
}
/**
* Audio route change detection mechanism for Android API >= 23.
*/
@TargetApi(Build.VERSION_CODES.M)
private void setupAudioRouteChangeDetectionM() {
android.media.AudioDeviceCallback audioDeviceCallback =
new android.media.AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(
AudioDeviceInfo[] addedDevices) {
Log.d(TAG, "Audio devices added");
onAudioDeviceChange();
}
@Override
public void onAudioDevicesRemoved(
AudioDeviceInfo[] removedDevices) {
Log.d(TAG, "Audio devices removed");
onAudioDeviceChange();
}
};
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null);
}
/**
* Audio route change detection mechanism for Android API < 23.
*/
private void setupAudioRouteChangeDetectionPreM() {
Context context = getReactApplicationContext();
// Detect changes in wired headset connections.
IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG);
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Wired headset added / removed");
onAudioDeviceChange();
}
};
context.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter);
// Detect Bluetooth device changes.
bluetoothHeadsetMonitor = new BluetoothHeadsetMonitor(this, context);
}
/**
* Updates the audio route for the given mode.
*
* @param mode the audio mode to be used when computing the audio route.
* @return true if the audio route was updated successfully, false
* otherwise.
*/
private boolean updateAudioRoute(int mode) {
Log.d(TAG, "Update audio route for mode: " + mode);
if (mode == DEFAULT) {
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocus(null);
audioManager.setSpeakerphoneOn(false);
audioManager.setMicrophoneMute(true);
setBluetoothAudioRoute(false);
return true;
}
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setMicrophoneMute(false);
if (audioManager.requestAudioFocus(
null,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN)
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.d(TAG, "Audio focus request failed");
return false;
}
boolean useSpeaker = (mode == VIDEO_CALL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// On Android >= M we use the AudioDeviceCallback API, so turn on
// Bluetooth SCO from the start.
if (audioManager.isBluetoothScoAvailableOffCall()) {
audioManager.startBluetoothSco();
}
} else {
// On older Android versions we must set the Bluetooth route
// manually. Also disable the speaker in that case.
setBluetoothAudioRoute(
bluetoothHeadsetMonitor.isHeadsetAvailable());
if (bluetoothHeadsetMonitor.isHeadsetAvailable()) {
useSpeaker = false;
}
}
// XXX: isWiredHeadsetOn is not deprecated when used just for knowing if
// there is a wired headset connected, regardless of audio being routed
// to it.
audioManager.setSpeakerphoneOn(
useSpeaker
&& !(audioManager.isWiredHeadsetOn()
|| audioManager.isBluetoothScoOn()));
return true;
}
}

View File

@@ -0,0 +1,48 @@
package org.jitsi.meet.audiomode;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Implements {@link ReactPackage} for {@link AudioModeModule}.
*/
public class AudioModePackage implements ReactPackage {
/**
* {@inheritDoc}
*/
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
/**
* {@inheritDoc}
*
* @return List of native modules to be exposed by React Native.
*/
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new AudioModeModule(reactContext));
return modules;
}
/**
* {@inheritDoc}
*/
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,189 @@
package org.jitsi.meet.audiomode;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
/**
* Helper class to detect and handle Bluetooth device changes. It monitors
* Bluetooth headsets being connected / disconnected and notifies the module
* about device changes when this occurs.
*/
public class BluetoothHeadsetMonitor {
/**
* {@link AudioModeModule} where this monitor reports.
*/
private final AudioModeModule audioModeModule;
/**
* The {@link Context} in which {@link #audioModeModule} executes.
*/
private final Context context;
/**
* Reference to a proxy object which allows us to query connected devices.
*/
private BluetoothHeadset headset;
/**
* Flag indicating if there are any Bluetooth headset devices currently
* available.
*/
private boolean headsetAvailable = false;
/**
* {@link Handler} for running all operations on the main thread.
*/
private final Handler mainThreadHandler
= new Handler(Looper.getMainLooper());
/**
* Helper for running Bluetooth operations on the main thread.
*/
private final Runnable updateDevicesRunnable
= new Runnable() {
@Override
public void run() {
headsetAvailable
= (headset != null)
&& !headset.getConnectedDevices().isEmpty();
audioModeModule.onAudioDeviceChange();
}
};
public BluetoothHeadsetMonitor(
AudioModeModule audioModeModule,
Context context) {
this.audioModeModule = audioModeModule;
this.context = context;
AudioManager audioManager
= (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (!audioManager.isBluetoothScoAvailableOffCall()) {
Log.w(AudioModeModule.TAG, "Bluetooth SCO is not available");
return;
}
if (getBluetoothHeadsetProfileProxy()) {
registerBluetoothReceiver();
// Initial detection.
updateDevices();
}
}
private boolean getBluetoothHeadsetProfileProxy() {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth");
return false;
}
// XXX: The profile listener listens for system services of the given
// type being available to the application. That is, if our Bluetooth
// adapter has the "headset" profile.
BluetoothProfile.ServiceListener listener
= new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(
int profile,
BluetoothProfile proxy) {
if (profile == BluetoothProfile.HEADSET) {
headset = (BluetoothHeadset) proxy;
updateDevices();
}
}
@Override
public void onServiceDisconnected(int profile) {
// The logic is the same as the logic of onServiceConnected.
onServiceConnected(profile, /* proxy */ null);
}
};
return
adapter.getProfileProxy(
context,
listener,
BluetoothProfile.HEADSET);
}
/**
* Returns the current headset availability.
*
* @return true if there is a Bluetooth headset connected, false otherwise.
*/
public boolean isHeadsetAvailable() {
return headsetAvailable;
}
private void onBluetoothReceiverReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
// XXX: This action will be fired when a Bluetooth headset is
// connected or disconnected to the system. This is not related to
// audio routing.
int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99);
switch (state) {
case BluetoothHeadset.STATE_CONNECTED:
case BluetoothHeadset.STATE_DISCONNECTED:
Log.d(
AudioModeModule.TAG,
"BT headset connection state changed: " + state);
updateDevices();
break;
}
} else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) {
// XXX: This action will be fired when the connection established
// with a Bluetooth headset (called a SCO connection) changes state.
// When the SCO connection is active we route audio to it.
int state
= intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99);
switch (state) {
case AudioManager.SCO_AUDIO_STATE_CONNECTED:
case AudioManager.SCO_AUDIO_STATE_DISCONNECTED:
Log.d(
AudioModeModule.TAG,
"BT SCO connection state changed: " + state);
updateDevices();
break;
}
}
}
private void registerBluetoothReceiver() {
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onBluetoothReceiverReceive(context, intent);
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
filter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
context.registerReceiver(receiver, filter);
}
/**
* Detects if there are new devices connected / disconnected and fires the
* {@link AudioModeModule#onAudioDeviceChange()} callback.
*/
private void updateDevices() {
mainThreadHandler.post(updateDevicesRunnable);
}
}

View File

@@ -1,6 +1,8 @@
rootProject.name = 'jitsi-meet-react'
include ':app'
include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
include ':react-native-vector-icons'

4
app.js
View File

@@ -22,6 +22,7 @@ import conference from './conference';
import API from './modules/API/API';
import translation from "./modules/translation/translation";
import remoteControl from "./modules/remotecontrol/RemoteControl";
const APP = {
// Used by do_external_connect.js if we receive the attach data after
@@ -59,7 +60,8 @@ const APP = {
*/
ConferenceUrl : null,
connection: null,
API
API,
remoteControl
};
// TODO The execution of the mobile app starts from react/index.native.js.

View File

@@ -10,13 +10,16 @@ import Recorder from './modules/recorder/Recorder';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import {reportError} from './modules/util/helpers';
import { reload, reportError } from './modules/util/helpers';
import UIEvents from './service/UI/UIEvents';
import UIUtil from './modules/UI/util/UIUtil';
import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import analytics from './modules/analytics/analytics';
import EventEmitter from "events";
const ConnectionEvents = JitsiMeetJS.events.connection;
const ConnectionErrors = JitsiMeetJS.errors.connection;
@@ -28,6 +31,8 @@ const TrackErrors = JitsiMeetJS.errors.track;
const ConnectionQualityEvents = JitsiMeetJS.events.connectionQuality;
const eventEmitter = new EventEmitter();
let room, connection, localAudio, localVideo;
/**
@@ -198,10 +203,8 @@ function maybeRedirectToWelcomePage(options) {
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', APP.tokenData.isGuest);
if (options.feedbackSubmitted)
window.location.pathname = "close.html";
else
window.location.pathname = "close2.html";
assignWindowLocationPathname(
options.feedbackSubmitted ? "close.html" : "close2.html");
return;
}
@@ -214,11 +217,42 @@ function maybeRedirectToWelcomePage(options) {
if (config.enableWelcomePage) {
setTimeout(() => {
APP.settings.setWelcomePageEnabled(true);
window.location.pathname = "/";
assignWindowLocationPathname('./');
}, 3000);
}
}
/**
* Assigns a specific pathname to window.location.pathname taking into account
* the context root of the Web app.
*
* @param {string} pathname - The pathname to assign to
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @return {void}
*/
function assignWindowLocationPathname(pathname) {
const windowLocation = window.location;
if (!pathname.startsWith('/')) {
// XXX To support a deployment in a sub-directory, assume that the room
// (name) is the last non-directory component of the path (name).
let contextRoot = windowLocation.pathname;
contextRoot
= contextRoot.substring(0, contextRoot.lastIndexOf('/') + 1);
// A pathname equal to ./ specifies the current directory. It will be
// fine but pointless to include it because contextRoot is the current
// directory.
pathname.startsWith('./') && (pathname = pathname.substring(2));
pathname = contextRoot + pathname;
}
windowLocation.pathname = pathname;
}
/**
* Create local tracks of specified types.
* @param {Object} options
@@ -318,7 +352,7 @@ class ConferenceConnector {
case ConferenceErrors.NOT_ALLOWED_ERROR:
{
// let's show some auth not allowed page
window.location.pathname = "authError.html";
assignWindowLocationPathname('authError.html');
}
break;
@@ -383,7 +417,7 @@ class ConferenceConnector {
APP.UI.notifyMaxUsersLimitReached();
break;
case ConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS:
window.location.reload();
reload();
break;
default:
this._handleConferenceFailed(err, ...params);
@@ -485,10 +519,11 @@ export default {
}).then(([tracks, con]) => {
logger.log('initialized with %s local tracks', tracks.length);
APP.connection = connection = con;
this._bindConnectionFailedHandler(con);
this._createRoom(tracks);
this.isDesktopSharingEnabled =
JitsiMeetJS.isDesktopSharingEnabled();
APP.remoteControl.init();
this._bindConnectionFailedHandler(con);
this._createRoom(tracks);
if (UIUtil.isButtonEnabled('contacts')
&& !interfaceConfig.filmStripOnly) {
@@ -981,7 +1016,7 @@ export default {
let externalInstallation = false;
if (shareScreen) {
createLocalTracks({
this.screenSharingPromise = createLocalTracks({
devices: ['desktop'],
desktopSharingExtensionExternalInstallation: {
interval: 500,
@@ -1070,7 +1105,10 @@ export default {
dialogTitleKey, dialogTxt, false);
});
} else {
createLocalTracks({ devices: ['video'] }).then(
APP.remoteControl.receiver.stop();
this.screenSharingPromise = createLocalTracks(
{ devices: ['video'] })
.then(
([stream]) => this.useVideoStream(stream)
).then(() => {
this.videoSwitchInProgress = false;
@@ -1102,6 +1140,8 @@ export default {
}
);
room.on(ConferenceEvents.PARTCIPANT_FEATURES_CHANGED,
user => APP.UI.onUserFeaturesChanged(user));
room.on(ConferenceEvents.USER_JOINED, (id, user) => {
if (user.isHidden())
return;
@@ -1434,7 +1474,7 @@ export default {
APP.UI.addListener(UIEvents.LOGOUT, () => {
AuthHandler.logout(room).then(url => {
if (url) {
window.location.href = url;
UIUtil.redirect(url);
} else {
this.hangup(true);
}
@@ -1600,12 +1640,23 @@ export default {
},
/**
* Adds any room listener.
* @param eventName one of the ConferenceEvents
* @param callBack the function to be called when the event occurs
* @param {string} eventName one of the ConferenceEvents
* @param {Function} listener the function to be called when the event
* occurs
*/
addConferenceListener(eventName, callBack) {
room.on(eventName, callBack);
addConferenceListener(eventName, listener) {
room.on(eventName, listener);
},
/**
* Removes any room listener.
* @param {string} eventName one of the ConferenceEvents
* @param {Function} listener the listener to be removed.
*/
removeConferenceListener(eventName, listener) {
room.off(eventName, listener);
},
/**
* Inits list of current devices and event listener for device change.
* @private
@@ -1763,6 +1814,7 @@ export default {
* requested
*/
hangup (requestFeedback = false) {
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
APP.UI.hideRingOverLay();
let requestFeedbackPromise = requestFeedback
? APP.UI.requestFeedbackOnHangup()
@@ -1813,5 +1865,36 @@ export default {
APP.settings.setAvatarUrl(url);
APP.UI.setUserAvatarUrl(room.myUserId(), url);
sendData(commands.AVATAR_URL, url);
},
/**
* Sends a message via the data channel.
* @param {string} to the id of the endpoint that should receive the
* message. If "" - the message will be sent to all participants.
* @param {object} payload the payload of the message.
* @throws NetworkError or InvalidStateError or Error if the operation
* fails.
*/
sendEndpointMessage (to, payload) {
room.sendEndpointMessage(to, payload);
},
/**
* Adds new listener.
* @param {String} eventName the name of the event
* @param {Function} listener the listener.
*/
addListener (eventName, listener) {
eventEmitter.addListener(eventName, listener);
},
/**
* Removes listener.
* @param {String} eventName the name of the event that triggers the
* listener
* @param {Function} listener the listener.
*/
removeListener (eventName, listener) {
eventEmitter.removeListener(eventName, listener);
}
};

View File

@@ -20,10 +20,10 @@ var config = { // eslint-disable-line no-unused-vars
//focusUserJid: 'focus@auth.jitsi-meet.example.com', // The real JID of focus participant - can be overridden here
//defaultSipNumber: '', // Default SIP number
// Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
desktopSharingChromeMethod: 'ext',
// The ID of the jidesha extension for Chrome.
desktopSharingChromeExtId: 'diibjkoicjeejcmhdnailmkgecihlobk',
desktopSharingChromeExtId: null,
// Whether desktop sharing should be disabled on Chrome.
desktopSharingChromeDisabled: true,
// The media sources to use when using screen sharing with the Chrome
// extension.
desktopSharingChromeSources: ['screen', 'window', 'tab'],
@@ -78,5 +78,7 @@ var config = { // eslint-disable-line no-unused-vars
// edit their profile.
enableUserRolesBasedOnToken: false,
// Suspending video might cause problems with audio playback. Disabling until these are fixed.
disableSuspendVideo: true
disableSuspendVideo: true,
// disables or enables RTX (RFC 4588).
disableRtx: true
};

View File

@@ -70,7 +70,7 @@
display: none;
position: relative;
background-size: contain;
border: $thumbnailVideoBorder solid $thumbnailBorderColor;
border: $thumbnailVideoBorder solid transparent;
border-radius: $borderRadius;
margin: 0 $thumbnailVideoMargin;
@@ -92,7 +92,7 @@
0 0 3px $videoThumbnailSelected !important;
}
.remotevideomenu {
.remotevideomenu > .icon-menu {
display: none;
}
@@ -105,7 +105,7 @@
box-shadow: inset 0 0 3px $videoThumbnailHovered,
0 0 3px $videoThumbnailHovered;
.remotevideomenu {
.remotevideomenu > .icon-menu {
display: inline-block;
}
}
@@ -121,4 +121,4 @@
}
}
}
}
}

View File

@@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-mic-camera-combined:before {
content: "\e903";
}
.icon-feedback:before {
content: "\e91d";
}

View File

@@ -1,6 +1,6 @@
.inlay {
margin-top: 14%;
@include border-radius(3px);
@include border-radius(4px);
padding: 40px 38px 44px;
color: #fff;
background: $inlayColorBg;

View File

@@ -10,7 +10,7 @@
color: $popoverFontColor;
background-color: $popoverBg;
background-clip: padding-box;
border-radius: 3px;
border-radius: $borderRadius;
/*-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);*/
/*box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);*/
white-space: normal;

View File

@@ -1,18 +1,18 @@
@charset "UTF-8";
/*!
* jQuery contextMenu - Plugin for simple contextMenu handling
*
* Version: v2.1.1
*
* Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
* Web: http://swisnl.github.io/jQuery-contextMenu/
*
* Copyright (c) 2011-2016 SWIS BV and contributors
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
*
* Date: 2016-02-28T09:53:18.890Z
/*!
* jQuery contextMenu - Plugin for simple contextMenu handling
*
* Version: v2.1.1
*
* Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
* Web: http://swisnl.github.io/jQuery-contextMenu/
*
* Copyright (c) 2011-2016 SWIS BV and contributors
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
*
* Date: 2016-02-28T09:53:18.890Z
*/
@font-face {
font-family: "context-menu-icons";
@@ -88,7 +88,7 @@
list-style-type: none;
background: #fff;
border: 1px solid #bebebe;
border-radius: 3px;
border-radius: $borderRadius;
-webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
}
@@ -156,8 +156,8 @@
transform: translateY(-50%);
}
/**
* Inputs
/**
* Inputs
*/
.context-menu-item.context-menu-input {
padding: 5px 10px;

View File

@@ -6,7 +6,6 @@
padding: 0;
margin: 2px 0;
bottom: 0;
width: 100px;
height: auto;
&:first-child {
@@ -66,4 +65,9 @@
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
display:block !important;
}
}
.remote-control-spinner {
top: 6px;
left: 2px;
}

View File

@@ -127,3 +127,10 @@ $inputControlEmColor: #f29424;
//buttons
$linkFontColor: #489afe;
$linkHoverFontColor: #287ade;
/**
* Landing
*/
$primaryUnsupportedBrowserButtonBgColor: #17a0db;
$unsupportedBrowserButtonBgColor: #ff9a00;
$unsupportedBrowserTextColor: #4a4a4a;

View File

@@ -19,7 +19,7 @@
&__background {
@include topLeft();
background-color: black;
border-radius: $borderRadius - 1;
border-radius: $borderRadius;
width: 100%;
height: 100%;
}
@@ -474,6 +474,7 @@
#localConnectionMessage {
display: none;
position: absolute;
left: 0;
width: 100%;
top:50%;
z-index: 2;

View File

@@ -67,5 +67,7 @@
@import '404';
@import 'policy';
@import 'filmstrip';
@import 'unsupported-browser/unsupported-desktop-browser';
@import 'unsupported-browser/unsupported-mobile-browser';
/* Modules END */

View File

@@ -85,9 +85,6 @@ $popupMenuSelectedItemBackground: rgba(256, 256, 256, .2);
// Toolbar
$splitterColor: #ccc;
// Thumbnail
$thumbnailBorderColor: rgba(71, 71, 71, .7);
/**
* Forms
*/

View File

@@ -0,0 +1,132 @@
.supported-browser {
color: #929391;
display: inline-block;
font-size: 20px;
margin: 1em 7px;
vertical-align: middle;
width: 138px;
&__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;
}
&__link {
color: #087dba;
text-decoration: none;
&:hover {
text-decoration: none;
}
&: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

@@ -0,0 +1,69 @@
.unsupported-mobile-browser {
background-color: #fff;
height: 100vh;
padding: 35px 0;
width: 100vw;
&__body {
color: $unsupportedBrowserTextColor;
margin: auto;
max-width: 40em;
text-align: center;
width: 75%;
a:active {
text-decoration: none;
}
}
&__text {
font-size: 1.8em;
line-height: em(29px, 21px);
margin-bottom: 0.65em;
&_small {
font-size: 1.5em;
margin-bottom: 1em;
margin-top: em(21, 18);
strong {
font-size: em(21, 18);
}
}
}
&__logo {
height: 108px;
width: 77px;
}
&__button {
border: 0;
height: 42px;
margin: 0 auto;
max-width: 300px;
width: 98%;
@include border-radius(8px);
background-color: $unsupportedBrowserButtonBgColor;
font-size: 1.5em;
font-weight: 300;
letter-spacing: 0.5px;
text-shadow: 0px 1px 2px $unsupportedBrowserTextColor;
// Disable standard button effects.
box-shadow: none;
outline: none;
&:active {
background-color: $unsupportedBrowserButtonBgColor;
}
&_primary {
background-color: $primaryUnsupportedBrowserButtonBgColor;
&:active {
background-color: $primaryUnsupportedBrowserButtonBgColor;
}
}
}
}

View File

@@ -1,138 +0,0 @@
@import 'variables';
body {
width:100%;
height:100%;
background-color: white;
color: #424242;
font-family: $baseFontFamily;
font-size: 28px;
margin:0;
padding:0;
}
#wrap{
display: block;
position: absolute;
width:500px;
height: 565px;
overflow:hidden;
text-align: center;
margin: auto;
top: 0; left: 0; bottom: 0; right: 0;
}
.firefox{
font-size: 11pt;
color: #c8c8c8;
width: 468px;
text-align: center;
margin: 30px auto 0px auto;
padding-left: 15px;
}
#text{
display:inline-block;
font-size: 28px;
/* width: 568px; */
vertical-align:middle;
padding-top: 25px;
}
a {
color: #087dba;
text-decoration:none;
}
.browser {
width: 138px;
height: 163px;
margin-top: 5px;
background-color: #e8e8e8;
border: 1px solid #cfcfcf;
border-radius: 10px;
}
.browser_wrapper
{
width: 138px;
/* height: 188px; */
vertical-align: middle;
color: #929391;
font-size: 20px;
float: left;
margin-left: 15px;
margin-top: 5px;
}
.browser_text
{
height: 2em;
}
.supported_browsers
{
margin: 0px auto 0px auto;
/* width: 660px; */
}
.clear
{
clear: both;
}
.button
{
background-color: #62c82a;
border: 1px solid #3c8117;
border-radius: 10px;
color: #FFFFFF;
font-size: 12px;
text-align: center;
width: 115px;
height: 26px;
padding-top: 13px;
margin: 15px auto 0px auto;
}
.logo
{
margin: 20px auto 0px auto;
}
#chrome_logo
{
width: 78px;
height: 78px;
background-image: url('../images/chrome.png');
}
#chromium_logo
{
width: 77px;
height: 78px;
background-image: url('../images/chromium.png');
}
#firefox_logo
{
width: 86px;
height: 80px;
background-image: url('../images/firefox.png');
}
#opera_logo
{
width: 73px;
height: 78px;
background-image: url('../images/opera.png');
}
#safari_logo
{
width: 78px;
height: 79px;
background-image: url('../images/safari.png');
}
#ie_logo
{
width: 80px;
height: 78px;
background-image: url('../images/ie.png');
}

View File

@@ -4,7 +4,6 @@
*.ico /usr/share/jitsi-meet/
libs /usr/share/jitsi-meet/
css/all.css /usr/share/jitsi-meet/css/
css/unsupported_browser.css /usr/share/jitsi-meet/css/
sounds /usr/share/jitsi-meet/
fonts /usr/share/jitsi-meet/
images /usr/share/jitsi-meet/

223
doc/api.md Normal file
View File

@@ -0,0 +1,223 @@
# Jitsi Meet API
You can use the Jitsi Meet API to embed Jitsi Meet in to your application.
## Installation
To embed Jitsi Meet in your application you need to add the Jitsi Meet API library:
```javascript
<script src="https://meet.jit.si/external_api.js"></script>
```
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object:
```javascript
<script>
var domain = "meet.jit.si";
var room = "JitsiMeetAPIExample";
var width = 700;
var height = 700;
var api = new JitsiMeetExternalAPI(domain, room, width, height);
</script>
```
You can use the above lines to indicate where exactly you want the Jitsi Meet conference to be placed in your HTML code,
or you can specify the parent HTML element for the Jitsi Meet conference in the `JitsiMeetExternalAPI`
constructor:
```javascript
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement);
```
If you don't specify the room the user will enter in new conference with a random room name.
You can overwrite options set in [config.js]() and [interface_config.js](). For example, to enable the film-strip-only interface mode and disable simulcast, you can use:
```javascript
var configOverwrite = {disableSimulcast: true};
var interfaceConfigOverwrite = {filmStripOnly: true};
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, configOverwrite, interfaceConfigOverwrite);
```
## Controlling the embedded Jitsi Meet Conference
You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`:
```javascript
api.executeCommand(command, ...arguments)
```
The `command` parameter is String object with the name of the command. The following commands are currently supported:
* **displayName** - Sets the display name of the local participant. This command requires one argument - the new display name to be set.
```javascript
api.executeCommand('displayName', 'New Nickname');
```
* **toggleAudio** - Mutes / unmutes the audio for the local participant. No arguments are required.
```javascript
api.executeCommand('toggleAudio')
```
* **toggleVideo** - Mutes / unmutes the video for the local participant. No arguments are required.
```javascript
api.executeCommand('toggleVideo')
```
* **toggleFilmStrip** - Hides / shows the film strip. No arguments are required.
```javascript
api.executeCommand('toggleFilmStrip')
```
* **toggleChat** - Hides / shows the chat. No arguments are required.
```javascript
api.executeCommand('toggleChat')
```
* **toggleContactList** - Hides / shows the contact list. No arguments are required.
```javascript
api.executeCommand('toggleContactList')
```
* **toggleShareScreen** - Starts / stops screen sharing. No arguments are required.
```javascript
api.executeCommand('toggleShareScreen')
```
* **hangup** - Hangups the call. No arguments are required.
```javascript
api.executeCommand('hangup')
```
* **email** - Changes the local email address. This command requires one argument - the new email address to be set.
```javascript
api.executeCommand('email', 'example@example.com')
```
* **avatarUrl** - Changes the local avatar URL. This command requires one argument - the new avatar URL to be set.
```javascript
api.executeCommand('avatarUrl', 'https://avatars0.githubusercontent.com/u/3671647')
```
You can also execute multiple commands using the `executeCommands` method:
```javascript
api.executeCommands(commands)
```
The `commands` parameter is an object with the names of the commands as keys and the arguments for the commands asvalues:
```javascript
api.executeCommands({displayName: ['nickname'], toggleAudio: []});
```
You can add event listeners to the embedded Jitsi Meet using the `addEventListener` method.
```javascript
api.addEventListener(event, listener)
```
The `event` parameter is a String object with the name of the event.
The `listener` parameter is a Function object with one argument that will be notified when the event occurs with data related to the event.
The following events are currently supported:
* **incomingMessage** - Event notifications about incoming
messages. The listener will receive an object with the following structure:
```javascript
{
"from": from, // JID of the user that sent the message
"nick": nick, // the nickname of the user that sent the message
"message": txt // the text of the message
}
```
* **outgoingMessage** - Event notifications about outgoing
messages. The listener will receive an object with the following structure:
```javascript
{
"message": txt // the text of the message
}
```
* **displayNameChanged** - event notifications about display name
changes. The listener will receive an object with the following structure:
```javascript
{
"jid": jid, // the JID of the participant that changed his display name
"displayname": displayName // the new display name
}
```
* **participantJoined** - event notifications about new participants who join the room. The listener will receive an object with the following structure:
```javascript
{
"jid": jid // the JID of the participant
}
```
* **participantLeft** - event notifications about participants that leave the room. The listener will receive an object with the following structure:
```javascript
{
"jid": jid // the JID of the participant
}
```
* **videoConferenceJoined** - event notifications fired when the local user has joined the video conference. The listener will receive an object with the following structure:
```javascript
{
"roomName": room // the room name of the conference
}
```
* **videoConferenceLeft** - event notifications fired when the local user has left the video conference. The listener will receive an object with the following structure:
```javascript
{
"roomName": room // the room name of the conference
}
```
* **readyToClose** - event notification fired when Jitsi Meet is ready to be closed (hangup operations are completed).
You can also add multiple event listeners by using `addEventListeners`.
This method requires one argument of type Object. The object argument must
have the names of the events as keys and the listeners of the events as values.
```javascript
function incomingMessageListener(object)
{
// ...
}
function outgoingMessageListener(object)
{
// ...
}
api.addEventListeners({
incomingMessage: incomingMessageListener,
outgoingMessage: outgoingMessageListener})
```
If you want to remove a listener you can use `removeEventListener` method with argument the name of the event.
```javascript
api.removeEventListener("incomingMessage");
```
If you want to remove more than one event you can use `removeEventListeners` method with an Array with the names of the events as an argument.
```javascript
api.removeEventListeners(["incomingMessage", "outgoingMessageListener"]);
```
You can get the number of participants in the conference with the following API function:
```javascript
var numberOfParticipants = api.getNumberOfParticipants();
```
You can remove the embedded Jitsi Meet Conference with the following API function:
```javascript
api.dispose()
```
NOTE: It's a good practice to remove the conference before the page is unloaded.
[config.js]: https://github.com/jitsi/jitsi-meet/blob/master/config.js
[interface_config.js]: https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js

View File

@@ -1,215 +0,0 @@
Jitsi Meet API
============
You can use Jitsi Meet API to embed Jitsi Meet in to your application.
Installation
==========
To embed Jitsi Meet in your application you need to add Jitsi Meet API library
```javascript
<script src="https://meet.jit.si/external_api.js"></script>
```
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object
```javascript
<script>
var domain = "meet.jit.si";
var room = "JitsiMeetAPIExample";
var width = 700;
var height = 700;
var api = new JitsiMeetExternalAPI(domain, room, width, height);
</script>
```
You can paste that lines in your html code where you want to be placed the Jitsi Meet conference
or you can specify the parent HTML element for the Jitsi Meet conference in the JitsiMeetExternalAPI
constructor.
```javascript
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement);
```
If you don't specify room the user will enter in new conference with random room name.
You can overwrite options set in config.js and interface_config.js. For example, to enable the film-strip-only interface mode and disable simulcast, you can use:
```javascript
var configOverwrite = {enableSimulcast: false};
var interfaceConfigOverwrite = {filmStripOnly: true};
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, configOverwrite, interfaceConfigOverwrite);
```
Controlling embedded Jitsi Meet Conference
=========
You can control the embedded Jitsi Meet conference using the JitsiMeetExternalAPI object.
You can send command to Jitsi Meet conference using ```executeCommand```.
```
api.executeCommand(command, ...arguments)
```
The ```command``` parameter is String object with the name of the command.
Currently we support the following commands:
* **displayName** - sets the display name of the local participant. This command requires one argument -
the new display name to be set
```
api.executeCommand('displayName', 'New Nickname');
```
* **toggleAudio** - mutes / unmutes the audio for the local participant. No arguments are required.
```
api.executeCommand('toggleAudio')
```
* **toggleVideo** - mutes / unmutes the video for the local participant. No arguments are required.
```
api.executeCommand('toggleVideo')
```
* **toggleFilmStrip** - hides / shows the film strip. No arguments are required.
```
api.executeCommand('filmStrip')
```
* **toggleChat** - hides / shows the chat. No arguments are required.
```
api.executeCommand('toggleChat')
```
* **toggleContactList** - hides / shows the contact list. No arguments are required.
```
api.executeCommand('toggleContactList')
```
* **toggleShareScreen** - starts / stops the screen sharing. No arguments are required.
```
api.executeCommand('toggleShareScreen')
```
* **hangup** - Hangups the call. No arguments are required.
```
api.executeCommand('hangup')
```
* **email** - Hangups the call. No arguments are required.
```
api.executeCommand('email', 'example@example.com')
```
* **avatarUrl** - Hangups the call. No arguments are required.
```
api.executeCommand('avatarUrl', 'avatarUrl')
```
You can also execute multiple commands using the method ```executeCommands```.
```
api.executeCommands(commands)
```
The ```commands``` parameter is object with keys the names of the commands and values the arguments for the
commands.
```
api.executeCommands({displayName: ['nickname'], toggleAudio: []});
```
You can add event listeners to the embedded Jitsi Meet using ```addEventListener``` method.
```
api.addEventListener(event, listener)
```
The ```event``` parameter is String object with the name of the event.
The ```listener``` paramenter is Function object with one argument that will be notified when the event occurs
with data related to the event.
Currently we support the following events:
* **incomingMessage** - event notifications about incoming
messages. The listener will receive object with the following structure:
```
{
"from": from,//JID of the user that sent the message
"nick": nick,//the nickname of the user that sent the message
"message": txt//the text of the message
}
```
* **outgoingMessage** - event notifications about outgoing
messages. The listener will receive object with the following structure:
```
{
"message": txt//the text of the message
}
```
* **displayNameChanged** - event notifications about display name
change. The listener will receive object with the following structure:
```
{
jid: jid,//the JID of the participant that changed his display name
displayname: displayName //the new display name
}
```
* **participantJoined** - event notifications about new participant.
The listener will receive object with the following structure:
```
{
jid: jid //the jid of the participant
}
```
* **participantLeft** - event notifications about participant that left room.
The listener will receive object with the following structure:
```
{
jid: jid //the jid of the participant
}
```
* **videoConferenceJoined** - event notifications fired when the local user has joined the video conference.
The listener will receive object with the following structure:
```
{
roomName: room //the room name of the conference
}
```
* **videoConferenceLeft** - event notifications fired when the local user has left the video conference.
The listener will receive object with the following structure:
```
{
roomName: room //the room name of the conference
}
```
* **readyToClose** - event notification fired when Jitsi Meet is ready to be closed (hangup operations are completed).
You can also add multiple event listeners by using ```addEventListeners```.
This method requires one argument of type Object. The object argument must
have keys with the names of the events and values the listeners of the events.
```
function incomingMessageListener(object)
{
...
}
function outgoingMessageListener(object)
{
...
}
api.addEventListeners({
incomingMessage: incomingMessageListener,
outgoingMessage: outgoingMessageListener})
```
If you want to remove a listener you can use ```removeEventListener``` method with argument the name of the event.
```
api.removeEventListener("incomingMessage");
```
If you want to remove more than one event you can use ```removeEventListeners``` method with argument
array with the names of the events.
```
api.removeEventListeners(["incomingMessage", "outgoingMessageListener"]);
```
You can get the number of participants in the conference with the following code:
```
var numberOfParticipants = api.getNumberOfParticipants();
```
You can remove the embedded Jitsi Meet Conference with the following code:
```
api.dispose()
```
It is a good practice to remove the conference before the page is unloaded.

View File

@@ -131,9 +131,9 @@ Or autostart it by adding the line in `/etc/rc.local`:
## Install Jitsi Conference Focus (jicofo)
Install JDK and Ant if missing:
Install JDK and Maven if missing:
```
apt-get install default-jdk ant
apt-get install default-jdk maven
```
_NOTE: When installing on older Debian releases keep in mind that you need JDK >= 1.7._
@@ -145,12 +145,14 @@ git clone https://github.com/jitsi/jicofo.git
Build distribution package. Replace {os-name} with one of: 'lin', 'lin64', 'macosx', 'win', 'win64'.
```sh
cd jicofo
ant dist.{os-name}
mvn package -DskipTests -Dassembly.skipAssembly=false
```
Run jicofo:
```sh
cd dist/{os-name}'
./jicofo.sh --host=127.0.0.1 --domain=jitsi.example.com --secret=YOURSECRET2 --user_domain=auth.jitsi.example.com --user_name=focus --user_password=YOURSECRET3
=======
unzip target/jicofo-{os-name}-1.0-SNAPSHOT.zip
cd jicofo-{os-name}-1.0-SNAPSHOT'
./jicofo.sh --host=localhost --domain=jitsi.example.com --secret=YOURSECRET2 --user_domain=auth.jitsi.example.com --user_name=focus --user_password=YOURSECRET3
```
## Deploy Jitsi Meet
@@ -159,6 +161,8 @@ Checkout and configure Jitsi Meet:
cd /srv
git clone https://github.com/jitsi/jitsi-meet.git
mv jitsi-meet/ jitsi.example.com
npm install
make
```
Edit host names in `/srv/jitsi.example.com/config.js` (see also the example config file):
@@ -167,11 +171,11 @@ var config = {
hosts: {
domain: 'jitsi.example.com',
muc: 'conference.jitsi.example.com',
bridge: 'jitsi-videobridge.jitsi.example.com'
bridge: 'jitsi-videobridge.jitsi.example.com',
focus: 'focus.jitsi.example.com'
},
useNicks: false,
bosh: '//jitsi.example.com/http-bind', // FIXME: use xep-0156 for that
desktopSharing: 'false' // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
//chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
//minChromeExtVersion: '0.1' // Required version of Chrome extension
};

94
doc/mobile.md Normal file
View File

@@ -0,0 +1,94 @@
# 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.
First make sure the [React Native dependencies]() are installed.
**NOTE**: This document assumes the app is being built on a macOS system.
**NOTE**: The app must be built for an actual device since the simulators don't
work properly with the native plugins we require.
## iOS
1. Install some extra dependencies
- Install ios-deploy globally (in case you want to use the React Native CLI
to deploy the app to the device)
```bash
npm install -g ios-deploy
```
2. Build the app
There are 2 ways to build the app: using the CLI or using Xcode.
Using the CLI:
```bash
react-native run-ios --device
```
When the app is launched from the CLI the output can be checked with the
following command:
```bash
react-native log-ios
```
Using Xcode
- Open **ios/jitsi-meet-react.xcworkspace** in Xcode. Make sure it's the
workspace file!
- Select your device from the top bar and hit the "play" button.
When the app is launched from Xcode the Debug console will show the output
logs the application creates.
3. Other remarks
It's likely you'll need to change the bundle ID for deploying to a device
because the default bundle ID points to the application signed by Atlassian.
This can be changed in the "General" tab. Under "Identity" set
"Bundle Identifier" to a different value, and adjust the "Team" in the
"Signing" section to match your own.
## Android
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
The app can be built using the CLI utility as follows:
```bash
react-native run-android
```
It will be launched on the connected Android device.
## Debugging
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
is being interpreted by Chrome's V8 engine, instead of JSCore which
React Native uses. It's important to keep this in mind due to potential
differences in supported JavaScript features.
[Android Studio]: https://developer.android.com/studio/index.html
[debugging]: https://facebook.github.io/react-native/docs/debugging.html
[React Native]: https://facebook.github.io/react-native/
[React Native dependencies]: https://facebook.github.io/react-native/docs/getting-started.html#installing-dependencies

6
flow-typed/npm/flow-bin_v0.x.x.js vendored Normal file
View File

@@ -0,0 +1,6 @@
// flow-typed signature: 6a5610678d4b01e13bbfbbc62bdaf583
// flow-typed version: 3817bc6980/flow-bin_v0.x.x/flow_>=v0.25.x
declare module "flow-bin" {
declare module.exports: string;
}

89
flow-typed/npm/react-redux_v5.x.x.js vendored Normal file
View File

@@ -0,0 +1,89 @@
// flow-typed signature: 0ed284c5a2e97a9e3c0e87af3dedc09d
// flow-typed version: bdf1e66252/react-redux_v5.x.x/flow_>=v0.30.x
import type { Dispatch, Store } from 'redux'
declare module 'react-redux' {
/*
S = State
A = Action
OP = OwnProps
SP = StateProps
DP = DispatchProps
*/
declare type MapStateToProps<S, OP: Object, SP: Object> = (state: S, ownProps: OP) => SP | MapStateToProps<S, OP, SP>;
declare type MapDispatchToProps<A, OP: Object, DP: Object> = ((dispatch: Dispatch<A>, ownProps: OP) => DP) | DP;
declare type MergeProps<SP, DP: Object, OP: Object, P: Object> = (stateProps: SP, dispatchProps: DP, ownProps: OP) => P;
declare type StatelessComponent<P> = (props: P) => ?React$Element<any>;
declare class ConnectedComponent<OP, P, Def, St> extends React$Component<void, OP, void> {
static WrappedComponent: Class<React$Component<Def, P, St>>;
getWrappedInstance(): React$Component<Def, P, St>;
static defaultProps: void;
props: OP;
state: void;
}
declare type ConnectedComponentClass<OP, P, Def, St> = Class<ConnectedComponent<OP, P, Def, St>>;
declare type Connector<OP, P> = {
(component: StatelessComponent<P>): ConnectedComponentClass<OP, P, void, void>;
<Def, St>(component: Class<React$Component<Def, P, St>>): ConnectedComponentClass<OP, P, Def, St>;
};
declare class Provider<S, A> extends React$Component<void, { store: Store<S, A>, children?: any }, void> { }
declare type ConnectOptions = {
pure?: boolean,
withRef?: boolean
};
declare type Null = null | void;
declare function connect<A, OP>(
...rest: Array<void> // <= workaround for https://github.com/facebook/flow/issues/2360
): Connector<OP, $Supertype<{ dispatch: Dispatch<A> } & OP>>;
declare function connect<A, OP>(
mapStateToProps: Null,
mapDispatchToProps: Null,
mergeProps: Null,
options: ConnectOptions
): Connector<OP, $Supertype<{ dispatch: Dispatch<A> } & OP>>;
declare function connect<S, A, OP, SP>(
mapStateToProps: MapStateToProps<S, OP, SP>,
mapDispatchToProps: Null,
mergeProps: Null,
options?: ConnectOptions
): Connector<OP, $Supertype<SP & { dispatch: Dispatch<A> } & OP>>;
declare function connect<A, OP, DP>(
mapStateToProps: Null,
mapDispatchToProps: MapDispatchToProps<A, OP, DP>,
mergeProps: Null,
options?: ConnectOptions
): Connector<OP, $Supertype<DP & OP>>;
declare function connect<S, A, OP, SP, DP>(
mapStateToProps: MapStateToProps<S, OP, SP>,
mapDispatchToProps: MapDispatchToProps<A, OP, DP>,
mergeProps: Null,
options?: ConnectOptions
): Connector<OP, $Supertype<SP & DP & OP>>;
declare function connect<S, A, OP, SP, DP, P>(
mapStateToProps: MapStateToProps<S, OP, SP>,
mapDispatchToProps: MapDispatchToProps<A, OP, DP>,
mergeProps: MergeProps<SP, DP, OP, P>,
options?: ConnectOptions
): Connector<OP, P>;
}

56
flow-typed/npm/redux_v3.x.x.js vendored Normal file
View File

@@ -0,0 +1,56 @@
// flow-typed signature: ba132c96664f1a05288f3eb2272a3c35
// flow-typed version: c4bbd91cfc/redux_v3.x.x/flow_>=v0.33.x
declare module 'redux' {
/*
S = State
A = Action
*/
declare type Dispatch<A: { type: $Subtype<string> }> = (action: A) => A;
declare type MiddlewareAPI<S, A> = {
dispatch: Dispatch<A>;
getState(): S;
};
declare type Store<S, A> = {
// rewrite MiddlewareAPI members in order to get nicer error messages (intersections produce long messages)
dispatch: Dispatch<A>;
getState(): S;
subscribe(listener: () => void): () => void;
replaceReducer(nextReducer: Reducer<S, A>): void
};
declare type Reducer<S, A> = (state: S, action: A) => S;
declare type Middleware<S, A> =
(api: MiddlewareAPI<S, A>) =>
(next: Dispatch<A>) => Dispatch<A>;
declare type StoreCreator<S, A> = {
(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
};
declare type StoreEnhancer<S, A> = (next: StoreCreator<S, A>) => StoreCreator<S, A>;
declare function createStore<S, A>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
declare function createStore<S, A>(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<S, A>): Store<S, A>;
declare function applyMiddleware<S, A>(...middlewares: Array<Middleware<S, A>>): StoreEnhancer<S, A>;
declare type ActionCreator<A, B> = (...args: Array<B>) => A;
declare type ActionCreators<K, A> = { [key: K]: ActionCreator<A, any> };
declare function bindActionCreators<A, C: ActionCreator<A, any>>(actionCreator: C, dispatch: Dispatch<A>): C;
declare function bindActionCreators<A, K, C: ActionCreators<K, A>>(actionCreators: C, dispatch: Dispatch<A>): C;
declare function combineReducers<O: Object, A>(reducers: O): Reducer<$ObjMap<O, <S>(r: Reducer<S, any>) => S>, A>;
declare function compose<S, A>(...fns: Array<StoreEnhancer<S, A>>): Function;
}

Binary file not shown.

View File

@@ -14,6 +14,7 @@
<glyph unicode="&#xe900;" glyph-name="connection-lost" horiz-adv-x="1414" d="M0 299.153h196.337v-187.951h-196.337v187.951zM271.842 480.372h196.337v-369.169h-196.337v369.169zM543.656 661.562h196.337v-550.36h-196.337v550.36zM815.47 842.766v-731.564h119.56c-14.589 33.025-23.125 71.503-23.232 111.943 0.132 86.42 38.697 163.851 99.656 216.468l0.348 403.153h-196.332zM1087.292 1024v-533.672c28.874 10.572 62.222 16.73 97.009 16.825 35.717-0.129 69.823-6.614 101.322-18.371l-1.999 535.218h-196.332zM1192.868 439.852c-0.009 0-0.020 0-0.031 0-122.247 0-221.351-98.447-221.372-219.896 0-0.007 0-0.014 0-0.021 0-121.467 99.111-219.935 221.372-219.935 0.011 0 0.021 0 0.032 0 122.248 0.014 221.345 98.477 221.345 219.935 0 0.007 0 0.013 0 0.020-0.021 121.441-99.11 219.883-221.345 219.897zM1194.706 372.607c87.601-0.006 158.614-69.787 158.614-155.866 0-0.006 0-0.012 0-0.019-0.022-86.062-71.026-155.822-158.614-155.828-87.588 0.006-158.593 69.766-158.615 155.826 0 0.007 0 0.014 0 0.020 0 86.079 71.013 155.86 158.613 155.866zM1286.795 355.682l48.348-52.528-236.375-217.567-48.348 52.528 236.375 217.567z" />
<glyph unicode="&#xe901;" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe902;" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
<glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
<glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
<glyph unicode="&#xe905;" glyph-name="hangup" d="M512 640c-68 0-134-10-196-30v-132c0-16-10-34-24-40-42-20-80-46-114-78-8-8-18-12-30-12s-22 4-30 12l-106 106c-8 8-12 18-12 30s4 22 12 30c130 124 306 200 500 200s370-76 500-200c8-8 12-18 12-30s-4-22-12-30l-106-106c-8-8-18-12-30-12s-22 4-30 12c-34 32-72 58-114 78-14 6-24 20-24 38v132c-62 20-128 32-196 32z" />
<glyph unicode="&#xe906;" glyph-name="chat" d="M854 342v512h-684v-598l86 86h598zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,35 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M756.704 395.862l267.296-202.213v635.075l-267.296-202.213v191.923c0 12.085-11.296 21.863-25.216 21.863h-706.272c-13.92 0-25.216-9.777-25.216-21.863v-612.25c0-12.085 11.296-21.863 25.216-21.863h706.272c13.92 0 25.216 9.777 25.216 21.863v189.679zM371.338 647.772c47.817 0 86.529-40.232 86.529-89.811v-184.835c0-49.651-38.713-89.883-86.529-89.883-47.788 0-86.515 40.232-86.515 89.883v184.835c0 49.579 38.756 89.811 86.515 89.811v0zM356.754 709.93v32.78h33.718v-33.412c73.858-9.606 131.235-73.73 131.235-151.351v-88.232h-30.636v88.232c0 67.57-53.696 122.534-119.734 122.534-66.024 0-119.691-54.964-119.691-122.534v-88.232h-30.636v88.232c0 79.215 59.674 144.502 135.744 151.969v0.014z"
],
"attrs": [
{}
],
"isMulticolor": false,
"isMulticolor2": false,
"grid": 0,
"tags": [
"Combined Shape"
]
},
"attrs": [
{}
],
"properties": {
"order": 109,
"id": 0,
"name": "mic-camera-combined",
"prevSize": 32,
"code": 59651
},
"setIdx": 0,
"setId": 2,
"iconIdx": 0
},
{
"icon": {
"paths": [
@@ -26,8 +55,8 @@
"prevSize": 32,
"code": 59677
},
"setIdx": 0,
"setId": 2,
"setIdx": 1,
"setId": 1,
"iconIdx": 0
},
{
@@ -55,8 +84,8 @@
"prevSize": 32,
"code": 59676
},
"setIdx": 0,
"setId": 2,
"setIdx": 1,
"setId": 1,
"iconIdx": 1
},
{
@@ -81,9 +110,9 @@
"code": 59649,
"name": "avatar"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 12
"setIdx": 1,
"setId": 1,
"iconIdx": 2
},
{
"icon": {
@@ -107,9 +136,9 @@
"code": 59653,
"name": "hangup"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 123
"setIdx": 1,
"setId": 1,
"iconIdx": 3
},
{
"icon": {
@@ -133,9 +162,9 @@
"code": 59654,
"name": "chat"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 149
"setIdx": 1,
"setId": 1,
"iconIdx": 4
},
{
"icon": {
@@ -159,9 +188,9 @@
"code": 59650,
"name": "download"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 165
"setIdx": 1,
"setId": 1,
"iconIdx": 5
},
{
"icon": {
@@ -185,9 +214,9 @@
"code": 59655,
"name": "edit"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 186
"setIdx": 1,
"setId": 1,
"iconIdx": 6
},
{
"icon": {
@@ -211,9 +240,9 @@
"code": 59656,
"name": "share-doc"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 207
"setIdx": 1,
"setId": 1,
"iconIdx": 7
},
{
"icon": {
@@ -237,9 +266,9 @@
"code": 59657,
"name": "telephone"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 216
"setIdx": 1,
"setId": 1,
"iconIdx": 8
},
{
"icon": {
@@ -263,9 +292,9 @@
"code": 59652,
"name": "kick"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 243
"setIdx": 1,
"setId": 1,
"iconIdx": 9
},
{
"icon": {
@@ -289,9 +318,9 @@
"code": 59679,
"name": "menu-up"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 257
"setIdx": 1,
"setId": 1,
"iconIdx": 10
},
{
"icon": {
@@ -315,9 +344,9 @@
"code": 59680,
"name": "menu-down"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 258
"setIdx": 1,
"setId": 1,
"iconIdx": 11
},
{
"icon": {
@@ -341,9 +370,9 @@
"code": 59659,
"name": "full-screen"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 351
"setIdx": 1,
"setId": 1,
"iconIdx": 12
},
{
"icon": {
@@ -367,9 +396,9 @@
"code": 59660,
"name": "exit-full-screen"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 352
"setIdx": 1,
"setId": 1,
"iconIdx": 13
},
{
"icon": {
@@ -393,9 +422,9 @@
"code": 59658,
"name": "star-full"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 364
"setIdx": 1,
"setId": 1,
"iconIdx": 14
},
{
"icon": {
@@ -419,9 +448,9 @@
"code": 59661,
"name": "security"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 474
"setIdx": 1,
"setId": 1,
"iconIdx": 15
},
{
"icon": {
@@ -445,9 +474,9 @@
"code": 59662,
"name": "security-locked"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 475
"setIdx": 1,
"setId": 1,
"iconIdx": 16
},
{
"icon": {
@@ -471,9 +500,9 @@
"code": 59663,
"name": "reload"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 483
"setIdx": 1,
"setId": 1,
"iconIdx": 17
},
{
"icon": {
@@ -497,9 +526,9 @@
"code": 59664,
"name": "microphone"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 493
"setIdx": 1,
"setId": 1,
"iconIdx": 18
},
{
"icon": {
@@ -523,9 +552,9 @@
"code": 59665,
"name": "mic-empty"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 494
"setIdx": 1,
"setId": 1,
"iconIdx": 19
},
{
"icon": {
@@ -549,9 +578,9 @@
"code": 59666,
"name": "mic-disabled"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 495
"setIdx": 1,
"setId": 1,
"iconIdx": 20
},
{
"icon": {
@@ -575,9 +604,9 @@
"code": 59678,
"name": "raised-hand"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 540
"setIdx": 1,
"setId": 1,
"iconIdx": 21
},
{
"icon": {
@@ -601,9 +630,9 @@
"code": 59675,
"name": "contactList"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 550
"setIdx": 1,
"setId": 1,
"iconIdx": 22
},
{
"icon": {
@@ -627,9 +656,9 @@
"code": 59667,
"name": "link"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 560
"setIdx": 1,
"setId": 1,
"iconIdx": 23
},
{
"icon": {
@@ -653,9 +682,9 @@
"code": 59668,
"name": "shared-video"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 591
"setIdx": 1,
"setId": 1,
"iconIdx": 24
},
{
"icon": {
@@ -679,9 +708,9 @@
"code": 59669,
"name": "settings"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 666
"setIdx": 1,
"setId": 1,
"iconIdx": 25
},
{
"icon": {
@@ -705,9 +734,9 @@
"code": 59670,
"name": "star"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 718
"setIdx": 1,
"setId": 1,
"iconIdx": 26
},
{
"icon": {
@@ -731,9 +760,9 @@
"code": 59681,
"name": "switch-camera"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 742
"setIdx": 1,
"setId": 1,
"iconIdx": 27
},
{
"icon": {
@@ -757,9 +786,9 @@
"code": 59671,
"name": "share-desktop"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 784
"setIdx": 1,
"setId": 1,
"iconIdx": 28
},
{
"icon": {
@@ -783,9 +812,9 @@
"code": 59672,
"name": "camera"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 799
"setIdx": 1,
"setId": 1,
"iconIdx": 29
},
{
"icon": {
@@ -809,9 +838,9 @@
"code": 59673,
"name": "camera-disabled"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 800
"setIdx": 1,
"setId": 1,
"iconIdx": 30
},
{
"icon": {
@@ -835,9 +864,9 @@
"code": 59674,
"name": "volume"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 822
"setIdx": 1,
"setId": 1,
"iconIdx": 31
},
{
"icon": {
@@ -885,7 +914,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
"iconIdx": 32
},
{
"icon": {
@@ -956,7 +985,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 4
"iconIdx": 33
},
{
"icon": {
@@ -983,7 +1012,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 12
"iconIdx": 34
},
{
"icon": {
@@ -1011,7 +1040,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 13
"iconIdx": 35
},
{
"icon": {
@@ -1039,7 +1068,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 22
"iconIdx": 36
}
],
"height": 1024,

182
images/logo-blue.svg Normal file
View File

@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 744.09448819 1052.3622047"
id="svg3526"
version="1.1"
inkscape:version="0.91 r13725"
inkscape:export-filename="/Users/ystamcheva/Dropbox/Designs/appLogo.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
sodipodi:docname="logo-blue.svg">
<defs
id="defs3528" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.49497475"
inkscape:cx="817.30793"
inkscape:cy="496.00851"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1440"
inkscape:window-height="851"
inkscape:window-x="0"
inkscape:window-y="1"
inkscape:window-maximized="1" />
<metadata
id="metadata3531">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g4181">
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g33">
<path
d="m 257.311,591.057 c 27.544,0 85.707,-16.445 124.179,-27.332 5.595,-1.575 10.81,-3.049 15.549,-4.371 0.767,-0.211 1.51,-0.403 2.213,-0.579 -2.161,-2.139 -5.755,-5.387 -11.612,-10.295 -25.628,-17.369 -49.827,-25.456 -76.146,-25.456 -5.741,0 -11.707,0.352 -18.208,1.088 -22.283,2.535 -40.848,7.845 -49.767,10.39 -4.521,1.296 -5.883,1.683 -7.292,1.683 -2.688,0 -4.997,-1.599 -5.9,-4.069 -0.904,-2.483 -0.13,-5.223 1.969,-6.981 l 0.127,-0.102 c 15.379,-12.883 44.032,-36.866 98.39,-47.582 9.428,-1.853 19.514,-2.796 29.968,-2.796 24.334,0 49.53,5.026 74.869,14.925 34.511,13.474 58.094,30.771 77.062,44.67 10.211,7.489 19.03,13.959 26.705,17.516 1.961,0.912 2.979,1.169 3.453,1.236 0.349,-0.452 1.106,-1.7 2.219,-4.974 0.298,-0.867 2.453,-10.019 -13.007,-62.071 -8.985,-30.217 -19.822,-61.077 -25.465,-74.778 -10.916,-26.509 -8.237,-45.296 -4.877,-56.284 -9.248,3.399 -18.701,8.688 -28.646,15.993 l -0.62,0.458 c -4.969,3.684 -10.031,7.853 -15.482,12.725 -32.074,28.718 -56.104,43.69 -71.455,44.504 l -0.423,0.021 -0.421,-0.036 c -13.524,-1.148 -34.019,-20.834 -42.403,-30.801 -1.743,-1.169 -3.729,-1.699 -6.35,-1.699 -2.632,0 -5.583,0.553 -8.438,1.095 -2.077,0.394 -4.218,0.795 -6.341,1.01 -6.767,0.679 -16.252,2.867 -25.406,4.974 -4.413,1.014 -8.967,2.063 -13.13,2.922 -0.079,0.013 -1.866,0.382 -5.06,1.224 -22.624,6.693 -39.673,14.372 -48.012,21.628 -0.091,0.079 -0.36,0.288 -0.789,0.603 -5.64,4.009 -19.199,15.447 -23.29,34.907 l -0.043,0.162 c -8.541,35.837 4.408,80.28 21.615,105.666 8.093,11.932 16.814,19.376 23.944,20.42 1.775,0.252 3.905,0.386 6.321,0.386 z"
id="path35"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g37">
<path
d="m 383.729,400.995 c 0.549,0.108 1.191,0.162 1.9,0.162 14.785,0 47.804,-21.408 53.912,-31.205 l 0.486,-0.78 0.694,-0.611 c 2.083,-2.056 8.099,-12.885 11.019,-19.367 -31.312,-9.394 -34.767,-26.347 -37.821,-41.41 -0.355,-1.749 -0.667,-3.324 -0.946,-4.732 -0.357,-1.842 -0.731,-3.713 -1.052,-5.159 -46.646,15.471 -60.905,24.154 -68.687,30.611 -4.027,3.345 -6.398,12.858 5.215,39.189 5.932,13.422 26.386,31.591 35.28,33.302 z"
id="path39"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<path
style="fill:#17a0db;fill-opacity:1"
inkscape:connector-curvature="0"
id="path43"
d="m 183.03568,710.19014 c -5.799,-6.834 -8.258,-15.447 -7.293,-25.624 4.105,-49.397 -1.525,-61.33 -4.132,-64.162 -0.629,-0.685 -0.969,-0.685 -1.238,-0.685 -0.101,0 -0.195,0.006 -0.296,0.016 -4.84,1.157 -37.441,23.198 -44.638,89.005 -3.471,31.758 2.611,72.542 7.794,97.348 4.165,-14.646 10.742,-30.779 23.483,-47.384 11.862,-15.444 24.801,-27.623 40.852,-38.298 -4.99,-2.075 -10.346,-5.274 -14.532,-10.216 z" />
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g45">
<path
d="m 485.028,154.141 c -3.896,25.701 -10.239,50.115 -22.077,75.883 12.904,-14.609 20.445,-30.481 22.971,-48.296 1.051,-7.38 2.045,-14.439 -0.894,-27.587 z"
id="path47"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g49">
<path
d="m 413.102,273.797 c 23.135,-20.915 37.22,-55.455 43.078,-75.971 -20.149,19.407 -44.636,29.82 -60.351,36.512 -5.412,2.308 -10.08,4.295 -12.878,5.926 -1.178,0.685 -2.367,1.374 -3.571,2.069 -9.533,5.515 -23.924,13.85 -26.022,18.987 l -0.06,0.167 -0.078,0.165 c -6.529,13.72 -10.208,34.352 -11.387,46.184 15.135,-9.242 30.738,-15.41 43.699,-20.529 12.03,-4.753 22.432,-8.863 27.57,-13.51 z"
id="path51"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g53">
<path
d="m 436.439,291.877 c -0.141,0.357 -0.292,0.695 -0.455,1.017 -3.833,11.143 1.446,26.3 11.227,32.017 2.602,1.522 5.132,2.452 7.559,2.772 0.334,0.014 0.666,0.027 1.001,0.027 7.601,0 13.801,-5.56 18.4,-16.519 2.896,-8.34 3.308,-18.23 1.125,-27.158 -1.696,-6.936 -6.084,-15.215 -8.88,-19.343 -5.219,3.582 -15.533,11.462 -22.615,17.716 -4.946,4.777 -6.733,7.785 -7.362,9.471 z"
id="path55"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g57">
<path
d="m 501.845,575.103 c 8.403,-2.29 15.076,-4.165 19.998,-5.623 -10.137,-7.061 -21.871,-15.846 -37.823,-28.253 -39.096,-30.404 -81.019,-45.826 -124.587,-45.826 -23.861,0 -44.647,4.592 -61.098,10.151 4.101,-0.255 8.271,-0.377 12.554,-0.377 5.088,0 10.42,0.179 15.842,0.541 16.949,1.136 60.616,8.845 100.106,55.931 7.956,9.469 16.507,17.307 40.828,17.307 8.679,0 18.796,-0.967 30.913,-2.959 0.749,-0.209 1.882,-0.518 3.267,-0.892 z"
id="path59"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g61">
<path
d="m 557.268,369.949 c -7.755,-12.043 -17.498,-19.524 -25.41,-19.524 -1.464,0 -2.862,0.258 -4.154,0.765 -4.239,1.672 -10.952,21.042 -7.979,35.126 2.023,9.582 13.67,41.96 19.262,57.52 2.142,5.958 3.18,8.869 3.527,9.951 0.275,0.853 0.67,2.077 1.17,3.621 4.517,13.765 16.111,49.145 19.562,77.793 7.175,-30.554 11.239,-67.36 9.647,-111.409 -0.723,-20.199 -6.274,-39.323 -15.625,-53.843 z"
id="path63"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g65">
<path
d="m 412.08,575.289 c -0.153,-0.2 -0.3,-0.397 -0.445,-0.585 -0.614,0.1 -1.616,0.319 -3.185,0.776 l -0.657,0.197 c -8.011,2.95 -22.707,7.908 -39.694,13.64 -20.387,6.87 -43.477,14.659 -62.808,21.595 -24.596,9.165 -32.572,12.781 -35.073,14.048 -0.454,1.218 -0.963,2.772 -1.53,4.486 -5.817,17.705 -19.139,58.23 -84.831,86.562 13.568,13.744 43.101,38.415 101.24,38.415 5.035,0 10.258,-0.188 15.494,-0.566 43.896,-3.121 85.158,-22.544 116.206,-54.673 28.233,-29.21 44.259,-65.641 44.507,-100.76 -6.871,-0.571 -18.519,-2.281 -29.301,-7.4 -0.125,-0.061 -12.447,-6.002 -19.923,-15.735 z"
id="path67"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g69">
<path
d="m 162.104,639.109 c -0.122,10.334 -1.489,20.245 -2.82,29.907 -0.716,5.216 -1.464,10.615 -2.014,16.041 -0.746,10.914 1.612,14.717 2.659,15.829 0.571,0.629 1.513,1.346 3.536,1.346 1.558,0 3.418,-0.432 5.383,-1.251 19.507,-8.176 38.032,-22.367 46.937,-30.243 -13.668,-6.095 -34.689,-19.26 -53.681,-31.629 z"
id="path71"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
style="fill:#17a0db;fill-opacity:1"
transform="translate(43.272677,-6.8248629)"
id="g73">
<path
d="m 484.26,598.224 c -0.552,7.258 -1.737,20.949 -3.631,31.378 -2.295,12.629 -6.095,23.31 -8.305,28.889 3.945,3.648 7.878,7.228 10.429,9.488 10.265,-6.718 43.961,-32.297 67.208,-90.368 -7.447,5.03 -17.906,9.456 -31.465,13.332 -13.797,3.929 -27.204,6.229 -34.236,7.281 z"
id="path75"
inkscape:connector-curvature="0"
style="fill:#17a0db;fill-opacity:1" />
</g>
<g
transform="translate(43.272677,-6.8248629)"
id="g85"
style="fill:#17a0db;fill-opacity:1">
<path
style="fill:#17a0db;fill-opacity:1"
inkscape:connector-curvature="0"
id="path87"
d="M 627.562,350.519 C 613.5,321.124 593.893,306.283 580.351,297.677 568.965,290.444 555.872,285.188 541.339,282 c -1.622,-10.158 -4.397,-20.542 -8.198,-30.646 24.507,-36.531 30.407,-77.605 17.008,-119.213 C 539.858,100.151 531.868,79.524 524.996,67.213 510.15,40.585 489.58,34.997 474.936,34.997 c -15.09,0 -29.538,6.412 -39.667,17.61 -10.37,11.462 -15.213,26.462 -13.634,42.228 1.349,13.446 -2.178,37.872 -4.519,46.594 -0.04,0.117 -4.202,11.776 -35.168,26.784 -0.746,0.268 -2.332,0.811 -4.773,1.629 -17.812,5.965 -50.913,17.062 -72.963,46.219 -16.847,20.407 -20.985,40.629 -25.766,64.036 -2.858,13.955 -5.846,32.187 -5.105,53.745 -55.35,12.291 -95.226,37.338 -118.609,74.54 -24.203,38.52 -28.402,86.272 -12.468,141.993 l 0.14,0.414 c 0.292,1.014 0.6,2.024 0.921,3.03 -2.718,-0.466 -5.465,-0.858 -8.285,-1.169 -2.469,-0.284 -5.015,-0.42 -7.54,-0.42 -27.636,0 -57.043,17.371 -78.666,46.474 -16.427,22.098 -36.156,61.131 -36.852,121.593 -0.523,44.905 4.279,86.306 14.283,123.054 7.461,27.381 15.784,44.202 18.979,50.09 l 67.793,127.079 31.06,-140.731 c 10.6,-47.935 21.066,-68.283 34.571,-81.732 31.425,18.938 68.541,28.901 107.941,28.901 43.919,0 89.715,-12.667 128.934,-35.662 25.477,-14.954 47.193,-33.324 64.629,-54.658 0.236,0 0.469,0 0.704,0 l 1.857,-0.038 c 10.782,-0.365 25.522,-5.697 40.434,-14.63 12.421,-7.433 31.147,-21.108 49.946,-44.064 18.945,-23.155 34.402,-51.324 45.926,-83.731 13.5,-37.939 21.717,-82.115 24.404,-131.272 3.253,-45.723 -2.078,-83.533 -15.881,-112.384 z m -31.124,109.427 c -2.415,44.805 -9.745,84.66 -21.764,118.441 -9.713,27.302 -22.502,50.739 -38.005,69.69 -26.696,32.611 -52.783,41.355 -55.551,41.465 l -0.22,0 c -2.528,0 -4.012,-1.032 -11.095,-5.988 -1.979,-1.379 -4.969,-3.467 -7.436,-5.075 -14.813,28.811 -39.145,53.701 -70.659,72.185 -32.098,18.824 -69.432,29.202 -105.1,29.202 -42.352,0 -79.532,-13.979 -107.842,-40.493 -38.621,24.556 -61.833,45.044 -80.652,130.273 l -3.562,16.157 -7.787,-14.59 C 84.8,867.621 78.058,854.32 71.708,830.982 62.852,798.444 58.598,761.384 59.071,720.842 c 0.944,-80.909 44.373,-121.518 68.427,-121.518 0.792,0 1.578,0.039 2.328,0.128 22.8,2.551 37.699,12.402 64.745,30.291 2.796,1.853 5.74,3.8 8.843,5.838 9.69,6.36 23.387,14.125 26.835,14.791 6.562,-0.381 12.986,-15.079 14.853,-28.713 0.114,-0.829 0.226,-1.598 0.334,-2.315 0.147,-1.612 0.227,-3.03 0.27,-4.194 -1.144,-0.399 -2.333,-0.869 -3.547,-1.403 l -0.27,-0.091 c -17.012,-5.857 -41.868,-34.625 -54.378,-76.385 -12.081,-42.21 -9.691,-77.122 7.099,-103.83 27.221,-43.328 86.307,-53.515 105.849,-56.861 6.109,-1.214 12.498,-2.351 18.999,-3.378 3.035,-0.762 5.11,-1.399 6.449,-1.978 0.58,-0.403 0.835,-0.833 0.439,-2.403 l 0.53,-0.148 -0.513,0.115 c -0.237,-1.065 -0.565,-2.311 -0.941,-3.753 -0.521,-1.997 -1.103,-4.256 -1.705,-6.936 -6.05,-27.141 -2.962,-49.884 0.863,-68.559 4.297,-21.019 6.678,-32.656 16.605,-44.279 13.152,-18.103 36.803,-26.025 50.953,-30.77 3.948,-1.322 7.359,-2.462 9.331,-3.412 43.344,-20.789 57.145,-42.646 61.091,-57.318 3.127,-11.642 8.084,-42.253 5.931,-63.63 -0.239,-2.425 0.326,-4.421 1.695,-5.935 1.215,-1.341 2.942,-2.104 4.748,-2.104 4.061,0 9.623,0 30.377,64.478 10.949,33.996 2.785,65.868 -24.244,94.74 -0.347,0.375 -0.7,0.742 -1.04,1.095 -0.738,0.76 -1.848,1.909 -1.999,2.326 0.006,0 -0.048,1.042 1.755,4.031 11.425,18.864 17.633,42.323 15.832,59.763 -0.429,4.062 -1.206,7.971 -1.879,11.411 -0.4,1.968 -0.879,4.377 -1.126,6.241 0.111,0 0.226,0 0.347,0 3.088,-0.327 7.867,-0.7 13.628,-0.7 13.556,0 32.969,2.077 48.503,11.951 9.382,5.952 21.255,15.137 29.981,33.404 10.281,21.472 14.096,51.453 11.369,89.114 z" />
</g>
</g>
<path
style="fill:#ffffff"
d=""
id="path3618"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -2,6 +2,7 @@
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="base.html" -->
<script>
window.indexLoadedTime = window.performance.now();

View File

@@ -8,10 +8,11 @@
*/
#import "AppDelegate.h"
#import "RCTAssert.h"
#import "RCTBundleURLProvider.h"
#import "RCTLinkingManager.h"
#import "RCTRootView.h"
#import <React/RCTAssert.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h>
/**
* A <tt>RCTFatalHandler</tt> implementation which swallows JavaScript errors.

View File

@@ -20,6 +20,19 @@
<string>1.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>org.jitsi.meet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>org.jitsi.meet</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>
@@ -43,11 +56,11 @@
</dict>
</dict>
<key>NSCameraUsageDescription</key>
<string></string>
<string>Participate in conferences with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string></string>
<string>Participate in conferences with audio.</string>
<key>UIAppFonts</key>
<array>
<string>jitsi.ttf</string>

View File

@@ -5,8 +5,10 @@
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:beta.meet.jit.si</string>
<string>applinks:beta.hipchat.me</string>
<string>applinks:chaos.hipchat.me</string>
<string>applinks:enso.me</string>
<string>applinks:hipchat.me</string>
<string>applinks:meet.jit.si</string>
</array>
</dict>

View File

@@ -969,7 +969,6 @@
);
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios;
PRODUCT_NAME = "jitsi-meet-react";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -1006,7 +1005,6 @@
);
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios;
PRODUCT_NAME = "jitsi-meet-react";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};

View File

@@ -156,8 +156,8 @@
"kick": "Kick out",
"muted": "Muted",
"domute": "Mute",
"flip": "Flip"
"flip": "Flip",
"remoteControl": "Remote control"
},
"connectionindicator":
{
@@ -316,7 +316,12 @@
"externalInstallationMsg": "You need to install our desktop sharing extension.",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute"
"muteParticipantButton": "Mute",
"remoteControlTitle": "Remote Control",
"remoteControlDeniedMessage": "__user__ rejected your remote control request!",
"remoteControlAllowedMessage": "__user__ accepted your remote control request!",
"remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!",
"remoteControlStopMessage": "The remote control session ended!"
},
"email":
{

View File

@@ -55,7 +55,9 @@ function initCommands() {
APP.conference.toggleScreenSharing.bind(APP.conference),
"video-hangup": () => APP.conference.hangup(),
"email": APP.conference.changeLocalEmail,
"avatar-url": APP.conference.changeLocalAvatarUrl
"avatar-url": APP.conference.changeLocalAvatarUrl,
"remote-control-event": event =>
APP.remoteControl.onRemoteControlAPIEvent(event)
};
Object.keys(commands).forEach(function (key) {
postis.listen(key, args => commands[key](...args));
@@ -94,7 +96,13 @@ function triggerEvent (name, object) {
}
}
export default {
class API {
/**
* Constructs new instance
* @constructor
*/
constructor() { }
/**
* Initializes the APIConnector. Setups message event listeners that will
* receive information from external applications that embed Jitsi Meet.
@@ -108,6 +116,17 @@ export default {
return;
enabled = true;
if(!postis) {
this._initPostis();
}
}
/**
* initializes postis library.
* @private
*/
_initPostis() {
let postisOptions = {
window: target
};
@@ -116,7 +135,7 @@ export default {
= "jitsi_meet_external_api_" + jitsi_meet_external_api_id;
postis = postisInit(postisOptions);
initCommands();
},
}
/**
* Notify external application (if API is enabled) that message was sent.
@@ -124,7 +143,7 @@ export default {
*/
notifySendingChatMessage (body) {
triggerEvent("outgoing-message", {"message": body});
},
}
/**
* Notify external application (if API is enabled) that
@@ -143,7 +162,7 @@ export default {
"incoming-message",
{"from": id, "nick": nick, "message": body, "stamp": ts}
);
},
}
/**
* Notify external application (if API is enabled) that
@@ -152,7 +171,7 @@ export default {
*/
notifyUserJoined (id) {
triggerEvent("participant-joined", {id});
},
}
/**
* Notify external application (if API is enabled) that
@@ -161,7 +180,7 @@ export default {
*/
notifyUserLeft (id) {
triggerEvent("participant-left", {id});
},
}
/**
* Notify external application (if API is enabled) that
@@ -171,7 +190,7 @@ export default {
*/
notifyDisplayNameChanged (id, displayName) {
triggerEvent("display-name-change", {id, displayname: displayName});
},
}
/**
* Notify external application (if API is enabled) that
@@ -181,7 +200,7 @@ export default {
*/
notifyConferenceJoined (room) {
triggerEvent("video-conference-joined", {roomName: room});
},
}
/**
* Notify external application (if API is enabled) that
@@ -191,7 +210,7 @@ export default {
*/
notifyConferenceLeft (room) {
triggerEvent("video-conference-left", {roomName: room});
},
}
/**
* Notify external application (if API is enabled) that
@@ -199,13 +218,23 @@ export default {
*/
notifyReadyToClose () {
triggerEvent("video-ready-to-close", {});
},
}
/**
* Sends remote control event.
* @param {RemoteControlEvent} event the remote control event.
*/
sendRemoteControlEvent(event) {
sendMessage({method: "remote-control-event", params: event});
}
/**
* Removes the listeners.
*/
dispose: function () {
dispose () {
if(enabled)
postis.destroy();
}
};
}
export default new API();

View File

@@ -88,8 +88,10 @@ function changeParticipantNumber(APIInstance, number) {
* @param width width of the iframe
* @param height height of the iframe
* @param parent_node the node that will contain the iframe
* @param filmStripOnly if the value is true only the small videos will be
* visible.
* @param configOverwrite object containing configuration options defined in
* config.js to be overridden.
* @param interfaceConfigOverwrite object containing configuration options
* defined in interface_config.js to be overridden.
* @param noSsl if the value is true https won't be used
* @constructor
*/

View File

@@ -1441,4 +1441,11 @@ UI.hideUserMediaPermissionsGuidanceOverlay = function () {
GumPermissionsOverlay.hide();
};
/**
* Handles user's features changes.
*/
UI.onUserFeaturesChanged = function (user) {
VideoLayout.onUserFeaturesChanged(user);
};
module.exports = UI;

View File

@@ -54,7 +54,7 @@ const IndicatorFontSizes = {
/**
* Returns the available video width.
*/
getAvailableVideoWidth: function () {
getAvailableVideoWidth() {
return window.innerWidth;
},
@@ -70,7 +70,7 @@ const IndicatorFontSizes = {
*
* @param el the element
*/
getTextWidth: function (el) {
getTextWidth(el) {
return (el.clientWidth + 1);
},
@@ -79,7 +79,7 @@ const IndicatorFontSizes = {
*
* @param el the element
*/
getTextHeight: function (el) {
getTextHeight(el) {
return (el.clientHeight + 1);
},
@@ -88,14 +88,14 @@ const IndicatorFontSizes = {
*
* @param id the identifier of the audio element.
*/
playSoundNotification: function (id) {
playSoundNotification(id) {
document.getElementById(id).play();
},
/**
* Escapes the given text.
*/
escapeHtml: function (unsafeText) {
escapeHtml(unsafeText) {
return $('<div/>').text(unsafeText).html();
},
@@ -105,11 +105,11 @@ const IndicatorFontSizes = {
* @param {string} safe string which contains escaped html
* @returns {string} unescaped html string.
*/
unescapeHtml: function (safe) {
unescapeHtml(safe) {
return $('<div />').html(safe).text();
},
imageToGrayScale: function (canvas) {
imageToGrayScale(canvas) {
var context = canvas.getContext('2d');
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
var pixels = imgData.data;
@@ -156,7 +156,7 @@ const IndicatorFontSizes = {
* @param key the tooltip data-i18n key
* @param position the position of the tooltip in relation to the element
*/
setTooltip: function (element, key, position) {
setTooltip(element, key, position) {
if (element !== null) {
element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]);
element.setAttribute('data-i18n', '[content]' + key);
@@ -170,7 +170,7 @@ const IndicatorFontSizes = {
*
* @param element the element to remove the tooltip from
*/
removeTooltip: function (element) {
removeTooltip(element) {
element.removeAttribute('data-tooltip', '');
element.removeAttribute('data-i18n','');
element.removeAttribute('content','');
@@ -183,7 +183,7 @@ const IndicatorFontSizes = {
* @returns {string|*}
* @private
*/
_getTooltipText: function (element) {
_getTooltipText(element) {
let title = element.getAttribute('content');
let shortcut = element.getAttribute('shortcut');
if(shortcut) {
@@ -198,7 +198,7 @@ const IndicatorFontSizes = {
* @param container the container to which new child element will be added
* @param newChild the new element that will be inserted into the container
*/
prependChild: function (container, newChild) {
prependChild(container, newChild) {
var firstChild = container.childNodes[0];
if (firstChild) {
container.insertBefore(newChild, firstChild);
@@ -214,7 +214,7 @@ const IndicatorFontSizes = {
* @returns {boolean} {true} to indicate that the given toolbar button
* is enabled, {false} - otherwise
*/
isButtonEnabled: function (name) {
isButtonEnabled(name) {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1
|| interfaceConfig.MAIN_TOOLBAR_BUTTONS.indexOf(name) !== -1;
},
@@ -226,7 +226,7 @@ const IndicatorFontSizes = {
* @returns {boolean} {true} to indicate that the given setting section
* is enabled, {false} - otherwise
*/
isSettingEnabled: function (name) {
isSettingEnabled(name) {
return interfaceConfig.SETTINGS_SECTIONS.indexOf(name) !== -1;
},
@@ -235,7 +235,7 @@ const IndicatorFontSizes = {
*
* @returns {boolean}
*/
isAuthenticationEnabled: function() {
isAuthenticationEnabled() {
return interfaceConfig.AUTHENTICATION_ENABLE;
},
@@ -303,7 +303,7 @@ const IndicatorFontSizes = {
}
},
hideDisabledButtons: function (mappings) {
hideDisabledButtons(mappings) {
var selector = Object.keys(mappings)
.map(function (buttonName) {
return UIUtil.isButtonEnabled(buttonName)
@@ -313,7 +313,7 @@ const IndicatorFontSizes = {
$(selector).hide();
},
redirect (url) {
redirect(url) {
window.location.href = url;
},
@@ -368,7 +368,7 @@ const IndicatorFontSizes = {
* @param {Object} attrs object with properties
* @returns {String} string of html element attributes
*/
attrsToString: function (attrs) {
attrsToString(attrs) {
return Object.keys(attrs).map(
key => ` ${key}="${attrs[key]}"`
).join(' ');

View File

@@ -64,7 +64,7 @@ ConnectionIndicator.getStringFromArray = function (array) {
ConnectionIndicator.prototype.generateText = function () {
var downloadBitrate, uploadBitrate, packetLoss, i;
if(this.bitrate === null) {
if(!this.bitrate) {
downloadBitrate = "N/A";
uploadBitrate = "N/A";
}
@@ -75,7 +75,7 @@ ConnectionIndicator.prototype.generateText = function () {
this.bitrate.upload? this.bitrate.upload + " Kbps" : "N/A";
}
if(this.packetLoss === null) {
if(!this.packetLoss) {
packetLoss = "N/A";
} else {
@@ -132,7 +132,7 @@ ConnectionIndicator.prototype.generateText = function () {
if (this.showMoreValue) {
var downloadBandwidth, uploadBandwidth, transport;
if (this.bandwidth === null) {
if (!this.bandwidth) {
downloadBandwidth = "N/A";
uploadBandwidth = "N/A";
} else {
@@ -341,7 +341,7 @@ ConnectionIndicator.prototype.updateConnectionStatusIndicator
*/
ConnectionIndicator.prototype.updateConnectionQuality =
function (percent, object) {
if (percent === null) {
if (!percent) {
this.connectionIndicatorContainer.style.display = "none";
this.popover.forceHide();
return;

View File

@@ -3,6 +3,7 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';
import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
@@ -19,8 +20,11 @@ export default class LargeVideoManager {
* @type {Object.<string, LargeContainer>}
*/
this.containers = {};
this.eventEmitter = emitter;
this.state = VIDEO_CONTAINER_TYPE;
// FIXME: We are passing resizeContainer as parameter which is calling
// Container.resize. Probably there's better way to implement this.
this.videoContainer = new VideoContainer(
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
@@ -164,6 +168,7 @@ export default class LargeVideoManager {
// after everything is done check again if there are any pending
// new streams.
this.updateInProcess = false;
this.eventEmitter.emit(UIEvents.LARGE_VIDEO_ID_CHANGED, this.id);
this.scheduleLargeVideoUpdate();
});
}

View File

@@ -29,6 +29,7 @@ function RemoteVideo(user, VideoLayout, emitter) {
this.videoSpanId = `participant_${this.id}`;
SmallVideo.call(this, VideoLayout);
this.hasRemoteVideoMenu = false;
this._supportsRemoteControl = false;
this.addRemoteVideoContainer();
this.connectionIndicator = new ConnectionIndicator(this, this.id);
this.setDisplayName();
@@ -64,7 +65,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
this.initBrowserSpecificProperties();
if (APP.conference.isModerator) {
if (APP.conference.isModerator || this._supportsRemoteControl) {
this.addRemoteVideoMenu();
}
@@ -106,14 +107,6 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
// call the original show, passing its actual this
origShowFunc.call(this.popover);
}.bind(this);
// override popover hide method so we can cleanup click handlers
let origHideFunc = this.popover.forceHide;
this.popover.forceHide = function () {
$(document).off("click", '#mutelink_' + this.id);
$(document).off("click", '#ejectlink_' + this.id);
origHideFunc.call(this.popover);
}.bind(this);
};
/**
@@ -139,38 +132,68 @@ RemoteVideo.prototype._generatePopupContent = function () {
let popupmenuElement = document.createElement('ul');
popupmenuElement.className = 'popupmenu';
popupmenuElement.id = `remote_popupmenu_${this.id}`;
let menuItems = [];
let muteTranslationKey;
let muteClassName;
if (this.isAudioMuted) {
muteTranslationKey = 'videothumbnail.muted';
muteClassName = 'mutelink disabled';
} else {
muteTranslationKey = 'videothumbnail.domute';
muteClassName = 'mutelink';
if(APP.conference.isModerator) {
let muteTranslationKey;
let muteClassName;
if (this.isAudioMuted) {
muteTranslationKey = 'videothumbnail.muted';
muteClassName = 'mutelink disabled';
} else {
muteTranslationKey = 'videothumbnail.domute';
muteClassName = 'mutelink';
}
let muteHandler = this._muteHandler.bind(this);
let kickHandler = this._kickHandler.bind(this);
menuItems = [
{
id: 'mutelink_' + this.id,
handler: muteHandler,
icon: 'icon-mic-disabled',
className: muteClassName,
data: {
i18n: muteTranslationKey
}
}, {
id: 'ejectlink_' + this.id,
handler: kickHandler,
icon: 'icon-kick',
data: {
i18n: 'videothumbnail.kick'
}
}
];
}
let muteHandler = this._muteHandler.bind(this);
let kickHandler = this._kickHandler.bind(this);
let menuItems = [
{
id: 'mutelink_' + this.id,
handler: muteHandler,
icon: 'icon-mic-disabled',
className: muteClassName,
data: {
i18n: muteTranslationKey
}
}, {
id: 'ejectlink_' + this.id,
handler: kickHandler,
icon: 'icon-kick',
data: {
i18n: 'videothumbnail.kick'
}
if(this._supportsRemoteControl) {
let icon, handler, className;
if(APP.remoteControl.controller.getRequestedParticipant()
=== this.id) {
handler = () => {};
className = "requestRemoteControlLink disabled";
icon = "remote-control-spinner fa fa-spinner fa-spin";
} else if(!APP.remoteControl.controller.isStarted()) {
handler = this._requestRemoteControlPermissions.bind(this);
icon = "fa fa-play";
className = "requestRemoteControlLink";
} else {
handler = this._stopRemoteControl.bind(this);
icon = "fa fa-stop";
className = "requestRemoteControlLink";
}
];
menuItems.push({
id: 'remoteControl_' + this.id,
handler,
icon,
className,
data: {
i18n: 'videothumbnail.remoteControl'
}
});
}
menuItems.forEach(el => {
let menuItem = this._generatePopupMenuItem(el);
@@ -182,6 +205,76 @@ RemoteVideo.prototype._generatePopupContent = function () {
return popupmenuElement;
};
/**
* Sets the remote control supported value and initializes or updates the menu
* depending on the remote control is supported or not.
* @param {boolean} isSupported
*/
RemoteVideo.prototype.setRemoteControlSupport = function(isSupported = false) {
if(this._supportsRemoteControl === isSupported) {
return;
}
this._supportsRemoteControl = isSupported;
if(!isSupported) {
return;
}
if(!this.hasRemoteVideoMenu) {
//create menu
this.addRemoteVideoMenu();
} else {
//update the content
this.updateRemoteVideoMenu(this.isAudioMuted, true);
}
};
/**
* Requests permissions for remote control session.
*/
RemoteVideo.prototype._requestRemoteControlPermissions = function () {
APP.remoteControl.controller.requestPermissions(
this.id, this.VideoLayout.getLargeVideoWrapper()).then(result => {
if(result === null) {
return;
}
this.updateRemoteVideoMenu(this.isAudioMuted, true);
APP.UI.messageHandler.openMessageDialog(
"dialog.remoteControlTitle",
(result === false) ? "dialog.remoteControlDeniedMessage"
: "dialog.remoteControlAllowedMessage",
{user: this.user.getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME}
);
if(result === true) {//the remote control permissions has been granted
// pin the controlled participant
let pinnedId = this.VideoLayout.getPinnedId();
if(pinnedId !== this.id) {
this.VideoLayout.handleVideoThumbClicked(this.id);
}
}
}, error => {
logger.error(error);
this.updateRemoteVideoMenu(this.isAudioMuted, true);
APP.UI.messageHandler.openMessageDialog(
"dialog.remoteControlTitle",
"dialog.remoteControlErrorMessage",
{user: this.user.getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME}
);
});
this.updateRemoteVideoMenu(this.isAudioMuted, true);
};
/**
* Stops remote control session.
*/
RemoteVideo.prototype._stopRemoteControl = function () {
// send message about stopping
APP.remoteControl.controller.stop();
this.updateRemoteVideoMenu(this.isAudioMuted, true);
};
RemoteVideo.prototype._muteHandler = function () {
if (this.isAudioMuted)
return;
@@ -244,8 +337,7 @@ RemoteVideo.prototype._generatePopupMenuItem = function (opts = {}) {
linkItem.appendChild(textContent);
linkItem.id = id;
// Delegate event to the document.
$(document).on("click", `#${id}`, handler);
linkItem.onclick = handler;
menuItem.appendChild(linkItem);
return menuItem;

View File

@@ -164,12 +164,20 @@ export class VideoContainer extends LargeContainer {
return getStreamOwnerId(this.stream);
}
constructor (onPlay, emitter) {
/**
* Creates new VideoContainer instance.
* @param resizeContainer {Function} function that takes care of the size
* of the video container.
* @param emitter {EventEmitter} the event emitter that will be used by
* this instance.
*/
constructor (resizeContainer, emitter) {
super();
this.stream = null;
this.videoType = null;
this.localFlipX = true;
this.emitter = emitter;
this.resizeContainer = resizeContainer;
this.isVisible = false;
@@ -199,8 +207,8 @@ export class VideoContainer extends LargeContainer {
this.avatarHeight = $("#dominantSpeakerAvatar").height();
var onPlayCallback = function (event) {
if (typeof onPlay === 'function') {
onPlay(event);
if (typeof resizeContainer === 'function') {
resizeContainer(event);
}
this.wasVideoRendered = true;
}.bind(this);
@@ -336,8 +344,14 @@ export class VideoContainer extends LargeContainer {
* @param {string} videoType video type
*/
setStream (stream, videoType) {
if (this.stream === stream) {
// Handles the use case for the remote participants when the
// videoType is received with delay after turning on/off the
// desktop sharing.
if(this.videoType !== videoType) {
this.videoType = videoType;
this.resizeContainer();
}
return;
} else {
// The stream has changed, so the image will be lost on detach
@@ -361,6 +375,9 @@ export class VideoContainer extends LargeContainer {
this.$video.css({
transform: flipX ? 'scaleX(-1)' : 'none'
});
// Reset the large video background depending on the stream.
this.setLargeVideoBackground(this.avatarDisplayed);
}
/**
@@ -395,8 +412,7 @@ export class VideoContainer extends LargeContainer {
// default background set.
// In order to fix this code we need to introduce video background or
// find a workaround for the video flickering.
$("#largeVideoContainer").css("background",
(show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
this.setLargeVideoBackground(show);
this.$avatar.css("visibility", show ? "visible" : "hidden");
this.avatarDisplayed = show;
@@ -465,4 +481,19 @@ export class VideoContainer extends LargeContainer {
stayOnStage () {
return false;
}
/**
* Sets the large video container background depending on the container
* type and the parameter indicating if an avatar is currently shown on
* large.
*
* @param {boolean} isAvatar - Indicates if the avatar is currently shown
* on the large video.
* @returns {void}
*/
setLargeVideoBackground (isAvatar) {
$("#largeVideoContainer").css("background",
(this.videoType === VIDEO_CONTAINER_TYPE && !isAvatar)
? "#000" : interfaceConfig.DEFAULT_BACKGROUND);
}
}

View File

@@ -406,6 +406,7 @@ var VideoLayout = {
remoteVideo = smallVideo;
else
remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
this._setRemoteControlProperties(user, remoteVideo);
this.addRemoteVideoContainer(id, remoteVideo);
},
@@ -1158,12 +1159,44 @@ var VideoLayout = {
* Sets the flipX state of the local video.
* @param {boolean} true for flipped otherwise false;
*/
setLocalFlipX: function (val) {
setLocalFlipX (val) {
this.localFlipX = val;
},
getEventEmitter: () => {return eventEmitter;}
getEventEmitter() {return eventEmitter;},
/**
* Handles user's features changes.
*/
onUserFeaturesChanged (user) {
let video = this.getSmallVideo(user.getId());
if (!video) {
return;
}
this._setRemoteControlProperties(user, video);
},
/**
* Sets the remote control properties (checks whether remote control
* is supported and executes remoteVideo.setRemoteControlSupport).
* @param {JitsiParticipant} user the user that will be checked for remote
* control support.
* @param {RemoteVideo} remoteVideo the remoteVideo on which the properties
* will be set.
*/
_setRemoteControlProperties (user, remoteVideo) {
APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
remoteVideo.setRemoteControlSupport(result));
},
/**
* Returns the wrapper jquery selector for the largeVideo
* @returns {JQuerySelector} the wrapper jquery selector for the largeVideo
*/
getLargeVideoWrapper() {
return this.getCurrentlyOnLargeContainer().$wrapper;
}
};
export default VideoLayout;

View File

@@ -1,6 +1,6 @@
const logger = require("jitsi-meet-logger").getLogger(__filename);
import { redirect } from '../util/helpers';
import { replace } from '../util/helpers';
/**
* The modules stores information about the URL used to start the conference and
@@ -68,6 +68,6 @@ export default class ConferenceUrl {
*/
reload() {
logger.info("Reloading the conference using URL: " + this.originalURL);
redirect(this.originalURL);
replace(this.originalURL);
}
}

View File

@@ -65,6 +65,12 @@ function showKeyboardShortcutsPanel(show) {
*/
let _shortcuts = {};
/**
* True if the keyboard shortcuts are enabled and false if not.
* @type {boolean}
*/
let enabled = true;
/**
* Maps keycode to character, id of popover for given function and function.
*/
@@ -74,6 +80,9 @@ var KeyboardShortcut = {
var self = this;
window.onkeyup = function(e) {
if(!enabled) {
return;
}
var key = self._getKeyboardKey(e).toUpperCase();
var num = parseInt(key, 10);
if(!($(":focus").is("input[type=text]") ||
@@ -93,6 +102,9 @@ var KeyboardShortcut = {
};
window.onkeydown = function(e) {
if(!enabled) {
return;
}
if(!($(":focus").is("input[type=text]") ||
$(":focus").is("input[type=password]") ||
$(":focus").is("textarea"))) {
@@ -105,6 +117,14 @@ var KeyboardShortcut = {
};
},
/**
* Enables/Disables the keyboard shortcuts.
* @param {boolean} value - the new value.
*/
enable: function (value) {
enabled = value;
},
/**
* Registers a new shortcut.
*

163
modules/keycode/keycode.js Normal file
View File

@@ -0,0 +1,163 @@
/**
* Enumerates the supported keys.
* NOTE: The maps represents physical keys on the keyboard, not chars.
* @readonly
* @enum {string}
*/
export const KEYS = {
BACKSPACE: "backspace" ,
DELETE : "delete",
RETURN : "enter",
TAB : "tab",
ESCAPE : "escape",
UP : "up",
DOWN : "down",
RIGHT : "right",
LEFT : "left",
HOME : "home",
END : "end",
PAGEUP : "pageup",
PAGEDOWN : "pagedown",
F1 : "f1",
F2 : "f2",
F3 : "f3",
F4 : "f4",
F5 : "f5",
F6 : "f6",
F7 : "f7",
F8 : "f8",
F9 : "f9",
F10 : "f10",
F11 : "f11",
F12 : "f12",
META : "command",
CMD_L: "command",
CMD_R: "command",
ALT : "alt",
CONTROL : "control",
SHIFT : "shift",
CAPS_LOCK: "caps_lock", //not supported by robotjs
SPACE : "space",
PRINTSCREEN : "printscreen",
INSERT : "insert",
NUMPAD_0 : "numpad_0",
NUMPAD_1 : "numpad_1",
NUMPAD_2 : "numpad_2",
NUMPAD_3 : "numpad_3",
NUMPAD_4 : "numpad_4",
NUMPAD_5 : "numpad_5",
NUMPAD_6 : "numpad_6",
NUMPAD_7 : "numpad_7",
NUMPAD_8 : "numpad_8",
NUMPAD_9 : "numpad_9",
COMMA: ",",
PERIOD: ".",
SEMICOLON: ";",
QUOTE: "'",
BRACKET_LEFT: "[",
BRACKET_RIGHT: "]",
BACKQUOTE: "`",
BACKSLASH: "\\",
MINUS: "-",
EQUAL: "=",
SLASH: "/"
};
/**
* Mapping between the key codes and keys deined in KEYS.
* The mappings are based on
* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#Specifications
*/
let keyCodeToKey = {
8: KEYS.BACKSPACE,
9: KEYS.TAB,
13: KEYS.RETURN,
16: KEYS.SHIFT,
17: KEYS.CONTROL,
18: KEYS.ALT,
20: KEYS.CAPS_LOCK,
27: KEYS.ESCAPE,
32: KEYS.SPACE,
33: KEYS.PAGEUP,
34: KEYS.PAGEDOWN,
35: KEYS.END,
36: KEYS.HOME,
37: KEYS.LEFT,
38: KEYS.UP,
39: KEYS.RIGHT,
40: KEYS.DOWN,
42: KEYS.PRINTSCREEN,
44: KEYS.PRINTSCREEN,
45: KEYS.INSERT,
46: KEYS.DELETE,
59: KEYS.SEMICOLON,
61: KEYS.EQUAL,
91: KEYS.CMD_L,
92: KEYS.CMD_R,
93: KEYS.CMD_R,
96: KEYS.NUMPAD_0,
97: KEYS.NUMPAD_1,
98: KEYS.NUMPAD_2,
99: KEYS.NUMPAD_3,
100: KEYS.NUMPAD_4,
101: KEYS.NUMPAD_5,
102: KEYS.NUMPAD_6,
103: KEYS.NUMPAD_7,
104: KEYS.NUMPAD_8,
105: KEYS.NUMPAD_9,
112: KEYS.F1,
113: KEYS.F2,
114: KEYS.F3,
115: KEYS.F4,
116: KEYS.F5,
117: KEYS.F6,
118: KEYS.F7,
119: KEYS.F8,
120: KEYS.F9,
121: KEYS.F10,
122: KEYS.F11,
123: KEYS.F12,
124: KEYS.PRINTSCREEN,
173: KEYS.MINUS,
186: KEYS.SEMICOLON,
187: KEYS.EQUAL,
188: KEYS.COMMA,
189: KEYS.MINUS,
190: KEYS.PERIOD,
191: KEYS.SLASH,
192: KEYS.BACKQUOTE,
219: KEYS.BRACKET_LEFT,
220: KEYS.BACKSLASH,
221: KEYS.BRACKET_RIGHT,
222: KEYS.QUOTE,
224: KEYS.META,
229: KEYS.SEMICOLON
};
/**
* Generate codes for digit keys (0-9)
*/
for(let i = 0; i < 10; i++) {
keyCodeToKey[i + 48] = `${i}`;
}
/**
* Generate codes for letter keys (a-z)
*/
for(let i = 0; i < 26; i++) {
let keyCode = i + 65;
keyCodeToKey[keyCode] = String.fromCharCode(keyCode).toLowerCase();
}
/**
* Returns key associated with the keyCode from the passed event.
* @param {KeyboardEvent} event the event
* @returns {KEYS} the key on the keyboard.
*/
export function keyboardEventToKey(event) {
return keyCodeToKey[event.which];
}

View File

@@ -0,0 +1,374 @@
/* global $, JitsiMeetJS, APP */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import * as KeyCodes from "../keycode/keycode";
import {EVENT_TYPES, REMOTE_CONTROL_EVENT_TYPE, PERMISSIONS_ACTIONS}
from "../../service/remotecontrol/Constants";
import RemoteControlParticipant from "./RemoteControlParticipant";
import UIEvents from "../../service/UI/UIEvents";
const ConferenceEvents = JitsiMeetJS.events.conference;
/**
* Extract the keyboard key from the keyboard event.
* @param event {KeyboardEvent} the event.
* @returns {KEYS} the key that is pressed or undefined.
*/
function getKey(event) {
return KeyCodes.keyboardEventToKey(event);
}
/**
* Extract the modifiers from the keyboard event.
* @param event {KeyboardEvent} the event.
* @returns {Array} with possible values: "shift", "control", "alt", "command".
*/
function getModifiers(event) {
let modifiers = [];
if(event.shiftKey) {
modifiers.push("shift");
}
if(event.ctrlKey) {
modifiers.push("control");
}
if(event.altKey) {
modifiers.push("alt");
}
if(event.metaKey) {
modifiers.push("command");
}
return modifiers;
}
/**
* This class represents the controller party for a remote controller session.
* It listens for mouse and keyboard events and sends them to the receiver
* party of the remote control session.
*/
export default class Controller extends RemoteControlParticipant {
/**
* Creates new instance.
*/
constructor() {
super();
this.isCollectingEvents = false;
this.controlledParticipant = null;
this.requestedParticipant = null;
this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
this._userLeftListener = this._onUserLeft.bind(this);
this._largeVideoChangedListener
= this._onLargeVideoIdChanged.bind(this);
}
/**
* Requests permissions from the remote control receiver side.
* @param {string} userId the user id of the participant that will be
* requested.
* @param {JQuerySelector} eventCaptureArea the area that is going to be
* used mouse and keyboard event capture.
* @returns {Promise<boolean>} - resolve values:
* true - accept
* false - deny
* null - the participant has left.
*/
requestPermissions(userId, eventCaptureArea) {
if(!this.enabled) {
return Promise.reject(new Error("Remote control is disabled!"));
}
this.area = eventCaptureArea;// $("#largeVideoWrapper")
logger.log("Requsting remote control permissions from: " + userId);
return new Promise((resolve, reject) => {
const clearRequest = () => {
this.requestedParticipant = null;
APP.conference.removeConferenceListener(
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
permissionsReplyListener);
APP.conference.removeConferenceListener(
ConferenceEvents.USER_LEFT,
onUserLeft);
};
const permissionsReplyListener = (participant, event) => {
let result = null;
try {
result = this._handleReply(participant, event);
} catch (e) {
reject(e);
}
if(result !== null) {
clearRequest();
resolve(result);
}
};
const onUserLeft = (id) => {
if(id === this.requestedParticipant) {
clearRequest();
resolve(null);
}
};
APP.conference.addConferenceListener(
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
permissionsReplyListener);
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
onUserLeft);
this.requestedParticipant = userId;
this._sendRemoteControlEvent(userId, {
type: EVENT_TYPES.permissions,
action: PERMISSIONS_ACTIONS.request
}, e => {
clearRequest();
reject(e);
});
});
}
/**
* Handles the reply of the permissions request.
* @param {JitsiParticipant} participant the participant that has sent the
* reply
* @param {RemoteControlEvent} event the remote control event.
*/
_handleReply(participant, event) {
const remoteControlEvent = event.event;
const userId = participant.getId();
if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE
&& remoteControlEvent.type === EVENT_TYPES.permissions
&& userId === this.requestedParticipant) {
if(remoteControlEvent.action !== PERMISSIONS_ACTIONS.grant) {
this.area = null;
}
switch(remoteControlEvent.action) {
case PERMISSIONS_ACTIONS.grant: {
this.controlledParticipant = userId;
logger.log("Remote control permissions granted to: "
+ userId);
this._start();
return true;
}
case PERMISSIONS_ACTIONS.deny:
return false;
case PERMISSIONS_ACTIONS.error:
throw new Error("Error occurred on receiver side");
default:
throw new Error("Unknown reply received!");
}
} else {
//different message type or another user -> ignoring the message
return null;
}
}
/**
* Handles remote control stopped.
* @param {JitsiParticipant} participant the participant that has sent the
* event
* @param {Object} event EndpointMessage event from the data channels.
* @property {string} type property. The function process only events of
* type REMOTE_CONTROL_EVENT_TYPE
* @property {RemoteControlEvent} event - the remote control event.
*/
_handleRemoteControlStoppedEvent(participant, event) {
if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE
&& event.event.type === EVENT_TYPES.stop
&& participant.getId() === this.controlledParticipant) {
this._stop();
}
}
/**
* Starts processing the mouse and keyboard events. Sets conference
* listeners. Disables keyboard events.
*/
_start() {
logger.log("Starting remote control controller.");
APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
this._largeVideoChangedListener);
APP.conference.addConferenceListener(
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._stopListener);
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
this._userLeftListener);
this.resume();
}
/**
* Disables the keyboatd shortcuts. Starts collecting remote control
* events.
*
* It can be used to resume an active remote control session wchich was
* paused with this.pause().
*/
resume() {
if(!this.enabled || this.isCollectingEvents) {
return;
}
logger.log("Resuming remote control controller.");
this.isCollectingEvents = true;
APP.keyboardshortcut.enable(false);
this.area.mousemove(event => {
const position = this.area.position();
this._sendRemoteControlEvent(this.controlledParticipant, {
type: EVENT_TYPES.mousemove,
x: (event.pageX - position.left)/this.area.width(),
y: (event.pageY - position.top)/this.area.height()
});
});
this.area.mousedown(this._onMouseClickHandler.bind(this,
EVENT_TYPES.mousedown));
this.area.mouseup(this._onMouseClickHandler.bind(this,
EVENT_TYPES.mouseup));
this.area.dblclick(
this._onMouseClickHandler.bind(this, EVENT_TYPES.mousedblclick));
this.area.contextmenu(() => false);
this.area[0].onmousewheel = event => {
this._sendRemoteControlEvent(this.controlledParticipant, {
type: EVENT_TYPES.mousescroll,
x: event.deltaX,
y: event.deltaY
});
};
$(window).keydown(this._onKeyPessHandler.bind(this,
EVENT_TYPES.keydown));
$(window).keyup(this._onKeyPessHandler.bind(this, EVENT_TYPES.keyup));
}
/**
* Stops processing the mouse and keyboard events. Removes added listeners.
* Enables the keyboard shortcuts. Displays dialog to notify the user that
* remote control session has ended.
*/
_stop() {
if(!this.controlledParticipant) {
return;
}
logger.log("Stopping remote control controller.");
APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
this._largeVideoChangedListener);
APP.conference.removeConferenceListener(
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._stopListener);
APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
this._userLeftListener);
this.controlledParticipant = null;
this.pause();
this.area = null;
APP.UI.messageHandler.openMessageDialog(
"dialog.remoteControlTitle",
"dialog.remoteControlStopMessage"
);
}
/**
* Executes this._stop() mehtod:
* Stops processing the mouse and keyboard events. Removes added listeners.
* Enables the keyboard shortcuts. Displays dialog to notify the user that
* remote control session has ended.
*
* In addition:
* Sends stop message to the controlled participant.
*/
stop() {
if(!this.controlledParticipant) {
return;
}
this._sendRemoteControlEvent(this.controlledParticipant, {
type: EVENT_TYPES.stop
});
this._stop();
}
/**
* Pauses the collecting of events and enables the keyboard shortcus. But
* it doesn't removes any other listeners. Basically the remote control
* session will be still active after this.pause(), but no events from the
* controller side will be captured and sent.
*
* You can resume the collecting of the events with this.resume().
*/
pause() {
if(!this.controlledParticipant) {
return;
}
logger.log("Pausing remote control controller.");
this.isCollectingEvents = false;
APP.keyboardshortcut.enable(true);
this.area.off( "mousemove" );
this.area.off( "mousedown" );
this.area.off( "mouseup" );
this.area.off( "contextmenu" );
this.area.off( "dblclick" );
$(window).off( "keydown");
$(window).off( "keyup");
this.area[0].onmousewheel = undefined;
}
/**
* Handler for mouse click events.
* @param {String} type the type of event ("mousedown"/"mouseup")
* @param {Event} event the mouse event.
*/
_onMouseClickHandler(type, event) {
this._sendRemoteControlEvent(this.controlledParticipant, {
type: type,
button: event.which
});
}
/**
* Returns true if the remote control session is started.
* @returns {boolean}
*/
isStarted() {
return this.controlledParticipant !== null;
}
/**
* Returns the id of the requested participant
* @returns {string} this.requestedParticipant.
* NOTE: This id should be the result of JitsiParticipant.getId() call.
*/
getRequestedParticipant() {
return this.requestedParticipant;
}
/**
* Handler for key press events.
* @param {String} type the type of event ("keydown"/"keyup")
* @param {Event} event the key event.
*/
_onKeyPessHandler(type, event) {
this._sendRemoteControlEvent(this.controlledParticipant, {
type: type,
key: getKey(event),
modifiers: getModifiers(event),
});
}
/**
* Calls the stop method if the other side have left.
* @param {string} id - the user id for the participant that have left
*/
_onUserLeft(id) {
if(this.controlledParticipant === id) {
this._stop();
}
}
/**
* Handles changes of the participant displayed on the large video.
* @param {string} id - the user id for the participant that is displayed.
*/
_onLargeVideoIdChanged(id) {
if (!this.controlledParticipant) {
return;
}
if(this.controlledParticipant == id) {
this.resume();
} else {
this.pause();
}
}
}

View File

@@ -0,0 +1,192 @@
/* global APP, JitsiMeetJS, interfaceConfig */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import {DISCO_REMOTE_CONTROL_FEATURE, REMOTE_CONTROL_EVENT_TYPE, EVENT_TYPES,
PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants";
import RemoteControlParticipant from "./RemoteControlParticipant";
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
const ConferenceEvents = JitsiMeetJS.events.conference;
/**
* This class represents the receiver party for a remote controller session.
* It handles "remote-control-event" events and sends them to the
* API module. From there the events can be received from wrapper application
* and executed.
*/
export default class Receiver extends RemoteControlParticipant {
/**
* Creates new instance.
* @constructor
*/
constructor() {
super();
this.controller = null;
this._remoteControlEventsListener
= this._onRemoteControlEvent.bind(this);
this._userLeftListener = this._onUserLeft.bind(this);
this._hangupListener = this._onHangup.bind(this);
}
/**
* Enables / Disables the remote control
* @param {boolean} enabled the new state.
*/
enable(enabled) {
if(this.enabled === enabled) {
return;
}
this.enabled = enabled;
if(enabled === true) {
logger.log("Remote control receiver enabled.");
// Announce remote control support.
APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
APP.conference.addConferenceListener(
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._remoteControlEventsListener);
APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
this._hangupListener);
} else {
logger.log("Remote control receiver disabled.");
this._stop(true);
APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
APP.conference.removeConferenceListener(
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
this._remoteControlEventsListener);
APP.conference.removeListener(
JitsiMeetConferenceEvents.BEFORE_HANGUP,
this._hangupListener);
}
}
/**
* Removes the listener for ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED
* events. Sends stop message to the wrapper application. Optionally
* displays dialog for informing the user that remote control session
* ended.
* @param {boolean} dontShowDialog - if true the dialog won't be displayed.
*/
_stop(dontShowDialog = false) {
if(!this.controller) {
return;
}
logger.log("Remote control receiver stop.");
this.controller = null;
APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
this._userLeftListener);
APP.API.sendRemoteControlEvent({
type: EVENT_TYPES.stop
});
if(!dontShowDialog) {
APP.UI.messageHandler.openMessageDialog(
"dialog.remoteControlTitle",
"dialog.remoteControlStopMessage"
);
}
}
/**
* Calls this._stop() and sends stop message to the controller participant
*/
stop() {
if(!this.controller) {
return;
}
this._sendRemoteControlEvent(this.controller, {
type: EVENT_TYPES.stop
});
this._stop();
}
/**
* Listens for data channel EndpointMessage events. Handles only events of
* type remote control. Sends "remote-control-event" events to the API
* module.
* @param {JitsiParticipant} participant the controller participant
* @param {Object} event EndpointMessage event from the data channels.
* @property {string} type property. The function process only events of
* type REMOTE_CONTROL_EVENT_TYPE
* @property {RemoteControlEvent} event - the remote control event.
*/
_onRemoteControlEvent(participant, event) {
if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE) {
const remoteControlEvent = event.event;
if(this.controller === null
&& remoteControlEvent.type === EVENT_TYPES.permissions
&& remoteControlEvent.action === PERMISSIONS_ACTIONS.request) {
remoteControlEvent.userId = participant.getId();
remoteControlEvent.userJID = participant.getJid();
remoteControlEvent.displayName = participant.getDisplayName()
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
remoteControlEvent.screenSharing
= APP.conference.isSharingScreen;
} else if(this.controller !== participant.getId()) {
return;
} else if(remoteControlEvent.type === EVENT_TYPES.stop) {
this._stop();
return;
}
APP.API.sendRemoteControlEvent(remoteControlEvent);
} else if(event.type === REMOTE_CONTROL_EVENT_TYPE) {
logger.log("Remote control event is ignored because remote "
+ "control is disabled", event);
}
}
/**
* Handles remote control permission events received from the API module.
* @param {String} userId the user id of the participant related to the
* event.
* @param {PERMISSIONS_ACTIONS} action the action related to the event.
*/
_onRemoteControlPermissionsEvent(userId, action) {
if(action === PERMISSIONS_ACTIONS.grant) {
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
this._userLeftListener);
this.controller = userId;
logger.log("Remote control permissions granted to: " + userId);
if(!APP.conference.isSharingScreen) {
APP.conference.toggleScreenSharing();
APP.conference.screenSharingPromise.then(() => {
if(APP.conference.isSharingScreen) {
this._sendRemoteControlEvent(userId, {
type: EVENT_TYPES.permissions,
action: action
});
} else {
this._sendRemoteControlEvent(userId, {
type: EVENT_TYPES.permissions,
action: PERMISSIONS_ACTIONS.error
});
}
}).catch(() => {
this._sendRemoteControlEvent(userId, {
type: EVENT_TYPES.permissions,
action: PERMISSIONS_ACTIONS.error
});
});
return;
}
}
this._sendRemoteControlEvent(userId, {
type: EVENT_TYPES.permissions,
action: action
});
}
/**
* Calls the stop method if the other side have left.
* @param {string} id - the user id for the participant that have left
*/
_onUserLeft(id) {
if(this.controller === id) {
this._stop();
}
}
/**
* Handles hangup events. Disables the receiver.
*/
_onHangup() {
this.enable(false);
}
}

View File

@@ -0,0 +1,89 @@
/* global APP, config */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import Controller from "./Controller";
import Receiver from "./Receiver";
import {EVENT_TYPES, DISCO_REMOTE_CONTROL_FEATURE}
from "../../service/remotecontrol/Constants";
/**
* Implements the remote control functionality.
*/
class RemoteControl {
/**
* Constructs new instance. Creates controller and receiver properties.
* @constructor
*/
constructor() {
this.controller = new Controller();
this.receiver = new Receiver();
this.enabled = false;
this.initialized = false;
}
/**
* Initializes the remote control - checks if the remote control should be
* enabled or not, initializes the API module.
*/
init() {
if(config.disableRemoteControl || this.initialized
|| !APP.conference.isDesktopSharingEnabled) {
return;
}
logger.log("Initializing remote control.");
this.initialized = true;
APP.API.init({
forceEnable: true,
});
this.controller.enable(true);
if(this.enabled) { // supported message came before init.
this._onRemoteControlSupported();
}
}
/**
* Handles remote control events from the API module. Currently only events
* with type = EVENT_TYPES.supported or EVENT_TYPES.permissions
* @param {RemoteControlEvent} event the remote control event.
*/
onRemoteControlAPIEvent(event) {
switch(event.type) {
case EVENT_TYPES.supported:
this._onRemoteControlSupported();
break;
case EVENT_TYPES.permissions:
this.receiver._onRemoteControlPermissionsEvent(
event.userId, event.action);
break;
}
}
/**
* Handles API event for support for executing remote control events into
* the wrapper application.
*/
_onRemoteControlSupported() {
logger.log("Remote Control supported.");
if(!config.disableRemoteControl) {
this.enabled = true;
if(this.initialized) {
this.receiver.enable(true);
}
} else {
logger.log("Remote Control disabled.");
}
}
/**
* Checks whether the passed user supports remote control or not
* @param {JitsiParticipant} user the user to be tested
* @returns {Promise<boolean>} the promise will be resolved with true if
* the user supports remote control and with false if not.
*/
checkUserRemoteControlSupport(user) {
return user.getFeatures().then(features =>
features.has(DISCO_REMOTE_CONTROL_FEATURE), () => false
);
}
}
export default new RemoteControl();

View File

@@ -0,0 +1,42 @@
/* global APP */
const logger = require("jitsi-meet-logger").getLogger(__filename);
import {REMOTE_CONTROL_EVENT_TYPE}
from "../../service/remotecontrol/Constants";
export default class RemoteControlParticipant {
/**
* Creates new instance.
*/
constructor() {
this.enabled = false;
}
/**
* Enables / Disables the remote control
* @param {boolean} enabled the new state.
*/
enable(enabled) {
this.enabled = enabled;
}
/**
* Sends remote control event to other participant trough data channel.
* @param {RemoteControlEvent} event the remote control event.
* @param {Function} onDataChannelFail handler for data channel failure.
*/
_sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) {
if(!this.enabled || !to) {
logger.warn("Remote control: Skip sending remote control event."
+ " Params:", this.enable, to);
return;
}
try{
APP.conference.sendEndpointMessage(to,
{type: REMOTE_CONTROL_EVENT_TYPE, event});
} catch (e) {
logger.error("Failed to send EndpointMessage via the datachannels",
e);
onDataChannelFail(e);
}
}
}

View File

@@ -73,10 +73,6 @@ class TokenData{
this.jwt = jwt;
//External API settings
this.externalAPISettings = {
forceEnable: true
};
this._decode();
// Use JWT param as token if there is not other token set and if the
// iss field is not anonymous. If you want to pass data with JWT token

View File

@@ -2,12 +2,13 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
/**
* Create deferred object.
*
* @returns {{promise, resolve, reject}}
*/
export function createDeferred () {
let deferred = {};
export function createDeferred() {
const deferred = {};
deferred.promise = new Promise(function (resolve, reject) {
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
@@ -18,63 +19,58 @@ export function createDeferred () {
/**
* Reload page.
*/
export function reload () {
export function reload() {
window.location.reload();
}
/**
* Redirects to new URL.
* Redirects to a specific new URL by replacing the current location (in the
* history).
*
* @param {string} url the URL pointing to the location where the user should
* be redirected to.
*/
export function redirect (url) {
export function replace(url) {
window.location.replace(url);
}
/**
* Prints the error and reports it to the global error handler.
*
* @param e {Error} the error
* @param msg {string} [optional] the message printed in addition to the error
*/
export function reportError (e, msg = "") {
export function reportError(e, msg = "") {
logger.error(msg, e);
if(window.onerror)
window.onerror(msg, null, null,
null, e);
window.onerror && window.onerror(msg, null, null, null, e);
}
/**
* Creates a debounced function that delays invoking func until after wait
* milliseconds have elapsed since the last time the debounced
* function was invoked
* milliseconds have elapsed since the last time the debounced function was
* invoked.
*
* @param fn
* @param wait
* @param options
* @returns {function(...[*])}
*/
export function debounce(fn, wait = 0, options = {}) {
let leading = options.leading || false;
let trailing = true;
let isCalled = false;
if (typeof options.trailing !== 'undefined') {
trailing = options.trailing;
}
const leading = options.leading || false;
const trailing
= (typeof options.trailing === 'undefined') || options.trailing;
let called = false;
return (...args) => {
if(!isCalled) {
if (leading) {
fn(...args);
}
if (!called) {
leading && fn(...args);
setTimeout(() => {
isCalled = false;
if (trailing) {
fn(...args);
}
called = false;
trailing && fn(...args);
}, wait);
isCalled = true;
called = true;
}
};
}

View File

@@ -34,8 +34,9 @@
"postis": "^2.2.0",
"react": "15.4.2",
"react-dom": "15.4.2",
"react-native": "0.40.0",
"react-native-keep-awake": "^1.0.7",
"react-native": "0.41.2",
"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-webrtc": "jitsi/react-native-webrtc",
@@ -57,14 +58,17 @@
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-stage-1": "^6.16.0",
"clean-css": "*",
"clean-css": "^3.0.0",
"css-loader": "*",
"eslint": "^3.13.1",
"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": "*",
"file-loader": "^0.10.0",
"flow-bin": "^0.37.0",
"haste-resolver-webpack-plugin": "^0.2.2",
"imports-loader": "*",
"jshint": "2.9.4",
@@ -78,7 +82,7 @@
},
"license": "Apache-2.0",
"scripts": {
"lint": "jshint . && eslint .",
"lint": "jshint . && eslint . && flow",
"validate": "npm ls"
},
"pre-commit": [

View File

@@ -0,0 +1,13 @@
-- Copyright (c) 2015 &yet <https://andyet.com>
-- https://github.com/otalk/mod_muc_allowners/blob/9a86266a25ed32ade150742cc79f5a1669765a8f/mod_muc_allowners.lua
--
-- Used under the terms of the MIT License
-- https://github.com/otalk/mod_muc_allowners/blob/9a86266a25ed32ade150742cc79f5a1669765a8f/LICENSE
local muc_service = module:depends("muc");
local room_mt = muc_service.room_mt;
room_mt.get_affiliation = function (room, jid)
return "owner";
end

View File

@@ -0,0 +1,21 @@
--- muc.lib.lua 2016-10-26 18:26:53.432377291 +0000
+++ muc.lib.lua 2016-10-26 18:41:40.754426072 +0000
@@ -1256,15 +1256,16 @@
if actor == true then
actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
else
+ local actor_affiliation = self:get_affiliation(actor);
+
-- Can't do anything to other owners or admins
local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
- if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
+ if (occupant_affiliation == "owner" and actor_affiliation ~= "owner") or (occupant_affiliation == "admin" and actor_affiliation ~= "admin" and actor_affiliation ~= "owner") then
return nil, "cancel", "not-allowed";
end
-- If you are trying to give or take moderator role you need to be an owner or admin
if occupant.role == "moderator" or role == "moderator" then
- local actor_affiliation = self:get_affiliation(actor);
if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
return nil, "cancel", "not-allowed";
end

View File

@@ -1,12 +1,16 @@
module.exports = {
'extends': '../.eslintrc.js',
'parser': 'babel-eslint',
'parserOptions': {
'ecmaFeatures': {
'jsx': true
}
},
'plugins': [
// ESLint's rule no-duplicate-imports does not understand Flow's import
// type. Fortunately, eslint-plugin-import understands Flow's import
// type.
'import',
'jsdoc',
'react',
'react-native'
@@ -274,7 +278,6 @@ module.exports = {
'no-confusing-arrow': 2,
'no-const-assign': 2,
'no-dupe-class-members': 2,
'no-duplicate-imports': 2,
'no-new-symbol': 2,
'no-restricted-imports': 0,
'no-this-before-super': 2,
@@ -299,6 +302,8 @@ module.exports = {
'template-curly-spacing': 2,
'yield-star-spacing': 2,
'import/no-duplicates': 2,
// JsDoc plugin rules group. The following rules are in addition to
// valid-jsdoc rule.
'jsdoc/check-param-names': 0,

View File

@@ -4,8 +4,8 @@ import { loadConfig, setConfig } from '../base/lib-jitsi-meet';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
import {
_getRoomAndDomainFromUrlString,
_getRouteToRender,
_parseURIString,
init
} from './functions';
import './reducer';
@@ -31,9 +31,10 @@ export function appInit() {
*/
export function appNavigate(urlOrRoom) {
return (dispatch, getState) => {
const oldDomain = getDomain(getState());
const state = getState();
const oldDomain = getDomain(state);
const { domain, room } = _getRoomAndDomainFromUrlString(urlOrRoom);
const { domain, room } = _parseURIString(urlOrRoom);
// TODO Kostiantyn Tsaregradskyi: We should probably detect if user is
// currently in a conference and ask her if she wants to close the
@@ -41,13 +42,7 @@ export function appNavigate(urlOrRoom) {
// domain.
if (typeof domain === 'undefined' || oldDomain === domain) {
// If both domain and room vars became undefined, that means we're
// actually dealing with just room name and not with URL.
dispatch(
_setRoomAndNavigate(
typeof room === 'undefined' && typeof domain === 'undefined'
? urlOrRoom
: room));
dispatchSetRoomAndNavigate();
} else if (oldDomain !== domain) {
// Update domain without waiting for config to be loaded to prevent
// race conditions when we will start to load config multiple times.
@@ -59,7 +54,8 @@ export function appNavigate(urlOrRoom) {
loadConfig(`https://${domain}`)
.then(
config => configLoaded(/* err */ undefined, config),
err => configLoaded(err, /* config */ undefined));
err => configLoaded(err, /* config */ undefined))
.then(dispatchSetRoomAndNavigate);
}
/**
@@ -83,11 +79,22 @@ export function appNavigate(urlOrRoom) {
return;
}
// We set room name only here to prevent race conditions on app
// start to not make app re-render conference page for two times.
dispatch(setRoom(room));
dispatch(setConfig(config));
_navigate(getState());
}
/**
* Dispatches _setRoomAndNavigate in the Redux store.
*
* @returns {void}
*/
function dispatchSetRoomAndNavigate() {
// If both domain and room vars became undefined, that means we're
// actually dealing with just room name and not with URL.
dispatch(
_setRoomAndNavigate(
typeof room === 'undefined' && typeof domain === 'undefined'
? urlOrRoom
: room));
}
};
}
@@ -125,9 +132,10 @@ export function appWillUnmount(app) {
}
/**
* Navigates to route corresponding to current room name.
* Navigates to a route in accord with a specific Redux state.
*
* @param {Object} state - Redux state.
* @param {Object} state - The Redux state which determines/identifies the route
* to navigate to.
* @private
* @returns {void}
*/
@@ -147,15 +155,7 @@ function _navigate(state) {
*/
function _setRoomAndNavigate(newRoom) {
return (dispatch, getState) => {
const oldRoom = getState()['features/base/conference'].room;
dispatch(setRoom(newRoom));
const state = getState();
const room = state['features/base/conference'].room;
if (room !== oldRoom) {
_navigate(state);
}
_navigate(getState());
};
}

View File

@@ -1,10 +1,14 @@
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { compose, createStore } from 'redux';
import Thunk from 'redux-thunk';
import {
localParticipantJoined,
localParticipantLeft
} from '../../base/participants';
import { RouteRegistry } from '../../base/react';
import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
import {
appNavigate,
@@ -34,7 +38,7 @@ export class AbstractApp extends Component {
}
/**
* Initializes a new App instance.
* Initializes a new AbstractApp instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
@@ -44,11 +48,18 @@ export class AbstractApp extends Component {
this.state = {
/**
* The Route rendered by this App.
* The Route rendered by this AbstractApp.
*
* @type {Route}
*/
route: undefined
route: undefined,
/**
* The Redux store used by this AbstractApp.
*
* @type {Store}
*/
store: this._maybeCreateStore(props)
};
}
@@ -59,7 +70,7 @@ export class AbstractApp extends Component {
* @inheritdoc
*/
componentWillMount() {
const dispatch = this.props.store.dispatch;
const dispatch = this._getStore().dispatch;
dispatch(appWillMount(this));
@@ -68,6 +79,32 @@ export class AbstractApp extends Component {
this._openURL(this._getDefaultURL());
}
/**
* Notifies this mounted React Component that it will receive new props.
* Makes sure that this AbstractApp has a Redux store to use.
*
* @inheritdoc
* @param {Object} nextProps - The read-only React Component props that this
* instance will receive.
* @returns {void}
*/
componentWillReceiveProps(nextProps) {
// The consumer of this AbstractApp did not provide a Redux store.
if (typeof nextProps.store === 'undefined'
// The consumer of this AbstractApp did provide a Redux store
// before. Which means that the consumer changed their mind. In
// such a case this instance should create its own internal
// Redux store. If the consumer did not provide a Redux store
// before, then this instance is using its own internal Redux
// store already.
&& typeof this.props.store !== 'undefined') {
this.setState({
store: this._maybeCreateStore(nextProps)
});
}
}
/**
* Dispose lib-jitsi-meet and remove local participant when component is
* going to be unmounted.
@@ -75,7 +112,7 @@ export class AbstractApp extends Component {
* @inheritdoc
*/
componentWillUnmount() {
const dispatch = this.props.store.dispatch;
const dispatch = this._getStore().dispatch;
dispatch(localParticipantLeft());
@@ -93,7 +130,7 @@ export class AbstractApp extends Component {
if (route) {
return (
<Provider store = { this.props.store }>
<Provider store = { this._getStore() }>
{
this._createElement(route.component)
}
@@ -141,6 +178,36 @@ export class AbstractApp extends Component {
return React.createElement(component, { ...thisProps, ...props });
}
/**
* Initializes a new Redux store instance suitable for use by
* this AbstractApp.
*
* @private
* @returns {Store} - A new Redux store instance suitable for use by
* this AbstractApp.
*/
_createStore() {
// Create combined reducer from all reducers in ReducerRegistry.
const reducer = ReducerRegistry.combineReducers();
// Apply all registered middleware from the MiddlewareRegistry and
// additional 3rd party middleware:
// - Thunk - allows us to dispatch async actions easily. For more info
// @see https://github.com/gaearon/redux-thunk.
let middleware = MiddlewareRegistry.applyMiddleware(Thunk);
// Try to enable Redux DevTools Chrome extension in order to make it
// available for the purposes of facilitating development.
let devToolsExtension;
if (typeof window === 'object'
&& (devToolsExtension = window.devToolsExtension)) {
middleware = compose(middleware, devToolsExtension());
}
return createStore(reducer, middleware);
}
/**
* Gets the default URL to be opened when this App mounts.
*
@@ -188,6 +255,22 @@ export class AbstractApp extends Component {
return 'https://meet.jit.si';
}
/**
* Gets the Redux store used by this AbstractApp.
*
* @protected
* @returns {Store} - The Redux store used by this AbstractApp.
*/
_getStore() {
let store = this.state.store;
if (typeof store === 'undefined') {
store = this.props.store;
}
return store;
}
/**
* Gets a Location object from the window with information about the current
* location of the document. Explicitly defined to allow extenders to
@@ -203,6 +286,30 @@ export class AbstractApp extends Component {
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
* read-only React Component props.
*
* @param {Object} props - The read-only React Component props that will
* eventually be received by this AbstractApp.
* @private
* @returns {Store} - The Redux store to be used by this AbstractApp.
*/
_maybeCreateStore(props) {
// The application Jitsi Meet is architected with Redux. However, I do
// not want consumers of the App React Component to be forced into
// dealing with Redux. If the consumer did not provide an external Redux
// store, utilize an internal Redux store.
let store = props.store;
if (typeof store === 'undefined') {
store = this._createStore();
}
return store;
}
/**
* Navigates to a specific Route.
*
@@ -210,6 +317,10 @@ export class AbstractApp extends Component {
* @returns {void}
*/
_navigate(route) {
if (RouteRegistry.areRoutesEqual(this.state.route, route)) {
return;
}
let nextState = {
...this.state,
route
@@ -223,13 +334,7 @@ export class AbstractApp extends Component {
// (2) A replace function would be provided to the Route in case it
// chose to redirect to another path.
this._onRouteEnter(route, nextState, pathname => {
// FIXME In order to minimize the modifications related to the
// removal of react-router, the Web implementation is provided
// bellow because the replace function is used on Web only at the
// time of this writing. Provide a platform-agnostic implementation.
// It should likely find the best Route matching the specified
// pathname and navigate to it.
window.location.pathname = pathname;
this._openURL(pathname);
// Do not proceed with the route because it chose to redirect to
// another path.
@@ -264,6 +369,6 @@ export class AbstractApp extends Component {
* @returns {void}
*/
_openURL(url) {
this.props.store.dispatch(appNavigate(url));
this._getStore().dispatch(appNavigate(url));
}
}

View File

@@ -4,6 +4,7 @@ import { Linking } from 'react-native';
import { Platform } from '../../base/react';
import '../../audio-mode';
import '../../full-screen';
import '../../wake-lock';
import { AbstractApp } from './AbstractApp';

View File

@@ -14,6 +14,27 @@ export class App extends AbstractApp {
*/
static propTypes = AbstractApp.propTypes
/**
* Initializes a new App instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
...this.state,
/**
* The context root of window.location i.e. this Web App.
*
* @type {string}
*/
windowLocationContextRoot: this._getWindowLocationContextRoot()
};
}
/**
* Inits the app before component will mount.
*
@@ -22,7 +43,7 @@ export class App extends AbstractApp {
componentWillMount(...args) {
super.componentWillMount(...args);
this.props.store.dispatch(appInit());
this._getStore().dispatch(appInit());
}
/**
@@ -35,6 +56,22 @@ export class App extends AbstractApp {
return window.location;
}
/**
* Gets the context root of this Web App from window.location.
*
* @private
* @returns {string} The context root of window.location i.e. this Web App.
*/
_getWindowLocationContextRoot() {
const pathname = this._getWindowLocation().pathname;
const contextRootEndIndex = pathname.lastIndexOf('/');
return (
contextRootEndIndex === -1
? '/'
: pathname.substring(0, contextRootEndIndex + 1));
}
/**
* Navigates to a specific Route (via platform-specific means).
*
@@ -44,7 +81,7 @@ export class App extends AbstractApp {
*/
_navigate(route) {
let path = route.path;
const store = this.props.store;
const store = this._getStore();
// The syntax :room bellow is defined by react-router. It "matches a URL
// segment up to the next /, ?, or #. The matched string is called a
@@ -53,6 +90,7 @@ export class App extends AbstractApp {
= path.replace(
/:room/g,
store.getState()['features/base/conference'].room);
path = this._routePath2WindowLocationPathname(path);
// Navigate to the specified Route.
const windowLocation = this._getWindowLocation();
@@ -69,4 +107,22 @@ export class App extends AbstractApp {
windowLocation.pathname = path;
}
}
/**
* Converts a specific Route path to a window.location.pathname.
*
* @param {string} path - A Route path to be converted to/represeted as a
* window.location.pathname.
* @private
* @returns {string} A window.location.pathname-compatible representation of
* the specified Route path.
*/
_routePath2WindowLocationPathname(path) {
let pathname = this.state.windowLocationContextRoot;
pathname.endsWith('/') || (pathname += '/');
pathname += path.startsWith('/') ? path.substring(1) : path;
return pathname;
}
}

View File

@@ -1,25 +1,136 @@
import { isRoomValid } from '../base/conference';
import { RouteRegistry } from '../base/navigator';
import { RouteRegistry } from '../base/react';
import { Conference } from '../conference';
import { WelcomePage } from '../welcome';
/**
* The RegExp pattern of the authority of a URI.
*
* @private
* @type {string}
*/
const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)';
/**
* The RegExp pattern of the path of a URI.
*
* @private
* @type {string}
*/
const _URI_PATH_PATTERN = '([^?#]*)';
/**
* The RegExp patther of the protocol of a URI.
*
* @private
* @type {string}
*/
const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
/**
* Fixes the hier-part of a specific URI (string) so that the URI is well-known.
* For example, certain Jitsi Meet deployments are not conventional but it is
* possible to translate their URLs into conventional.
*
* @param {string} uri - The URI (string) to fix the hier-part of.
* @private
* @returns {string}
*/
function _fixURIStringHierPart(uri) {
// Rewrite the specified URL in order to handle special cases such as
// hipchat.com and enso.me which do not follow the common pattern of most
// Jitsi Meet deployments.
// hipchat.com
let regex
= new RegExp(
`^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
'gi');
let match = regex.exec(uri);
if (!match) {
// enso.me
regex
= new RegExp(
`^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
'gi');
match = regex.exec(uri);
}
if (match) {
/* eslint-disable no-param-reassign, prefer-template */
uri
= match[1] /* protocol */
+ '//enso.hipchat.me/'
+ uri.substring(regex.lastIndex); /* room (name) */
/* eslint-enable no-param-reassign, prefer-template */
}
return uri;
}
/**
* Fixes the scheme part of a specific URI (string) so that it contains a
* well-known scheme such as HTTP(S). For example, the mobile app implements an
* app-specific URI scheme in addition to Universal Links. The app-specific
* scheme may precede or replace the well-known scheme. In such a case, dealing
* with the app-specific scheme only complicates the logic and it is simpler to
* get rid of it (by translating the app-specific scheme into a well-known
* scheme).
*
* @param {string} uri - The URI (string) to fix the scheme of.
* @private
* @returns {string}
*/
function _fixURIStringScheme(uri) {
const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi');
const match = regex.exec(uri);
if (match) {
// As an implementation convenience, pick up the last scheme and make
// sure that it is a well-known one.
let protocol = match[match.length - 1].toLowerCase();
if (protocol !== 'http:' && protocol !== 'https:') {
protocol = 'https:';
}
/* eslint-disable no-param-reassign */
uri = uri.substring(regex.lastIndex);
if (uri.startsWith('//')) {
// The specified URL was not a room name only, it contained an
// authority.
uri = protocol + uri;
}
/* eslint-enable no-param-reassign */
}
return uri;
}
/**
* Gets room name and domain from URL object.
*
* @param {URL} url - URL object.
* @private
* @returns {{
* domain: (string|undefined),
* room: (string|undefined)
* }}
* domain: (string|undefined),
* room: (string|undefined)
* }}
*/
function _getRoomAndDomainFromUrlObject(url) {
function _getRoomAndDomainFromURLObject(url) {
let domain;
let room;
if (url) {
domain = url.hostname;
room = url.pathname.substr(1);
// The room (name) is the last component of pathname.
room = url.pathname;
room = room.substring(room.lastIndexOf('/') + 1);
// Convert empty string to undefined to simplify checks.
if (room === '') {
@@ -36,44 +147,6 @@ function _getRoomAndDomainFromUrlObject(url) {
};
}
/**
* Gets conference room name and connection domain from URL.
*
* @param {(string|undefined)} url - URL.
* @returns {{
* domain: (string|undefined),
* room: (string|undefined)
* }}
*/
export function _getRoomAndDomainFromUrlString(url) {
// Rewrite the specified URL in order to handle special cases such as
// hipchat.com and enso.me which do not follow the common pattern of most
// Jitsi Meet deployments.
if (typeof url === 'string') {
// hipchat.com
let regex = /^(https?):\/\/hipchat.com\/video\/call\//gi;
let match = regex.exec(url);
if (!match) {
// enso.me
regex = /^(https?):\/\/enso\.me\/(?:call|meeting)\//gi;
match = regex.exec(url);
}
if (match && match.length > 1) {
/* eslint-disable no-param-reassign, prefer-template */
url
= match[1] /* URL protocol */
+ '://enso.hipchat.me/'
+ url.substring(regex.lastIndex);
/* eslint-enable no-param-reassign, prefer-template */
}
}
return _getRoomAndDomainFromUrlObject(_urlStringToObject(url));
}
/**
* Determines which route is to be rendered in order to depict a specific Redux
* store.
@@ -94,24 +167,74 @@ export function _getRouteToRender(stateOrGetState) {
}
/**
* Parses a string into a URL (object).
* Parses a specific URI which (supposedly) references a Jitsi Meet resource
* (location).
*
* @param {(string|undefined)} url - The URL to parse.
* @private
* @returns {URL}
* @param {(string|undefined)} uri - The URI to parse which (supposedly)
* references a Jitsi Meet resource (location).
* @returns {{
* domain: (string|undefined),
* room: (string|undefined)
* }}
*/
function _urlStringToObject(url) {
let urlObj;
export function _parseURIString(uri) {
let obj;
if (url) {
try {
urlObj = new URL(url);
} catch (ex) {
// The return value will signal the failure & the logged exception
// will provide the details to the developers.
console.log(`${url} seems to be not a valid URL, but it's OK`, ex);
if (typeof uri === 'string') {
let str = uri;
str = _fixURIStringScheme(str);
str = _fixURIStringHierPart(str);
obj = {};
let regex;
let match;
// protocol
regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi');
match = regex.exec(str);
if (match) {
obj.protocol = match[1].toLowerCase();
str = str.substring(regex.lastIndex);
}
// authority
regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi');
match = regex.exec(str);
if (match) {
let authority = match[1].substring(/* // */ 2);
str = str.substring(regex.lastIndex);
// userinfo
const userinfoEndIndex = authority.indexOf('@');
if (userinfoEndIndex !== -1) {
authority = authority.substring(userinfoEndIndex + 1);
}
obj.host = authority;
// port
const portBeginIndex = authority.lastIndexOf(':');
if (portBeginIndex !== -1) {
obj.port = authority.substring(portBeginIndex + 1);
authority = authority.substring(0, portBeginIndex);
}
obj.hostname = authority;
}
// pathname
regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi');
match = regex.exec(str);
if (match) {
obj.pathname = match[1] || '/';
str = str.substring(regex.lastIndex);
}
}
return urlObj;
return _getRoomAndDomainFromURLObject(obj);
}

View File

@@ -1,5 +1,11 @@
/* global APP, JitsiMeetJS, loggingConfig */
import { isRoomValid } from '../base/conference';
import { RouteRegistry } from '../base/react';
import { interceptComponent } from '../base/util';
import { Conference } from '../conference';
import { WelcomePage } from '../welcome';
import URLProcessor from '../../../modules/config/URLProcessor';
import KeyboardShortcut
from '../../../modules/keyboardshortcut/keyboardshortcut';
@@ -9,7 +15,33 @@ import JitsiMeetLogStorage from '../../../modules/util/JitsiMeetLogStorage';
const Logger = require('jitsi-meet-logger');
export * from './functions.native';
export { _parseURIString } from './functions.native';
/**
* Determines which route is to be rendered in order to depict a specific Redux
* store.
*
* @param {(Object|Function)} stateOrGetState - Redux state or Regux getState()
* method.
* @returns {Route}
*/
export function _getRouteToRender(stateOrGetState) {
const state
= typeof stateOrGetState === 'function'
? stateOrGetState()
: stateOrGetState;
// If mobile browser page was shown, there is no need to show it again.
const { room } = state['features/base/conference'];
const component = isRoomValid(room) ? Conference : WelcomePage;
const route = RouteRegistry.getRouteByComponent(component);
// Intercepts route components if any of component interceptor rules
// is satisfied.
route.component = interceptComponent(state, component);
return route;
}
/**
* Temporary solution. Later we'll get rid of global APP and set its properties
@@ -23,7 +55,11 @@ export function init() {
APP.keyboardshortcut = KeyboardShortcut;
APP.tokenData = getTokenData();
APP.API.init(APP.tokenData.externalAPISettings);
// Force enable the API if jwt token is passed because most probably
// jitsi meet is displayed inside of wrapper that will need to communicate
// with jitsi meet.
APP.API.init(APP.tokenData.jwt ? { forceEnable: true } : undefined);
APP.translation.init(settings.getLanguage());
}

View File

@@ -1,3 +1,5 @@
/* @flow */
import { NativeModules } from 'react-native';
import { APP_WILL_MOUNT } from '../app';
@@ -22,19 +24,19 @@ MiddlewareRegistry.register(store => next => action => {
// The react-native module AudioMode is implemented on iOS at the time of
// this writing.
if (AudioMode) {
let audioMode;
let mode;
switch (action.type) {
case APP_WILL_MOUNT:
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
audioMode = AudioMode.DEFAULT;
mode = AudioMode.DEFAULT;
break;
case CONFERENCE_WILL_JOIN: {
const conference = store.getState()['features/base/conference'];
audioMode
mode
= conference.audioOnly
? AudioMode.AUDIO_CALL
: AudioMode.VIDEO_CALL;
@@ -42,14 +44,16 @@ MiddlewareRegistry.register(store => next => action => {
}
default:
audioMode = null;
mode = null;
break;
}
if (audioMode !== null) {
AudioMode.setMode(audioMode).catch(err => {
console.error(`Failed to set audio mode ${audioMode}: ${err}`);
});
if (mode !== null) {
AudioMode.setMode(mode)
.catch(err =>
console.error(
`Failed to set audio mode ${String(mode)}: `
+ `${err}`));
}
}

View File

@@ -1,3 +1,7 @@
/* @flow */
import type { Dispatch } from 'redux';
import { conferenceWillLeave } from '../conference';
import JitsiMeetJS from '../lib-jitsi-meet';
@@ -14,10 +18,10 @@ const JitsiConnectionEvents = JitsiMeetJS.events.connection;
/**
* Opens new connection.
*
* @returns {Promise<JitsiConnection>}
* @returns {Function}
*/
export function connect() {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const connectionOptions
= state['features/base/connection'].connectionOptions;
@@ -28,7 +32,14 @@ export function connect() {
connectionOptions.token,
{
...connectionOptions,
bosh: connectionOptions.bosh + (room ? `?room=${room}` : '')
bosh:
connectionOptions.bosh
// XXX The Jitsi Meet deployments require the room
// argument to be in lower case at the time of this
// writing but, unfortunately, they do not ignore
// case themselves.
+ (room ? `?room=${room.toLowerCase()}` : '')
});
connection.addEventListener(
@@ -50,7 +61,7 @@ export function connect() {
* @param {string} message - Disconnect reason.
* @returns {void}
*/
function connectionDisconnected(message) {
function connectionDisconnected(message: string) {
connection.removeEventListener(
JitsiConnectionEvents.CONNECTION_DISCONNECTED,
connectionDisconnected);
@@ -103,7 +114,7 @@ export function connect() {
* @returns {Function}
*/
export function disconnect() {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
const conference = state['features/base/conference'].conference;
const connection = state['features/base/connection'].connection;
@@ -141,7 +152,7 @@ export function disconnect() {
* domain: string
* }}
*/
export function setDomain(domain) {
export function setDomain(domain: string) {
return {
type: SET_DOMAIN,
domain
@@ -160,7 +171,7 @@ export function setDomain(domain) {
* message: string
* }}
*/
function _connectionDisconnected(connection, message) {
function _connectionDisconnected(connection, message: string) {
return {
type: CONNECTION_DISCONNECTED,
connection,
@@ -198,7 +209,7 @@ function _connectionEstablished(connection) {
* error: string
* }}
*/
function _connectionFailed(connection, error) {
function _connectionFailed(connection, error: string) {
return {
type: CONNECTION_FAILED,
connection,

View File

@@ -1,10 +1,15 @@
/* global APP, JitsiMeetJS */
/* @flow */
import type { Dispatch } from 'redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { SET_DOMAIN } from './actionTypes';
import './reducer';
declare var APP: Object;
declare var JitsiMeetJS: Object;
const JitsiConferenceEvents = JitsiMeetJS.events.conference;
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -14,7 +19,7 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
* @returns {Promise<JitsiConnection>}
*/
export function connect() {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
const state = getState();
// XXX Lib-jitsi-meet does not accept uppercase letters.
@@ -88,7 +93,7 @@ export function disconnect() {
* domain: string
* }}
*/
export function setDomain(domain) {
export function setDomain(domain: string) {
return {
type: SET_DOMAIN,
domain

View File

@@ -1,3 +1,5 @@
/* @flow */
/**
* Returns current domain.
*
@@ -5,7 +7,7 @@
* state.
* @returns {(string|undefined)}
*/
export function getDomain(stateOrGetState) {
export function getDomain(stateOrGetState: Function | Object) {
const state
= typeof stateOrGetState === 'function'
? stateOrGetState()

View File

@@ -1,3 +1,5 @@
/* @flow */
import { ReducerRegistry, setStateProperty } from '../redux';
import {
@@ -9,20 +11,22 @@ import {
/**
* Reduces the Redux actions of the feature base/connection.
*/
ReducerRegistry.register('features/base/connection', (state = {}, action) => {
switch (action.type) {
case CONNECTION_DISCONNECTED:
return _connectionDisconnected(state, action);
ReducerRegistry.register(
'features/base/connection',
(state: Object = {}, action: Object) => {
switch (action.type) {
case CONNECTION_DISCONNECTED:
return _connectionDisconnected(state, action);
case CONNECTION_ESTABLISHED:
return _connectionEstablished(state, action);
case CONNECTION_ESTABLISHED:
return _connectionEstablished(state, action);
case SET_DOMAIN:
return _setDomain(state, action);
}
case SET_DOMAIN:
return _setDomain(state, action);
}
return state;
});
return state;
});
/**
* Reduces a specific Redux action CONNECTION_DISCONNECTED of the feature
@@ -34,7 +38,7 @@ ReducerRegistry.register('features/base/connection', (state = {}, action) => {
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _connectionDisconnected(state, action) {
function _connectionDisconnected(state: Object, action: Object) {
if (state.connection === action.connection) {
return setStateProperty(state, 'connection', undefined);
}
@@ -52,7 +56,7 @@ function _connectionDisconnected(state, action) {
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _connectionEstablished(state, action) {
function _connectionEstablished(state: Object, action: Object) {
return setStateProperty(state, 'connection', action.connection);
}
@@ -62,9 +66,10 @@ function _connectionEstablished(state, action) {
*
* @param {string} domain - The domain with which the returned options are to be
* populated.
* @private
* @returns {Object}
*/
function _constructConnectionOptions(domain) {
function _constructConnectionOptions(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
@@ -88,7 +93,7 @@ function _constructConnectionOptions(domain) {
boshProtocol || (boshProtocol = 'https:');
return {
bosh: `${boshProtocol}//${domain}/http-bind`,
bosh: `${String(boshProtocol)}//${domain}/http-bind`,
hosts: {
domain,
focus: `focus.${domain}`,
@@ -106,7 +111,7 @@ function _constructConnectionOptions(domain) {
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _setDomain(state, action) {
function _setDomain(state: Object, action: Object) {
return {
...state,
connectionOptions: {

View File

@@ -1,8 +1,6 @@
import { NativeModules } from 'react-native';
import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
import { Platform } from '../../react';
import { POSIX } from '../../react-native';
// XXX At the time of this writing extending RTCPeerConnection using ES6 'class'
// and 'extends' causes a runtime error related to the attempt to define the
// onaddstream property setter. The error mentions that babelHelpers.set is
@@ -106,6 +104,7 @@ _RTCPeerConnection.prototype.setRemoteDescription = function(
/**
* Logs at error level.
*
* @private
* @returns {void}
*/
function _LOGE(...args) {
@@ -119,6 +118,8 @@ function _LOGE(...args) {
*
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* which specifies the configuration of the remote end of the connection.
* @private
* @private
* @returns {Promise}
*/
function _setRemoteDescription(sessionDescription) {
@@ -160,12 +161,13 @@ function _setRemoteDescription(sessionDescription) {
*
* @param {RTCSessionDescription} sdp - The RTCSessionDescription which
* specifies the configuration of the remote end of the connection.
* @private
* @returns {Promise}
*/
function _synthesizeIPv6Addresses(sdp) {
// The synthesis of IPv6 addresses is implemented on iOS only at the time of
// this writing.
if (Platform.OS !== 'ios') {
if (!NativeModules.POSIX) {
return Promise.resolve(sdp);
}
@@ -184,6 +186,7 @@ function _synthesizeIPv6Addresses(sdp) {
*
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* for which IPv6 addresses will be synthesized.
* @private
* @returns {{
* ips: Map,
* lines: Array
@@ -194,6 +197,7 @@ function _synthesizeIPv6Addresses0(sessionDescription) {
let start = 0;
const lines = [];
const ips = new Map();
const getaddrinfo = NativeModules.POSIX.getaddrinfo;
do {
const end = sdp.indexOf('\r\n', start);
@@ -232,7 +236,7 @@ function _synthesizeIPv6Addresses0(sessionDescription) {
if (v && typeof v === 'string') {
resolve(v);
} else {
POSIX.getaddrinfo(ip).then(
getaddrinfo(ip).then(
value => {
if (value.indexOf(':') === -1
|| value === ips.get(ip)) {
@@ -278,6 +282,7 @@ function _synthesizeIPv6Addresses0(sessionDescription) {
* @param {Map} ips - A Map of IPv4 addresses found in the specified
* sessionDescription to synthesized IPv6 addresses.
* @param {Array} lines - The lines of the specified sessionDescription.
* @private
* @returns {RTCSessionDescription} A RTCSessionDescription that represents the
* result of the synthesis of IPv6 addresses.
*/

View File

@@ -11,8 +11,8 @@ import {
* Initial state of 'features/base/lib-jitsi-meet'.
*
* @type {{
* initializationError: null,
* initialized: boolean
* initializationError: null,
* initialized: boolean
* }}
*/
const INITIAL_STATE = {

View File

@@ -1,3 +1,7 @@
/* @flow */
import type { Dispatch } from 'redux';
import {
SET_AUDIO_MUTED,
SET_CAMERA_FACING_MODE,
@@ -17,7 +21,7 @@ import './reducer';
* muted: boolean
* }}
*/
export function setAudioMuted(muted) {
export function setAudioMuted(muted: boolean) {
return {
type: SET_AUDIO_MUTED,
muted
@@ -33,7 +37,7 @@ export function setAudioMuted(muted) {
* cameraFacingMode: CAMERA_FACING_MODE
* }}
*/
export function setCameraFacingMode(cameraFacingMode) {
export function setCameraFacingMode(cameraFacingMode: CAMERA_FACING_MODE) {
return {
type: SET_CAMERA_FACING_MODE,
cameraFacingMode
@@ -50,7 +54,7 @@ export function setCameraFacingMode(cameraFacingMode) {
* muted: boolean
* }}
*/
export function setVideoMuted(muted) {
export function setVideoMuted(muted: boolean) {
return {
type: SET_VIDEO_MUTED,
muted
@@ -63,7 +67,7 @@ export function setVideoMuted(muted) {
* @returns {Function}
*/
export function toggleAudioMuted() {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
const muted = getState()['features/base/media'].audio.muted;
return dispatch(setAudioMuted(!muted));
@@ -76,7 +80,7 @@ export function toggleAudioMuted() {
* @returns {Function}
*/
export function toggleCameraFacingMode() {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
let cameraFacingMode
= getState()['features/base/media'].video.facingMode;
@@ -95,7 +99,7 @@ export function toggleCameraFacingMode() {
* @returns {Function}
*/
export function toggleVideoMuted() {
return (dispatch, getState) => {
return (dispatch: Dispatch<*>, getState: Function) => {
const muted = getState()['features/base/media'].video.muted;
return dispatch(setVideoMuted(!muted));

View File

@@ -140,9 +140,10 @@ class VideoTrack extends AbstractVideoTrack {
// eslint-disable-next-line valid-jsdoc
/**
* @inheritdoc
*
* Animate the setting of the video track to be rendered by this instance.
*
* @inheritdoc
* @protected
*/
_setVideoTrack(videoTrack) {
// If JitsiTrack instance didn't change, that means some other track's

View File

@@ -1,3 +1,5 @@
/* @flow */
import { CONFERENCE_LEFT } from '../conference';
import { MiddlewareRegistry } from '../redux';
import { setTrackMuted, TRACK_ADDED } from '../tracks';
@@ -21,11 +23,11 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_LEFT:
resetInitialMediaState(store);
_resetInitialMediaState(store);
break;
case TRACK_ADDED:
action.track.local && syncTrackMutedState(store, action.track);
action.track.local && _syncTrackMutedState(store, action.track);
break;
}
@@ -36,9 +38,10 @@ MiddlewareRegistry.register(store => next => action => {
* Resets initial media state.
*
* @param {Store} store - Redux store.
* @private
* @returns {void}
*/
function resetInitialMediaState(store) {
function _resetInitialMediaState(store) {
const { dispatch, getState } = store;
const state = getState()['features/base/media'];
@@ -53,9 +56,10 @@ function resetInitialMediaState(store) {
*
* @param {Store} store - Redux store.
* @param {Track} track - Local media track.
* @private
* @returns {void}
*/
function syncTrackMutedState(store, track) {
function _syncTrackMutedState(store, track) {
const state = store.getState()['features/base/media'];
const muted = state[track.mediaType].muted;

View File

@@ -31,9 +31,10 @@ const AUDIO_INITIAL_MEDIA_STATE = {
* @param {AudioMediaState} state - Media state of local audio.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @private
* @returns {AudioMediaState}
*/
function audio(state = AUDIO_INITIAL_MEDIA_STATE, action) {
function _audio(state = AUDIO_INITIAL_MEDIA_STATE, action) {
switch (action.type) {
case SET_AUDIO_MUTED:
return {
@@ -70,9 +71,10 @@ const VIDEO_INITIAL_MEDIA_STATE = {
* @param {VideoMediaState} state - Media state of local video.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @private
* @returns {VideoMediaState}
*/
function video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
switch (action.type) {
case SET_CAMERA_FACING_MODE:
return {
@@ -102,6 +104,6 @@ function video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
* @returns {Object}
*/
ReducerRegistry.register('features/base/media', combineReducers({
audio,
video
audio: _audio,
video: _video
}));

View File

@@ -1,75 +0,0 @@
/**
* Object describing application route.
*
* @typedef {Object} Route
* @property {Component} component - React Component constructor.
* @property {string} path - URL route, required for web routing.
*/
/**
* A registry for Navigator routes, allowing features to register themselves
* without needing to create additional inter-feature dependencies.
*/
class RouteRegistry {
/**
* Initializes a new RouteRegistry instance.
*/
constructor() {
/**
* The set of registered routes.
*
* @private
*/
this._routeRegistry = new Set();
}
/**
* Returns all registered routes.
*
* @returns {Route[]}
*/
getRoutes() {
// We use the destructuring operator to 'clone' the route object to
// prevent modifications from outside (e.g. React Native's Navigator
// extends it with additional properties).
return [ ...this._routeRegistry ].map(r => {
return { ...r };
});
}
/**
* Returns registered route by name if any.
*
* @param {Object} component - The React Component (class) of the route to
* retrieve.
* @returns {Route|null}
*/
getRouteByComponent(component) {
const route
= [ ...this._routeRegistry ].find(r => r.component === component);
// We use destructuring operator to 'clone' route object to prevent
// modifications from outside (e.g. React Native's Navigator extends
// it with some additional properties).
return route ? { ...route } : null;
}
/**
* Adds a route to this registry.
*
* @param {Route} route - Route definition object.
* @returns {void}
*/
register(route) {
if (this._routeRegistry.has(route)) {
throw new Error(`Route ${route.component} is registered already!`);
}
this._routeRegistry.add(route);
}
}
/**
* The public singleton instance of the RouteRegistry class.
*/
export default new RouteRegistry();

View File

@@ -1 +0,0 @@
export { default as RouteRegistry } from './RouteRegistry';

View File

@@ -48,9 +48,10 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE
* @param {Participant} action.participant - Information about participant to be
* added/modified.
* @param {JitsiConference} action.conference - Conference instance.
* @private
* @returns {Participant|undefined}
*/
function participant(state, action) {
function _participant(state, action) {
switch (action.type) {
case DOMINANT_SPEAKER_CHANGED:
// Only one dominant speaker is allowed.
@@ -146,7 +147,7 @@ function participant(state, action) {
ReducerRegistry.register('features/base/participants', (state = [], action) => {
switch (action.type) {
case PARTICIPANT_JOINED:
return [ ...state, participant(undefined, action) ];
return [ ...state, _participant(undefined, action) ];
case PARTICIPANT_LEFT:
return state.filter(p => p.id !== action.participant.id);
@@ -155,7 +156,7 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_UPDATED:
case PIN_PARTICIPANT:
return state.map(p => participant(p, action));
return state.map(p => _participant(p, action));
default:
return state;

View File

@@ -1,3 +0,0 @@
import { NativeModules } from 'react-native';
export default NativeModules.POSIX;

View File

@@ -1 +0,0 @@
export { default as POSIX } from './POSIX';

View File

@@ -1,3 +1,5 @@
/* @flow */
// Re-export react-native's Platform because we want to provide a minimal
// equivalent on Web.
import { Platform } from 'react-native';

View File

@@ -1,3 +1,5 @@
/* @flow */
const userAgent = navigator.userAgent;
let OS;

View File

@@ -0,0 +1,119 @@
/* @flow */
import { Component } from 'react';
/**
* Object describing application route.
*
* @typedef {Object} Route
* @property {Component} component - React Component constructor.
* @property {string} path - URL route, required for web routing.
*/
type Route = {
component: Class<Component<*>>, // eslint-disable-line no-undef
path: string
};
/**
* A registry for Navigator routes, allowing features to register themselves
* without needing to create additional inter-feature dependencies.
*/
class RouteRegistry {
_elements: Array<Route>;
/**
* Initializes a new RouteRegistry instance.
*/
constructor() {
/**
* The set of registered routes.
*
* @private
* @type {Route[]}
*/
this._elements = [];
}
/**
* Determines whether two specific Routes are equal i.e. they describe one
* and the same abstract route.
*
* @param {Object} a - The Route to compare to b.
* @param {Object} b - The Route to compare to a.
* @returns {boolean} True if the specified a and b describe one and the
* same abstract route; otherwise, false.
*/
areRoutesEqual(a: Route, b: Route) {
if (a === b) { // reflexive
return true;
}
if (!a) {
return !b;
}
if (!b) {
return !a;
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
return (
aKeys.length === bKeys.length /* symmetric */
&& aKeys.every(key => a[key] === b[key]));
}
/**
* Returns all registered routes.
*
* @returns {Route[]}
*/
getRoutes() {
// We use the destructuring operator to 'clone' the route object to
// prevent modifications from outside (e.g. React Native's Navigator
// extends it with additional properties).
return this._elements.map(r => {
return { ...r };
});
}
/* eslint-disable no-undef */
/**
* Returns registered route by name if any.
*
* @param {Component} component - The React Component (class) of the route
* to retrieve.
* @returns {Route|null}
*/
getRouteByComponent(component: Class<Component<*>>) {
/* eslint-enable no-undef */
const route = this._elements.find(r => r.component === component);
// We use destructuring operator to 'clone' route object to prevent
// modifications from outside (e.g. React Native's Navigator extends
// it with some additional properties).
return route ? { ...route } : null;
}
/**
* Adds a route to this registry.
*
* @param {Route} route - Route definition object.
* @returns {void}
*/
register(route: Route) {
if (this._elements.includes(route)) {
throw new Error(
`Route ${String(route.component)} is registered already!`);
}
this._elements.push(route);
}
}
/**
* The public singleton instance of the RouteRegistry class.
*/
export default new RouteRegistry();

View File

@@ -1,11 +1,16 @@
/* global APP, interfaceConfig */
/* @flow */
import React, { Component } from 'react';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* The CSS style of the element with CSS class <tt>rightwatermark</tt>.
*
* @private
*/
const RIGHT_WATERMARK_STYLE = {
const _RIGHT_WATERMARK_STYLE = {
backgroundImage: 'url(images/rightwatermark.png)'
};
@@ -14,13 +19,22 @@ const RIGHT_WATERMARK_STYLE = {
* etc.
*/
export class Watermarks extends Component {
state = {
brandWatermarkLink: String,
jitsiWatermarkLink: String,
showBrandWatermark: Boolean,
showJitsiWatermark: Boolean,
showJitsiWatermarkForGuests: Boolean,
showPoweredBy: Boolean
};
/**
* Initializes a new Watermarks instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: Object) {
super(props);
let showBrandWatermark;
@@ -87,7 +101,7 @@ export class Watermarks extends Component {
target = '_new'>
<div
className = 'watermark rightwatermark'
style = { RIGHT_WATERMARK_STYLE } />
style = { _RIGHT_WATERMARK_STYLE } />
</a>
);
}

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