Compare commits

...

83 Commits

Author SHA1 Message Date
bgrozev
6cc272fbd1 log: Fixes a log message 2018-03-19 14:41:18 -05:00
Saumeya Katyal
2334eb9967 doc: Add dev server steps (#2610)
* doc: Add webpack-dev-server steps
2018-03-19 10:16:01 -05:00
Emil Ivov
04bd4a9038 Merge pull request #2617 from virtuacoplenny/lenny/info-dialog-again
fix(info): update copy text to find correct var
2018-03-15 12:16:20 -05:00
virtuacoplenny
eb8f34cee8 Merge pull request #2612 from jitsi/no_protocol_in_intent_uri
feat(UnsupportedMobileBrowser): do not include protocol in the intent
2018-03-15 10:01:31 -07:00
Leonard Kim
b9379f5996 fix(info): update copy text to find correct var 2018-03-15 09:42:39 -07:00
paweldomas
40d7d0c9cb feat(UnsupportedMobileBrowser): do not include protocol in the intent
Do not include the protocol part in the intent URL.
2018-03-14 17:44:32 -05:00
zbettenbuk
357f173e85 Remove obsolate PlatformElements.native.js 2018-03-13 18:04:17 -05:00
zbettenbuk
7da26042b3 Avoid asking for calendar permission on app start 2018-03-13 18:04:17 -05:00
zbettenbuk
c86c7beb24 Refactor i18n calendar formatter 2018-03-13 18:04:17 -05:00
zbettenbuk
1020a54a33 Add Android navigation bar 2018-03-13 18:04:17 -05:00
zbettenbuk
c84abd543e Add support for app link scheme 2018-03-13 18:04:16 -05:00
zbettenbuk
4b17c6f015 Add pull-to-refresh functionality 2018-03-13 18:04:16 -05:00
zbettenbuk
cb973b61aa Implement adaptive known domain list 2018-03-13 18:04:16 -05:00
zbettenbuk
b096622995 Unify recent and meeting lists 2018-03-13 18:04:16 -05:00
zbettenbuk
ae0bf876a8 Add conference notification 2018-03-13 18:04:16 -05:00
zbettenbuk
bba480f329 Add calendar-sync feature 2018-03-13 18:04:14 -05:00
paweldomas
4dbcaf851f flow(AbstractAudio): specific function types 2018-03-13 16:57:29 -05:00
paweldomas
04dff9059b ref(AudioOutputPreview): use Audio from base/media 2018-03-13 16:57:28 -05:00
paweldomas
26cd2f17f6 ref(chat): port incoming chat msg sound to react 2018-03-13 16:57:28 -05:00
paweldomas
60e03e3dec feat: add join/leave sounds on mobile
Adds base/sounds feature which allows other features to register a sound
source under specified id. A new SoundsCollection component will then
render corresponding HTMLAudioElement for each such sound. Once "setRef"
callback is called by the HTMLAudioElement, this element will be added
to the Redux store. When that happens sound can be played through the
new 'playSound' action which will call play() method on the stored
HTMLAudioElement instance.
2018-03-13 16:57:28 -05:00
virtuacoplenny
bfb45ed0e8 fix(large-video): do not try to show background on safari with webrtc (#2606)
The animation for toggling filmstrip visibility was lagging on
Safari. Even though the background video is set to hidden, it is
still causing issues. Setting the background to display none
instead does help but might interfere with animations. So instead
do the easy thing and re-use logic used for Firefox to not show
the background video.
2018-03-13 14:37:35 -07:00
virtuacoplenny
e325199075 fix(invite): prefix a + when faking the validation response (#2597)
Pre-existing logic made it so numbers were assumed as valid
if no validation url was specified. To be consistent with
the validation server, the faked number should include a
+ at the beginning.
2018-03-12 13:25:42 -07:00
virtuacoplenny
4e4713c3e2 feat(invite): be able to call numbers from the invite dialog (#2555)
* feat(invite): be able to call numbers from the invite dialog

The major changes:
- Remove DialOutDialog, its views, redux hooks, css, and images.
  Its main functionality has been moved into AddPeopleDialog.
- Modify the AppPeopleDialog styling a bit so it is wider.
- Add phone numbers to AddPeopleDialog search results. Phone
  numbers are validated in parallel with the request for people
  and then appended to the result. The validation includes
  an ajax to validate the number is recognized as dialable by
  the server. The trigger for the validation is essentially if
  the entered input is numbers only.
- AddPeopleDialog holds onto the full object representation of
  an item selected in MultiSelectAutocomplete. This is so
  selected items can be removed on successful invite, leaving
  only unsuccessful items.
- More granular error handling on invite so individual invitees
  can be removed from the selected items list.

* squash: change load state, new regex for numbers

* squash: change strings, auto prepend 1 if no country code, add reminders
2018-03-12 12:23:40 -07:00
Saúl Ibarra Corretgé
ff8386e931 debian: fix setting the auth domain certificates
In 94813bc0fd (diff-6e9552c9bd8e61c8f277c21220160234)
two local variables got removed (AUTH_KEY_FILE and AUTH_CRT_FILE), which are used by the sed command
below to configure the virtualhost for auth.
2018-03-11 16:05:14 -05:00
Leonard Kim
8f520086e5 fix(info): do not show dial in numbers without a room specified
For the static page an error message displays stating no room
was specified. On mobile for unsupported browsers, the dial in
info will not show.
2018-03-09 17:18:10 -06:00
Shuai Li
5cde674eff fix(android): webrtc progurd rule
The new libwebrtc.jar contains an extra unused class file, when proguard is enabled result in the following warning:

org.chromium.build.BuildHooksAndroidImpl: can't find superclass or interface org.chromium.build.BuildHooksAndroid
2018-03-09 12:29:49 -08:00
Lyubo Marinov
c018252eee [Android] Fix RuntimeException in RNImmersiveModule
java.lang.RuntimeException: Tried to access a JS module before the React instance was fully set up. Calls to ReactContext#getJSModule should only happen once initialize() has been called on your native module.
	at com.facebook.react.bridge.ReactContext.getJSModule(ReactContext.java:102)
	at com.rnimmersive.RNImmersiveModule.emitImmersiveStateChangeEvent(RNImmersiveModule.java:74)
	at org.jitsi.meet.sdk.JitsiMeetView.onWindowFocusChanged(JitsiMeetView.java:504)
	at android.view.View.dispatchWindowFocusChanged(View.java:10257)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1193)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1197)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1197)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1197)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1197)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1197)
	at android.view.ViewGroup.dispatchWindowFocusChanged(ViewGroup.java:1197)
	at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:3602)
	at android.os.Handler.dispatchMessage(Handler.java:102)
	at android.os.Looper.loop(Looper.java:154)
	at android.app.ActivityThread.main(ActivityThread.java:6119)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
2018-03-09 13:50:07 -06:00
George Politis
c8cab1560c Merge pull request #2589 from jitsi/update-lib-jitsi-stats
Updates lib-jitsi-meet to d4b78721.
2018-03-09 12:44:53 -06:00
damencho
d218abfd97 Updates lib-jitsi-meet to d4b78721.
Implements the promised based getStats. Enables them for Safari and FF.
2018-03-09 12:03:01 -06:00
paweldomas
9e0fee6c7d fix(android): do not require java 8 target
Updates react-native-webrtc to get rid of Java 8 requirement for
the Android app.
2018-03-08 15:47:05 -06:00
damencho
5dca9e08f4 Bumps uglifyjs-webpack-plugin and its dependencies.
Solves a GitHub warning in lib-jitsi-meet about a vulnerability in a
uglifyjs-webpack-plugin dependency.
2018-03-08 15:12:15 -06:00
Lyubo Marinov
d3a1f7d4f7 [iOS] Fix uncaught NSInvalidArgumentException in RTCPeerConnection's createAnswer
WebRTC used to report createAnswer (and createOffer) NSError with key
"error". But now the key's called "NSLocalizedDescription".

Additionally, NSMutableDictionary doesn't accept nil.
2018-03-07 15:23:20 -06:00
Leonard Kim
80bdf908ca fix(info): always remove last part of path for meeting name 2018-03-06 15:22:27 -06:00
Leonard Kim
0d3b4eedf8 fix(info): make some text selectable for manual copying 2018-03-06 13:47:55 -06:00
Leonard Kim
824a8a8864 fix(info): respect path when linking to dial in page 2018-03-06 13:47:55 -06:00
virtuacoplenny
45c1438fe6 Merge pull request #2567 from jitsi/bgrozev-patch-1
doc: Add clarifying text to the top of the readme.
2018-03-06 11:29:25 -08:00
bgrozev
8caeabf6b4 doc: Add clarifying text to the top of the readme. 2018-03-06 11:45:11 -06:00
Leonard Kim
466561f99f fix(polyfills): implement createHTMLDocument for jquery 2018-03-06 10:46:53 -06:00
zbettenbuk
8dc866fab3 Fix setTimeout polyfill 2018-03-06 09:42:25 +01:00
George Politis
1c8b8e031b feat: Whitelists the gatherStats option. (#2561)
* feat: Whitelists the gatherStats option.

* doc: Replaces obsolete disableStats option with gatherStats.
2018-03-05 11:12:39 -06:00
Lyubo Marinov
796489dc77 Coding style: naming, consistency 2018-03-04 19:28:44 -06:00
Lyubo Marinov
a30412ba65 [RN] Automatically dispatch CONFERENCE_LEFT 2018-03-04 19:28:44 -06:00
Saúl Ibarra Corretgé
8b35ea8ad5 deps: update jquery version (#2441)
* deps: update jquery version

* squash: resize thumbnails after appending shared thumb

This forces jquery animate to show the thumbnail somehow...
Remote thumbnails basically work this way (append to filmstrip
and then resize filmstrip thumbnails) so I just copied that
implementation. ... So I admit I lost this fight because
even after looking at jquery I couldn't understand why
it doesn't work on the first resize but does on the second.
Plus I'm being put on a strict timebox to update jquery.

* squash: getJSON no longer supports .success
2018-03-02 21:20:47 -06:00
hristoterezov
e05f2a9027 chore(package.json): Update lib-jitsi-meet. 2018-03-02 21:19:16 -06:00
George Politis
0d5cc8898d chore(lib-jitsi-meet): Update version. 2018-03-02 16:33:04 -06:00
virtuacoplenny
7672a88990 fix(dial-in): remove console.warn used for debugging (#2547)
Looks like this guy snuck into the commit.
2018-03-01 15:50:23 -08:00
virtuacoplenny
5a45b52881 fix(hangup): destroy local tracks on conference leave (#2546)
The difference from this change and 88325ae is there is no
attempt to do this in redux. This is the safer change in that
the cleanup logic is known only to trigger on hangup.
2018-03-01 15:47:46 -08:00
Leonard Kim
ce6e8472f0 Revert "fix(hangup): destroy local tracks on conference leave (#2502)"
This reverts commit 88325aeef2.
Turns out a conference with a password triggers a failed conference
join. It's going to be tricky to decipher when to do actual
cleanup, and where to shove that code, so reverting is easier for
now.
2018-03-01 14:07:30 -06:00
Leonard Kim
b00aaf1de7 fix(dial-in): specify url root for more numbers link
This gets around the issue of a cdn page being opened.
2018-02-28 23:17:20 -06:00
Lyubo Marinov
e622829c1c [RN] Promised-base RTCPeerConnection API
Recent changes in lib-jitsi-meet probably led to (1) our
RTCPeerConnection customizations on react-native not being used which is
a problem because we need them for at least NAT64 on iOS in order to
pass the review in Apple's App Store and (2) unexpected exceptions
inside react-native-webrtc.

The Promise-based WebRTC API should be merged from react-native-webrtc's
upstream but I don't want to do it right now because last time we got
multiple bugs in addition.
2018-02-28 17:17:08 -06:00
Lyubo Marinov
037e7f59b0 [RN] Fix a TypeError when invoking humanize() on undefined 2018-02-28 17:17:07 -06:00
hristoterezov
df754f4f41 fix(reload): Preserve URL params on reload/redirect. 2018-02-28 14:28:56 -06:00
George Politis
fd0749000e feat: Whitelists the iceTransportPolicy option. (#2535) 2018-02-28 11:28:47 -08:00
Lyubo Marinov
d727ee80b2 [RN] Fix base/profile and recent-list bugs 2018-02-27 20:52:34 -06:00
zbettenbuk
e4da0e988e Split storage keys to features 2018-02-27 07:33:33 -06:00
zbettenbuk
1474304cc5 Add the ability to do partial updates on the flatten profile object 2018-02-27 07:33:33 -06:00
zbettenbuk
d02ab2c641 Flatten the store of the profile feature 2018-02-27 07:33:33 -06:00
zbettenbuk
0e07020d09 Flatten the store of the recent-list feature 2018-02-27 07:33:33 -06:00
zbettenbuk
e0deb6d64b Prepare PersistenceRegistry for flat subtrees 2018-02-27 07:33:33 -06:00
virtuacoplenny
88325aeef2 fix(hangup): destroy local tracks on conference leave (#2502)
Destroy local tracks and also destroy large video so the
user does not wonder why camera (and mic) are still enabled
even though hangup has been pressed.
2018-02-26 21:10:49 -08:00
Lyubo Marinov
9f69c4d730 Grow features/settings from features/app-settings and features/settings-menu 2018-02-26 19:19:01 -06:00
zbettenbuk
e23d4317eb Add hint box with dynamic join button 2018-02-26 18:39:48 -06:00
zbettenbuk
547ddee3a5 Dismiss keyboard on menu open 2018-02-26 18:39:48 -06:00
zbettenbuk
3cf9fd439b Add iOS 10 compatibility header padding 2018-02-26 18:39:48 -06:00
zbettenbuk
7cd40353e7 Remove android view clipping fix from welcome page 2018-02-26 18:39:48 -06:00
zbettenbuk
04690dfc8f Facelift Welcome screen 2018-02-26 18:39:48 -06:00
zbettenbuk
9a9890f86c Introduce SafeArea for Settings and Header 2018-02-26 18:35:13 -06:00
Leonard Kim
9b04a7852a fix(welcome-page): send analytics on join 2018-02-26 18:41:47 -05:00
Emil Ivov
df9d17ba18 Merge pull request #2523 from bgrozev/whitelist-ss-fps
config: Whitelists the desktopSharingFrameRate config key.
2018-02-26 13:18:20 -06:00
Boris Grozev
5d0ac7653d config: Whitelists the desktopSharingFrameRate config key. 2018-02-26 11:45:42 -06:00
virtuacoplenny
1ef2e2ee7e Merge pull request #2503 from jitsi/ss_fps
feat(ss_framerate): Add config option for min/max frame rate.
2018-02-23 12:28:48 -08:00
hristoterezov
b3431ab3e7 chore(lib-jitsi-meet): Update version. 2018-02-23 13:38:41 -06:00
Leonard Kim
6fbe78eb34 fix(welcome-page): change font family and spacing 2018-02-23 11:35:02 -06:00
Lyubo Marinov
b8de5bbfc3 [RN] Add Picture-in-Picture support (Coding style: naming, consistency) 2018-02-23 11:21:26 -06:00
Saúl Ibarra Corretgé
b3683068d4 [RN] Add Picture-in-Picture support
This only works automatically on Android >= 8. On other platforms / versions, it
relies on the SDK user on implementing a "reduced UI" mode and reacting to the
"request PIP" delegate method.
2018-02-23 11:21:25 -06:00
Lyubo Marinov
94473e5660 [Android] Allow accessing react-native's in-app developer menu (in the emulator) 2018-02-23 11:21:25 -06:00
virtuacoplenny
7f78050513 Merge pull request #2434 from slavakisel/external-notification-screen-sharing
Implement external API notification about screen sharing status
2018-02-22 17:39:03 -08:00
hristoterezov
2d9b906a3b feat(ss_framerate): Add config option for min/max frame rate. 2018-02-22 19:37:17 -06:00
Leonard Kim
1f82ce3d19 feat(unsupported-browser): show dial-in for mobile
- Move the existing components for the static dial in page into
  a separate folder for easier reuse.
- Reuse those components for displaying dial-on numbers on the
  mobile page for unsupported browsers.
- Modify those components to support having tel protocol
  links on the dial-in numbers.
- Have DialInSummary, formerly DialInInfoPage, respect a
  passed in className prop for easier styling differences.
2018-02-22 17:29:03 -06:00
Leonard Kim
68b710a222 fix(dial-in): allow text select in numbers page 2018-02-22 17:29:03 -06:00
brian baldino
9fea5e89b3 don't show 'user has left' notification for hidden users 2018-02-22 13:41:24 -06:00
Slava Kisel
e1d849e3a0 Implement external API notification about screen sharing status 2018-02-22 10:25:03 +03:00
virtuacoplenny
74a92f83c7 feat(welcome): new design (#2492)
* feat(welcome): new design

* squash: update strings

* squash: copy/paste error?

* squash: remove welcome page disabling checks

* squash: change strings again

* squash: background var

* squash: title and desc css as variables
2018-02-21 22:58:55 -06:00
224 changed files with 8363 additions and 4403 deletions

View File

@@ -1,13 +1,15 @@
# 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, [secure](#security) and scalable video conferences. You can see [Jitsi Meet in action](http://youtu.be/7vFUVClsNh0) here at the session #482 of the VoIP Users Conference.
Jitsi Meet is an open-source (Apache) WebRTC JavaScript application that uses [Jitsi Videobridge](https://jitsi.org/videobridge) to provide high quality, [secure](#security) and scalable video conferences. You can see Jitsi Meet in action [here at the session #482 of the VoIP Users Conference](http://youtu.be/7vFUVClsNh0).
You can also try it out yourself at https://meet.jit.si .
The Jitsi Meet client runs in your browser, without the need for installing anything on your computer. You can also try it out yourself at https://meet.jit.si .
Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad.
## Installation
On the client side, no installation is necessary. You just point your browser to the URL of your deployment. This section is about installing the Jitsi Meet suite on your server and hosting your own conferencing service.
Installing Jitsi Meet is quite a simple experience. For Debian-based systems, we recommend following the [quick-install](https://github.com/jitsi/jitsi-meet/blob/master/doc/quick-install.md) document, which uses the package system.
For other systems, or if you wish to install all components manually, see the [detailed manual installation instructions](https://github.com/jitsi/jitsi-meet/blob/master/doc/manual-install.md).
@@ -88,6 +90,19 @@ cd jitsi-meet
npm unlink lib-jitsi-meet
npm install
```
## Running with webpack-dev-server for development
Use it at the CLI, type
```
node_modules/.bin/webpack-dev-server
```
By default the backend deployment used is `beta.meet.jit.si`, you can point the Jitsi-Meet app at a different backend by using a proxy server. To do this set the WEBPACK_DEV_SERVER_PROXY_TARGET variable, type
```
WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com node_modules/.bin/webpack-dev-server
```
The app should be running at https://localhost:8080/
## Contributing

View File

@@ -43,16 +43,6 @@ Add the Maven repository
`https://github.com/jitsi/jitsi-maven-repository/raw/master/releases` and the
dependency `org.jitsi.react:jitsi-meet-sdk:1.9.0` into your `build.gradle`.
Add Java 1.8 compatibility support to your project by adding the following lines
into your `build.gradle` file:
```
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
```
## API
Jitsi Meet SDK is an Android library which embodies the whole Jitsi Meet
@@ -117,19 +107,19 @@ public class MainActivity extends AppCompatActivity {
JitsiMeetView.onNewIntent(intent);
}
@Override
protected void onPause() {
super.onPause();
JitsiMeetView.onHostPause(this);
}
@Override
protected void onResume() {
super.onResume();
JitsiMeetView.onHostResume(this);
}
@Override
protected void onStop() {
super.onStop();
JitsiMeetView.onHostPause(this);
}
}
```
@@ -142,6 +132,10 @@ which displays a single `JitsiMeetView`.
See JitsiMeetView.getDefaultURL.
#### getPictureInPictureEnabled()
See JitsiMeetView.getPictureInPictureEnabled.
#### getWelcomePageEnabled()
See JitsiMeetView.getWelcomePageEnabled.
@@ -154,6 +148,10 @@ See JitsiMeetView.loadURL.
See JitsiMeetView.setDefaultURL.
#### setPictureInPictureEnabled(boolean)
See JitsiMeetView.setPictureInPictureEnabled.
#### setWelcomePageEnabled(boolean)
See JitsiMeetView.setWelcomePageEnabled.
@@ -179,6 +177,12 @@ if set to `null`, the default built in JavaScript is used: https://meet.jit.si.
Returns the `JitsiMeetViewListener` instance attached to the view.
#### getPictureInPictureEnabled()
Returns `true` if Picture-in-Picture is enabled; `false`, otherwise. If not
explicitly set (by a preceding `setPictureInPictureEnabled` call), defaults to
`true` if the platform supports Picture-in-Picture natively; `false`, otherwise.
#### getWelcomePageEnabled()
Returns true if the Welcome page is enabled; otherwise, false. If false, a black
@@ -220,19 +224,30 @@ view.loadURLObject(urlObject);
Sets the default URL. See `getDefaultURL` for more information.
NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
NOTE: Must be called before (if at all) `loadURL`/`loadURLString` for it to take
effect.
#### setListener(listener)
Sets the given listener (class implementing the `JitsiMeetViewListener`
interface) on the view.
#### setPictureInPictureEnabled(boolean)
Sets whether Picture-in-Picture is enabled. If not set, Jitsi Meet SDK
automatically enables/disables Picture-in-Picture based on native platform
support.
NOTE: Must be called (if at all) before `loadURL`/`loadURLString` for it to take
effect.
#### setWelcomePageEnabled(boolean)
Sets whether the Welcome page is enabled. See `getWelcomePageEnabled` for more
information.
NOTE: Must be called before `loadURL`/`loadURLString` for it to take effect.
NOTE: Must be called (if at all) before `loadURL`/`loadURLString` for it to take
effect.
#### onBackPressed()
@@ -257,7 +272,8 @@ This is a static method.
#### onHostResume(activity)
Helper method which should be called from the activity's `onResume` method.
Helper method which should be called from the activity's `onResume` or `onStop`
method.
This is a static method.
@@ -269,6 +285,13 @@ activity's `onNewIntent` method.
This is a static method.
#### onUserLeaveHint()
Helper method for integrating automatic Picture-in-Picture. It should be called
from the activity's `onUserLeaveHint` method.
This is a static method.
#### JitsiMeetViewListener
`JitsiMeetViewListener` provides an interface apps can implement to listen to
@@ -379,9 +402,19 @@ rules file:
# WebRTC
-keep class org.webrtc.** { *; }
-dontwarn org.chromium.build.BuildHooksAndroid
# Jisti Meet SDK
-keep class org.jitsi.meet.sdk.** { *; }
```
## Picture-in-Picture
`JitsiMeetView` will automatically adjust its UI when presented in a
Picture-in-Picture style scenario, in a rectangle too small to accommodate its
"full" UI.
Jitsi Meet SDK automatically enables (unless explicitly disabled by a
`setPictureInPictureEnabled(false)` call) Android's native Picture-in-Picture
mode iff the platform is supported i.e. Android >= Oreo.

View File

@@ -31,10 +31,6 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {

View File

@@ -7,10 +7,12 @@
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:name=".MainActivity"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -23,6 +23,8 @@ import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetView;
import org.jitsi.meet.sdk.JitsiMeetViewListener;
import com.calendarevents.CalendarEventsPackage;
import java.util.Map;
/**
@@ -95,11 +97,18 @@ public class MainActivity extends JitsiMeetActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
// As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do
// want the Welcome page to be enabled. It defaults to disabled in the
// SDK at the time of this writing but it is clearer to be explicit
// about what we want anyway.
// want to enable some options.
// The welcome page defaults to disabled in the SDK at the time of this
// writing but it is clearer to be explicit about what we want anyway.
setWelcomePageEnabled(true);
super.onCreate(savedInstanceState);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults);
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

View File

@@ -2,6 +2,5 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowTranslucentStatus">true</item>
</style>
</resources>

View File

@@ -29,8 +29,10 @@ dependencies {
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-locale-detector')
compile project(':react-native-sound')
compile project(':react-native-vector-icons')
compile project(':react-native-webrtc')
compile project(':react-native-calendar-events')
}
// Build process helpers

View File

@@ -17,31 +17,32 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import java.net.URL;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
/**
* Base Activity for applications integrating Jitsi Meet at a higher level. It
* contains all the required wiring between the {@code JKConferenceView} and
* contains all the required wiring between the {@code JitsiMeetView} and
* the Activity lifecycle methods already implemented.
*
* In this activity we use a single {@code JKConferenceView} instance. This
* In this activity we use a single {@code JitsiMeetView} instance. This
* instance gives us access to a view which displays the welcome page and the
* conference itself. All lifetime methods associated with this Activity are
* hooked to the React Native subsystem via proxy calls through the
* {@code JKConferenceView} static methods.
* {@code JitsiMeetView} static methods.
*/
public class JitsiMeetActivity
extends AppCompatActivity {
public class JitsiMeetActivity extends AppCompatActivity {
/**
* The request code identifying requests for the permission to draw on top
* of other apps. The value must be 16-bit and is arbitrarily chosen here.
@@ -67,6 +68,12 @@ public class JitsiMeetActivity
*/
private JitsiMeetView view;
/**
* Whether Picture-in-Picture is enabled. The value is used only while
* {@link #view} equals {@code null}.
*/
private Boolean pictureInPictureEnabled;
/**
* Whether the Welcome page is enabled. The value is used only while
* {@link #view} equals {@code null}.
@@ -89,6 +96,17 @@ public class JitsiMeetActivity
return view == null ? defaultURL : view.getDefaultURL();
}
/**
*
* @see JitsiMeetView#getPictureInPictureEnabled()
*/
public boolean getPictureInPictureEnabled() {
return
view == null
? pictureInPictureEnabled
: view.getPictureInPictureEnabled();
}
/**
*
* @see JitsiMeetView#getWelcomePageEnabled()
@@ -121,6 +139,10 @@ public class JitsiMeetActivity
// XXX Before calling JitsiMeetView#loadURL, make sure to call whatever
// is documented to need such an order in order to take effect:
view.setDefaultURL(defaultURL);
if (pictureInPictureEnabled != null) {
view.setPictureInPictureEnabled(
pictureInPictureEnabled.booleanValue());
}
view.setWelcomePageEnabled(welcomePageEnabled);
view.loadURL(null);
@@ -199,17 +221,26 @@ public class JitsiMeetActivity
JitsiMeetView.onHostDestroy(this);
}
// ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java
@Override
public void onNewIntent(Intent intent) {
JitsiMeetView.onNewIntent(intent);
public boolean onKeyUp(int keyCode, KeyEvent event) {
ReactInstanceManager reactInstanceManager;
if (!super.onKeyUp(keyCode, event)
&& BuildConfig.DEBUG
&& (reactInstanceManager
= JitsiMeetView.getReactInstanceManager())
!= null
&& keyCode == KeyEvent.KEYCODE_MENU) {
reactInstanceManager.showDevOptionsDialog();
return true;
}
return false;
}
@Override
protected void onPause() {
super.onPause();
JitsiMeetView.onHostPause(this);
defaultBackButtonImpl = null;
public void onNewIntent(Intent intent) {
JitsiMeetView.onNewIntent(intent);
}
@Override
@@ -220,6 +251,19 @@ public class JitsiMeetActivity
JitsiMeetView.onHostResume(this, defaultBackButtonImpl);
}
@Override
public void onStop() {
super.onStop();
JitsiMeetView.onHostPause(this);
defaultBackButtonImpl = null;
}
@Override
protected void onUserLeaveHint() {
JitsiMeetView.onUserLeaveHint();
}
/**
*
* @see JitsiMeetView#setDefaultURL(URL)
@@ -232,6 +276,19 @@ public class JitsiMeetActivity
}
}
/**
*
* @see JitsiMeetView#setPictureInPictureEnabled(boolean)
*/
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
if (view == null) {
this.pictureInPictureEnabled
= Boolean.valueOf(pictureInPictureEnabled);
} else {
view.setPictureInPictureEnabled(pictureInPictureEnabled);
}
}
/**
*
* @see JitsiMeetView#setWelcomePageEnabled(boolean)

View File

@@ -21,17 +21,22 @@ import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.widget.FrameLayout;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.rnimmersive.RNImmersiveModule;
import java.net.URL;
@@ -49,6 +54,12 @@ public class JitsiMeetView extends FrameLayout {
*/
private static final int BACKGROUND_COLOR = 0xFF111111;
/**
* The {@link Log} tag which identifies the source of the log messages of
* {@code JitsiMeetView}.
*/
private final static String TAG = JitsiMeetView.class.getSimpleName();
/**
* React Native bridge. The instance manager allows embedding applications
* to create multiple root views off the same JavaScript bundle.
@@ -65,6 +76,7 @@ public class JitsiMeetView extends FrameLayout {
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new ExternalAPIModule(reactContext),
new PictureInPictureModule(reactContext),
new ProximityModule(reactContext),
new WiFiStatsModule(reactContext)
);
@@ -83,6 +95,11 @@ public class JitsiMeetView extends FrameLayout {
return null;
}
// XXX Strictly internal use only (at the time of this writing)!
static ReactInstanceManager getReactInstanceManager() {
return reactInstanceManager;
}
/**
* Internal method to initialize the React Native instance manager. We
* create a single instance in order to load the JavaScript bundle a single
@@ -97,6 +114,7 @@ public class JitsiMeetView extends FrameLayout {
.setApplication(application)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
@@ -105,6 +123,7 @@ public class JitsiMeetView extends FrameLayout {
.addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
.addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
.addPackage(new com.rnimmersive.RNImmersivePackage())
.addPackage(new com.zmxv.RNSound.RNSoundPackage())
.addPackage(new ReactPackageAdapter() {
@Override
public List<NativeModule> createNativeModules(
@@ -238,6 +257,38 @@ public class JitsiMeetView extends FrameLayout {
}
}
/**
* Activity lifecycle method which should be called from
* {@code Activity.onUserLeaveHint} so we can do the required internal
* processing.
*
* This is currently not mandatory.
*/
public static void onUserLeaveHint() {
sendEvent("onUserLeaveHint", null);
}
/**
* Helper function to send an event to JavaScript.
*
* @param eventName {@code String} containing the event name.
* @param params {@code WritableMap} optional ancillary data for the event.
*/
private static void sendEvent(
String eventName,
@Nullable WritableMap params) {
if (reactInstanceManager != null) {
ReactContext reactContext
= reactInstanceManager.getCurrentReactContext();
if (reactContext != null) {
reactContext
.getJSModule(
DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
}
/**
* The default base {@code URL} used to join a conference when a partial URL
* (e.g. a room name only) is specified to {@link #loadURLString(String)} or
@@ -259,6 +310,13 @@ public class JitsiMeetView extends FrameLayout {
*/
private JitsiMeetViewListener listener;
/**
* Whether Picture-in-Picture is enabled. If {@code null}, defaults to
* {@code true} iff the Android platform supports Picture-in-Picture
* natively.
*/
private Boolean pictureInPictureEnabled;
/**
* React Native root view.
*/
@@ -323,6 +381,21 @@ public class JitsiMeetView extends FrameLayout {
return listener;
}
/**
* Gets whether Picture-in-Picture is enabled. Picture-in-Picture is
* natively supported on Android API >= 26 (Oreo), so it should not be
* enabled on older platform versions.
*
* @return If Picture-in-Picture is enabled, {@code true}; {@code false},
* otherwise.
*/
public boolean getPictureInPictureEnabled() {
return
PictureInPictureModule.isPictureInPictureSupported()
&& (pictureInPictureEnabled == null
|| pictureInPictureEnabled.booleanValue());
}
/**
* Gets whether the Welcome page is enabled. If {@code true}, the Welcome
* page is rendered when this {@code JitsiMeetView} is not at a URL
@@ -364,12 +437,20 @@ public class JitsiMeetView extends FrameLayout {
if (defaultURL != null) {
props.putString("defaultURL", defaultURL.toString());
}
// externalAPIScope
props.putString("externalAPIScope", externalAPIScope);
// pictureInPictureEnabled
props.putBoolean(
"pictureInPictureEnabled",
getPictureInPictureEnabled());
// url
if (urlObject != null) {
props.putBundle("url", urlObject);
}
// welcomePageEnabled
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
@@ -387,7 +468,9 @@ public class JitsiMeetView extends FrameLayout {
if (reactRootView == null) {
reactRootView = new ReactRootView(getContext());
reactRootView.startReactApplication(
reactInstanceManager, "App", props);
reactInstanceManager,
"App",
props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
} else {
@@ -426,10 +509,29 @@ public class JitsiMeetView extends FrameLayout {
super.onWindowFocusChanged(hasFocus);
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
// FIXME The singleton pattern employed by RNImmersiveModule is not
// advisable because a react-native mobule is consumable only after its
// BaseJavaModule#initialize() has completed and here we have no
// knowledge of whether the precondition is really met.
RNImmersiveModule immersive = RNImmersiveModule.getInstance();
if (hasFocus && immersive != null) {
immersive.emitImmersiveStateChangeEvent();
try {
immersive.emitImmersiveStateChangeEvent();
} catch (RuntimeException re) {
// FIXME I don't know how to check myself whether
// BaseJavaModule#initialize() has been invoked and thus
// RNImmersiveModule is consumable. A safe workaround is to
// swallow the failure because the whole full-screen/immersive
// functionality is brittle anyway, akin to the icing on the
// cake, and has been working without onWindowFocusChanged for a
// very long time.
Log.e(
TAG,
"RNImmersiveModule#emitImmersiveStateChangeEvent() failed!",
re);
}
}
}
@@ -457,6 +559,18 @@ public class JitsiMeetView extends FrameLayout {
this.listener = listener;
}
/**
* Sets whether Picture-in-Picture is enabled. Because Picture-in-Picture is
* natively supported only since certain platform versions, specifying
* {@code true} will have no effect on unsupported platform versions.
*
* @param pictureInPictureEnabled To enable Picture-in-Picture,
* {@code true}; otherwise, {@code false}.
*/
public void setPictureInPictureEnabled(boolean pictureInPictureEnabled) {
this.pictureInPictureEnabled = Boolean.valueOf(pictureInPictureEnabled);
}
/**
* Sets whether the Welcome page is enabled. Must be called before
* {@link #loadURL(URL)} for it to take effect.

View File

@@ -0,0 +1,67 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.os.Build;
import android.util.Log;
import android.util.Rational;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class PictureInPictureModule extends ReactContextBaseJavaModule {
private final static String TAG = "PictureInPicture";
static boolean isPictureInPictureSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public PictureInPictureModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Enters Picture-in-Picture (mode) for the current {@link Activity}.
* Supported on Android API >= 26 (Oreo) only.
*
* @param promise a {@code Promise} which will resolve with a {@code null}
* value upon success, and an {@link Exception} otherwise.
*/
@ReactMethod
public void enterPictureInPicture(Promise promise) {
if (isPictureInPictureSupported()) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
promise.reject(new Exception("No current Activity!"));
return;
}
Log.d(TAG, "Entering Picture-in-Picture");
PictureInPictureParams.Builder builder
= new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(1, 1));
boolean r
= currentActivity.enterPictureInPictureMode(builder.build());
if (r) {
promise.resolve(null);
} else {
promise.reject(
new Exception("Failed to enter Picture-in-Picture"));
}
return;
}
promise.reject(new Exception("Picture-in-Picture not supported"));
}
@Override
public String getName() {
return TAG;
}
}

View File

@@ -11,7 +11,11 @@ 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-locale-detector'
project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
include ':react-native-sound'
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-webrtc'
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
include ':react-native-calendar-events'
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')

View File

@@ -7,7 +7,7 @@ import Recorder from './modules/recorder/Recorder';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import { reload, reportError } from './modules/util/helpers';
import { reportError } from './modules/util/helpers';
import * as RemoteControlEvents
from './service/remotecontrol/RemoteControlEvents';
@@ -24,6 +24,10 @@ import {
initAnalytics,
sendAnalytics
} from './react/features/analytics';
import {
redirectWithStoredParams,
reloadWithStoredParams
} from './react/features/app';
import EventEmitter from 'events';
@@ -216,7 +220,7 @@ function maybeRedirectToWelcomePage(options) {
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', isGuest);
assignWindowLocationPathname(`static/${
redirectToStaticPage(`static/${
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`);
return;
@@ -234,8 +238,7 @@ function maybeRedirectToWelcomePage(options) {
if (config.enableWelcomePage) {
setTimeout(
() => {
APP.settings.setWelcomePageEnabled(true);
assignWindowLocationPathname('./');
APP.store.dispatch(redirectWithStoredParams('/'));
},
3000);
}
@@ -251,7 +254,7 @@ function maybeRedirectToWelcomePage(options) {
* assigning it to window.location.pathname.
* @return {void}
*/
function assignWindowLocationPathname(pathname) {
function redirectToStaticPage(pathname) {
const windowLocation = window.location;
let newPathname = pathname;
@@ -311,7 +314,7 @@ class ConferenceConnector {
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
// let's show some auth not allowed page
assignWindowLocationPathname('static/authError.html');
redirectToStaticPage('static/authError.html');
break;
}
@@ -379,7 +382,7 @@ class ConferenceConnector {
break;
case JitsiConferenceErrors.INCOMPATIBLE_SERVER_VERSIONS:
reload();
APP.store.dispatch(reloadWithStoredParams());
break;
default:
@@ -751,7 +754,7 @@ export default {
track.mute();
}
});
logger.log('initialized with %s local tracks', tracks.length);
logger.log(`initialized with ${tracks.length} local tracks`);
this._localTracksInitialized = true;
con.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
@@ -1337,19 +1340,35 @@ export default {
replaceLocalTrack(this.localVideo, newStream, room))
.then(() => {
this.localVideo = newStream;
this._setSharingScreen(newStream);
if (newStream) {
this.isSharingScreen = newStream.videoType === 'desktop';
APP.UI.addLocalStream(newStream);
} else {
this.isSharingScreen = false;
}
this.setVideoMuteStatus(this.isLocalVideoMuted());
APP.UI.updateDesktopSharingButtons();
});
},
/**
* Sets `this.isSharingScreen` depending on provided video stream.
* In case new screen sharing status is not equal previous one
* it updates desktop sharing buttons in UI
* and notifies external application.
*
* @param {JitsiLocalTrack} [newStream] new stream to use or null
* @private
* @returns {void}
*/
_setSharingScreen(newStream) {
const wasSharingScreen = this.isSharingScreen;
this.isSharingScreen = newStream && newStream.videoType === 'desktop';
if (wasSharingScreen !== this.isSharingScreen) {
APP.UI.updateDesktopSharingButtons();
APP.API.notifyScreenSharingStatusChanged(this.isSharingScreen);
}
},
/**
* Start using provided audio stream.
* Stops previous audio stream.
@@ -1766,6 +1785,9 @@ export default {
});
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
if (user.isHidden()) {
return;
}
APP.store.dispatch(participantLeft(id, user));
logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
@@ -2623,6 +2645,7 @@ export default {
*/
hangup(requestFeedback = false) {
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
APP.UI.removeLocalMedia();
let requestFeedbackPromise;

View File

@@ -167,6 +167,12 @@ var config = {
// The URL to the Firefox extension for desktop sharing.
desktopSharingFirefoxExtensionURL: null,
// Optional desktop sharing frame rate options. Default value: min:5, max:5.
// desktopSharingFrameRate: {
// min: 5,
// max: 5
// },
// Try to start calls with screen-sharing instead of camera video.
// startScreenSharing: false,
@@ -239,8 +245,11 @@ var config = {
// Stats
//
// Whether to enable stats collection or not.
// disableStats: false,
// Whether to enable stats collection or not in the TraceablePeerConnection.
// This can be useful for debugging purposes (post-processing/analysis of
// the webrtc stats) as it is done in the jitsi-meet-torture bandwidth
// estimation tests.
// gatherStats: false,
// To enable sending statistics to callstats.io you must provide the
// Application ID and Secret.

View File

@@ -1,81 +0,0 @@
/**
* The dialog content element.
*/
.dial-out-content {
margin-top: 5px;
/**
* Wrap the contents in flex so items can be aligned on the same line.
*/
.form-control {
display: flex;
}
/**
* The style of the flag icon.
*/
.dial-out-flag-icon {
position: absolute;
left: 5px;
top: 50%;
transform: translate(0, -50%);
}
/**
* The style of the dial code element.
*/
.dial-out-code {
margin-bottom: 0;
padding-left: 25px;
}
/**
* The dial-out dialog error element.
*/
.dial-out-error {
color: $errorColor;
}
/**
* The style of the dial input element.
*/
.dial-out-input {
display: inline-block;
flex: 1;
margin-left: 5px;
}
/**
* Re-styling the default dropdown inside the dial-out-content.
*/
.dropdown {
position: relative;
width: 65px;
}
/**
* Re-styling the default form-control inside the dial-out-content.
*/
.form-control {
margin-bottom: 8px;
}
.dropdown {
position: relative;
input {
padding-left: 16px;
&:read-only {
color: inherit;
}
}
}
.dropdown-trigger-icon {
position: absolute;
right: 0;
top: 50%;
transform: translate(0, -50%);
}
}

View File

@@ -1,35 +0,0 @@
.flag-icon-background {
background-size: contain;
background-position: 50%;
background-repeat: no-repeat;
}
.flag-icon {
background-size: contain;
background-position: 50%;
background-repeat: no-repeat;
position: relative;
display: inline-block;
width: 1.33333333em;
line-height: 1em;
}
.flag-icon:before {
content: "\00a0";
}
.flag-icon-au {
background-image: url(../images/countries/au.svg);
}
.flag-icon-ca {
background-image: url(../images/countries/ca.svg);
}
.flag-icon-de {
background-image: url(../images/countries/de.svg);
}
.flag-icon-gb {
background-image: url(../images/countries/gb.svg);
}
.flag-icon-fr {
background-image: url(../images/countries/fr.svg);
}
.flag-icon-us {
background-image: url(../images/countries/us.svg);
}

View File

@@ -30,12 +30,21 @@
.icon-event_note:before {
content: "\e616";
}
.icon-menu:before {
content: "\e5d2";
}
.icon-navigate_before:before {
content: "\e408";
}
.icon-navigate_next:before {
content: "\e409";
}
.icon-public:before {
content: "\e80b";
}
.icon-restore:before {
content: "\e8b3";
}
.icon-timer:before {
content: "\e425";
}

View File

@@ -151,4 +151,12 @@ $unsupportedDesktopBrowserTextFontSize: 21px;
* The size of the default watermark.
*/
$watermarkWidth: 186px;
$watermarkHeight: 74px;
$watermarkHeight: 74px;
/**
* Welcome page variables.
*/
$welcomePageDescriptionColor: #fff;
$welcomePageFontFamily: inherit;
$welcomePageHeaderBackground: linear-gradient(#165ecc, #44A5FF);
$welcomePageTitleColor: #fff;

View File

@@ -1,208 +1,87 @@
#welcome_page {
.welcome {
font-family: $welcomePageFontFamily;
height: 100%;
overflow: auto;
position: relative;
}
#disable_welcome {
display:none;
}
.disable_welcome_position
{
margin: -139px auto 0px auto;
padding-left: 39px;
padding-top: 7px;
width: 269px;
height: 31px;
display:block;
}
#disable_welcome + label
{
background-image: url(../images/welcome_page/disable-welcome.png);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
background-repeat: no-repeat;
font-weight: 500;
font-size: 16px;
color: #acacac;
z-index: $zindex2;
}
#disable_welcome:checked + label
{
background-image: url(../images/welcome_page/disable-welcome-selected.png);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
background-repeat: no-repeat;
font-weight: 500;
font-size: 16px;
color: #acacac;
z-index: $zindex2;
}
#enter_room_form {
border-radius: 1px;
background-color: #FFFFFF;
border: none;
-moz-border-radius: 1px;
-webkit-border-radius: 1px;
-webkit-appearance: none;
height: 55px;
box-shadow: none;
float: left;
}
.domain-name
{
float: left;
height: 55px;
line-height: 55px;
font-size: 18px;
font-weight: 500;
padding-left: 20px;
color: $defaultDarkColor;
}
.enter-room {
&__field {
font-size: 15px;
border: none;
-webkit-appearance: none;
width: 228px;
height: 55px;
line-height: 55px;
font-weight: 500;
box-shadow: none;
float: left;
background-color: #FFFFFF;
.header {
align-items: center;
background: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
z-index: $zindex2;
}
&__reload {
display: block;
width: 30px;
color: #acacac;
font-size: 1.9em;
line-height: 55px;
z-index: $zindex3;
float: left;
cursor: pointer;
text-align: center;
.header-text {
display: flex;
flex-direction: column;
justify-content: space-around;
margin-top: 120px;
margin-bottom: 20px;
min-height: 286px;
width: 645px;
}
.header-text-title {
color: $welcomePageTitleColor;
font-size: 48px;
letter-spacing: -1px;
line-height: 58px;
margin-bottom: 20px;
}
.header-text-description {
color: $welcomePageDescriptionColor;
font-size: 20px;
line-height: 28px;
opacity: 0.8;
}
.header-image {
background-image: url(../images/welcome_page/curves.png);
background-size: contain;
height: 209px;
position: absolute;
width: 1070px;
}
#new_enter_room {
align-items: center;
display: flex;
margin-bottom: 20px;
position: relative;
z-index: 2;
.enter-room-input {
display: inline-block;
margin-right: 15px;
width: 350px;
}
}
}
&__button {
width: 73px;
height: 45px;
background-color: #21B9FC;
moz-border-radius: 1px;
-webkit-border-radius: 1px;
color: #ffffff;
font-weight: 600;
border: none;
margin-top: 5px;
font-size: 19px;
padding-top: 6px;
outline: none;
float:left;
position: relative;
z-index: $zindex2;
.welcome-page-button {
font-size: 16px;
}
}
#enter_room_container {
margin: 70px auto 0px auto;
display: table;
.welcome.with-content {
.header {
min-height: 552px;
}
.header-image {
left: -61px;
top: 401px;
}
}
#enter_room{
float:left;
padding-right: 5px;
}
#welcome_page_header
{
background-image: url(../images/welcome_page/pattern-header.png);
height: 290px;
width: 100%;
position: absolute;
}
#welcome_page_main
{
background-image:url(../images/welcome_page/pattern-body.png);
width: 100%;
position: absolute;
margin-top: 290px;
}
#brand_header
{
background-image:url(../images/welcome_page/header-big.png);
width: 583px;
height: 274px;
margin: -110px auto 0px auto;
}
#header_text
{
width: 885px;
height: 100px;
color: #ffffff;
font-size: 24px;
text-align: center;
margin: 0px auto 0px auto;
}
#features
{
margin-top: 30px;
position: relative;
}
.feature_row
{
position: relative;
width: 976px;
margin: 0px auto 30px auto;
padding-right: 75px;
}
.feature_holder
{
display: inline-block;
width: 169px;
padding-left: 75px;
padding-bottom: 30px;
vertical-align: top;
}
.feature_icon
{
background-image:url(../images/welcome_page/bubble.png);
background-repeat: no-repeat;
width: 169px;
height: 169px;
color: #ffffff;
font-size: 22px;
/*font-weight: bold;*/
text-align: center;
display: table-cell;
padding: 0px 26px 0px 20px;
vertical-align: middle;
}
.feature_description
{
width: 190px;
color: #ffffff;
font-size: 16px;
padding-top: 30px;
line-height: 22px;
font-weight: 200;
.welcome.without-content {
.header {
height: 100%;
}
.header-image {
bottom: -20px;
left: 0;
}
}

View File

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

View File

@@ -28,11 +28,8 @@
@import 'font-awesome';
/* Fonts END */
@import 'flag-icon';
/* Modules BEGIN */
@import 'dial-out';
@import 'aui_reset';
@import 'base';
@import 'utils';
@@ -55,6 +52,7 @@
@import 'chat';
@import 'ringing/ringing';
@import 'welcome_page';
@import 'welcome_page_content';
@import 'toolbars';
@import 'side_toolbar_container';
@import 'jquery.contextMenu';

View File

@@ -11,17 +11,11 @@
padding-left: 5px;
}
}
}
}
/**
* Styles the loading element in the MultiSelectAutocomplete.
*/
.autocomplete-loading {
justify-content: center;
display: flex;
min-width: 260px;
padding: 20px;
.add-telephone-icon {
transform: scaleX(-1);
}
}
}
/**

View File

@@ -59,11 +59,17 @@
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
user-select: text;
white-space: nowrap;
}
.info-dialog-dial-in {
white-space: nowrap;
.conference-id,
.phone-number {
user-select: text;
}
}
.info-dialog-icon {
@@ -101,6 +107,10 @@
padding-left: 0;
}
.info-password-local {
user-select: text;
}
.conference-id {
margin-left: 5px;
}
@@ -115,6 +125,10 @@
justify-content: center;
width: 100%;
* {
user-select: text;
}
.dial-in-numbers-list {
font-size: 24px;
margin-top: 20px;

View File

@@ -1,7 +1,8 @@
.unsupported-mobile-browser {
background-color: #fff;
height: 100vh;
padding: 35px 0;
overflow: auto;
position: relative;
width: 100vw;
a {
@@ -12,6 +13,7 @@
color: $unsupportedBrowserTextColor;
margin: auto;
max-width: 40em;
padding: 35px 0 40px 0;
text-align: center;
width: 75%;
@@ -20,7 +22,8 @@
}
}
&__text {
&__text,
.unsupported-dial-in {
font-size: 1.2em;
line-height: em(29px, 21px);
margin-bottom: 0.65em;
@@ -65,4 +68,22 @@
}
}
}
.unsupported-dial-in {
display: none;
&.has-numbers {
align-items: center;
display: flex;
flex-direction: column;
}
.dial-in-numbers-list {
color: $unsupportedBrowserTextColor;
}
.dial-in-numbers-body {
vertical-align: top;
}
}
}

View File

@@ -125,8 +125,11 @@ case "$1" in
# echo for using all default values
echo | prosodyctl cert generate $JICOFO_AUTH_DOMAIN
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.key /etc/prosody/certs/$JICOFO_AUTH_DOMAIN.key
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt /etc/prosody/certs/$JICOFO_AUTH_DOMAIN.crt
AUTH_KEY_FILE="/etc/prosody/certs/$JICOFO_AUTH_DOMAIN.key"
AUTH_CRT_FILE="/etc/prosody/certs/$JICOFO_AUTH_DOMAIN.crt"
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.key $AUTH_KEY_FILE
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt $AUTH_CRT_FILE
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt /usr/local/share/ca-certificates/$JICOFO_AUTH_DOMAIN.crt
update-ca-certificates

View File

@@ -164,6 +164,13 @@ changes. The listener will receive an object with the following structure:
}
```
* **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure:
```javascript
{
"on": on //whether screen sharing is on
}
```
* **incomingMessage** - Event notifications about incoming
messages. The listener will receive an object with the following structure:
```javascript

Binary file not shown.

View File

@@ -12,8 +12,10 @@
<glyph unicode="&#xe1aa;" glyph-name="bluetooth" d="M550 328l-80 82v-162zM470 776v-162l80 82zM670 696l-184-184 184-184-244-242h-42v324l-196-196-60 60 238 238-238 238 60 60 196-196v324h42zM834 738c40-64 62-142 62-222 0-84-24-160-66-226l-50 50c26 52 42 110 42 172s-16 120-42 172zM608 512l98 98c12-30 20-64 20-98s-8-70-20-100z" />
<glyph unicode="&#xe310;" glyph-name="headset" d="M512 982c212 0 384-172 384-384v-300c0-70-58-128-128-128h-128v342h170v86c0 166-132 298-298 298s-298-132-298-298v-86h170v-342h-128c-70 0-128 58-128 128v300c0 212 172 384 384 384z" />
<glyph unicode="&#xe408;" glyph-name="navigate_before" d="M658 708l-196-196 196-196-60-60-256 256 256 256z" />
<glyph unicode="&#xe409;" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
<glyph unicode="&#xe425;" glyph-name="timer" d="M512 170c166 0 298 134 298 300s-132 298-298 298-298-132-298-298 132-300 298-300zM812 708c52-66 84-148 84-238 0-212-172-384-384-384s-384 172-384 384 172 384 384 384c90 0 174-34 240-86l60 62c22-18 42-38 60-60zM470 426v256h84v-256h-84zM640 982v-86h-256v86h256z" />
<glyph unicode="&#xe5c4;" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
<glyph unicode="&#xe5d2;" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
<glyph unicode="&#xe5d4;" glyph-name="thumb-menu" d="M512 342c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 598c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 682c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe603;" glyph-name="presentation" horiz-adv-x="1088" d="M952.495 1019.065h-818.689c-72.81 0-132.183-60.63-132.183-135.162v-750.719c0-74.473 59.372-135.101 132.183-135.101h818.686c72.936 0 132.314 60.625 132.314 135.101v750.722c0.003 74.532-59.378 135.159-132.311 135.159zM946.346 139.651h-806.14v737.822h806.015l0.126-737.822zM685.753 738.544h216.911v-566.758h-216.911v566.758zM428.672 610.002h216.911v-438.216h-216.911v438.216zM172.339 481.46h216.161v-309.677h-216.161v309.677z" />
<glyph unicode="&#xe613;" glyph-name="recDisable" horiz-adv-x="1140" d="M1123.444 1003.015c-23.593 26.481-64.131 28.989-90.74 5.395l-1008.269-893.436c-26.609-23.468-28.991-64.131-5.46-90.676 12.674-14.306 30.308-21.649 48.126-21.649 15.123 0 30.372 5.401 42.544 16.195l130.045 115.22c90.743-81.844 210.569-132.165 342.473-132.101 282.816 0.061 510.913 227.969 511.287 510.972 0.126 109.934-34.682 211.367-93.499 294.72l118.088 104.625c26.483 23.526 28.997 64.129 5.404 90.735zM944.422 513.818c0.128-200.922-161.896-363.201-362.509-362.952-87.56 0.123-167.573 31.151-230.061 82.569l331.277 293.509v-73.176c1.071-60.993 32.696-92.18 94.944-93.692 61.997 1.512 93.686 32.763 95.131 93.756v41.096h-72.227v-47.499c0.251-4.642-0.564-10.607-2.511-17.949-1.25-3.261-3.448-6.020-6.525-8.093-3.197-2.572-7.845-3.828-13.868-3.828-10.543 0.31-17.132 4.268-19.827 11.921-1.068 3.512-1.947 6.905-2.508 10.163-0.254 2.887-0.377 5.532-0.377 7.786v143.511l42.477 37.634c0.215-0.432 0.452-0.851 0.63-1.303 1.947-6.467 2.762-12.799 2.511-19.076v-36.772h72.227v30.121c-0.246 31.245-9.086 54.699-26.363 70.447l40.711 36.069c35.787-56.055 56.803-122.585 56.867-194.244zM239.795 395.47c-12.613 37.023-19.827 76.557-19.827 117.913-0.19 200.236 161.584 362.009 361.945 362.135 56.853 0 110.313-13.302 158.133-36.398l117.846 104.421c-79.444 50.952-173.758 80.817-275.292 80.948-283.377 0.181-511.354-227.729-511.789-511.675-0.126-79.567 18.636-154.679 51.137-221.882l117.848 104.538zM388.576 690.020h-97.514v-249.057l72.23 64.070v0.689h0.815l117.72 104.418c0 0.564 0.123 0.94 0.123 1.509 0.753 53.898-30.369 80.069-93.374 78.37zM405.959 625.517c1.942-2.767 3.074-6.469 3.323-11.112 0.312-4.452 0.438-9.6 0.438-15.246 0.251-10.916-0.689-19.83-2.949-26.985-2.952-7.594-10.983-11.357-24.159-11.357h-19.325v74.043h15.31c7.842 0 13.865-0.683 18.072-2.19 4.397-1.573 7.468-3.953 9.29-7.153z" />
@@ -21,6 +23,7 @@
<glyph unicode="&#xe616;" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
<glyph unicode="&#xe61d;" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
<glyph unicode="&#xe80b;" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
<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" />

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,87 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M512 342h64v180l150 90-32 52-182-110v-212zM554 128c212 0 384 172 384 384s-172 384-384 384c-106 0-200-42-270-112l60-62c54 54 128 88 210 88 166 0 300-132 300-298s-134-298-300-298-298 132-298 298h128l-172 172-4-6-166-166h128c0-212 172-384 384-384z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"restore"
],
"defaultCode": 59571,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "history, restore",
"id": 385,
"order": 930,
"prevSize": 24,
"code": 59571,
"name": "restore"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 385
},
{
"icon": {
"paths": [
"M426 256l256 256-256 256-60-60 196-196-196-196z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"navigate_next"
],
"defaultCode": 58377,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "chevron_right, navigate_next",
"id": 153,
"order": 927,
"prevSize": 24,
"code": 58377,
"name": "navigate_next"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
},
{
"icon": {
"paths": [
"M128 256h768v86h-768v-86zM128 554v-84h768v84h-768zM128 768v-86h768v86h-768z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"menu"
],
"defaultCode": 58834,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "menu",
"id": 489,
"order": 926,
"prevSize": 24,
"code": 58834,
"name": "menu"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 1
},
{
"icon": {
"paths": [
@@ -24,9 +105,9 @@
"code": 58820,
"name": "arrow_back"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 45
"setIdx": 1,
"setId": 1,
"iconIdx": 2
},
{
"icon": {
@@ -51,9 +132,9 @@
"code": 58376,
"name": "navigate_before"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 152
"setIdx": 1,
"setId": 1,
"iconIdx": 3
},
{
"icon": {
@@ -80,7 +161,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
"iconIdx": 4
},
{
"icon": {
@@ -107,7 +188,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 1
"iconIdx": 5
},
{
"icon": {
@@ -127,14 +208,14 @@
"properties": {
"ligatures": "timer",
"id": 760,
"order": 916,
"order": 928,
"prevSize": 24,
"code": 58405,
"name": "timer"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 2
"iconIdx": 6
},
{
"icon": {
@@ -161,7 +242,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 3
"iconIdx": 7
},
{
"icon": {
@@ -188,7 +269,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 4
"iconIdx": 8
},
{
"icon": {
@@ -215,7 +296,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 5
"iconIdx": 9
},
{
"icon": {
@@ -242,7 +323,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 6
"iconIdx": 10
},
{
"icon": {
@@ -271,7 +352,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 7
"iconIdx": 11
},
{
"icon": {
@@ -298,7 +379,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 8
"iconIdx": 12
},
{
"icon": {
@@ -325,7 +406,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 9
"iconIdx": 13
},
{
"icon": {
@@ -354,7 +435,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 10
"iconIdx": 14
},
{
"icon": {
@@ -383,7 +464,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 11
"iconIdx": 15
},
{
"icon": {
@@ -412,7 +493,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 12
"iconIdx": 16
},
{
"icon": {
@@ -441,7 +522,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 13
"iconIdx": 17
},
{
"icon": {
@@ -470,7 +551,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 14
"iconIdx": 18
},
{
"icon": {
@@ -496,7 +577,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 15
"iconIdx": 19
},
{
"icon": {
@@ -522,7 +603,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 16
"iconIdx": 20
},
{
"icon": {
@@ -548,7 +629,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 17
"iconIdx": 21
},
{
"icon": {
@@ -574,7 +655,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 18
"iconIdx": 22
},
{
"icon": {
@@ -600,7 +681,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 19
"iconIdx": 23
},
{
"icon": {
@@ -626,7 +707,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 20
"iconIdx": 24
},
{
"icon": {
@@ -652,7 +733,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 21
"iconIdx": 25
},
{
"icon": {
@@ -678,7 +759,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 22
"iconIdx": 26
},
{
"icon": {
@@ -704,7 +785,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 23
"iconIdx": 27
},
{
"icon": {
@@ -730,7 +811,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 24
"iconIdx": 28
},
{
"icon": {
@@ -756,7 +837,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 25
"iconIdx": 29
},
{
"icon": {
@@ -782,7 +863,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 26
"iconIdx": 30
},
{
"icon": {
@@ -808,7 +889,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 27
"iconIdx": 31
},
{
"icon": {
@@ -834,7 +915,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 28
"iconIdx": 32
},
{
"icon": {
@@ -860,7 +941,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 29
"iconIdx": 33
},
{
"icon": {
@@ -886,7 +967,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 30
"iconIdx": 34
},
{
"icon": {
@@ -912,7 +993,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 31
"iconIdx": 35
},
{
"icon": {
@@ -938,7 +1019,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 32
"iconIdx": 36
},
{
"icon": {
@@ -964,7 +1045,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 33
"iconIdx": 37
},
{
"icon": {
@@ -990,7 +1071,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 34
"iconIdx": 38
},
{
"icon": {
@@ -1016,7 +1097,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 35
"iconIdx": 39
},
{
"icon": {
@@ -1042,7 +1123,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 36
"iconIdx": 40
},
{
"icon": {
@@ -1068,7 +1149,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 37
"iconIdx": 41
},
{
"icon": {
@@ -1094,7 +1175,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 38
"iconIdx": 42
},
{
"icon": {
@@ -1120,7 +1201,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 39
"iconIdx": 43
},
{
"icon": {
@@ -1146,7 +1227,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 40
"iconIdx": 44
},
{
"icon": {
@@ -1172,7 +1253,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 41
"iconIdx": 45
},
{
"icon": {
@@ -1198,7 +1279,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 42
"iconIdx": 46
},
{
"icon": {
@@ -1224,7 +1305,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 43
"iconIdx": 47
},
{
"icon": {
@@ -1253,7 +1334,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 44
"iconIdx": 48
},
{
"icon": {
@@ -1283,7 +1364,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 45
"iconIdx": 49
},
{
"icon": {
@@ -1313,7 +1394,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 46
"iconIdx": 50
},
{
"icon": {
@@ -1339,7 +1420,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 47
"iconIdx": 51
},
{
"icon": {
@@ -1365,7 +1446,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 48
"iconIdx": 52
},
{
"icon": {
@@ -1391,7 +1472,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 49
"iconIdx": 53
}
],
"height": 1024,

BIN
images/calendar@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/calendar@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
<g stroke-width="1pt">
<path fill="#006" d="M0 0h640v480H0z"/>
<path d="M0 0v27.95L307.037 250h38.647v-27.95L38.647 0H0zm345.684 0v27.95L38.647 250H0v-27.95L307.037 0h38.647z" fill="#fff"/>
<path d="M144.035 0v250h57.614V0h-57.615zM0 83.333v83.333h345.684V83.333H0z" fill="#fff"/>
<path d="M0 100v50h345.684v-50H0zM155.558 0v250h34.568V0h-34.568zM0 250l115.228-83.334h25.765L25.765 250H0zM0 0l115.228 83.333H89.463L0 18.633V0zm204.69 83.333L319.92 0h25.764L230.456 83.333H204.69zM345.685 250l-115.228-83.334h25.765l89.464 64.7V250z" fill="#c00"/>
<path d="M299.762 392.523l-43.653 3.795 6.013 43.406-30.187-31.764-30.186 31.764 6.014-43.406-43.653-3.795 37.68-22.364-24.244-36.495 40.97 15.514 13.42-41.713 13.42 41.712 40.97-15.515-24.242 36.494m224.444 62.372l-10.537-15.854 17.81 6.742 5.824-18.125 5.825 18.126 17.807-6.742-10.537 15.854 16.37 9.718-18.965 1.65 2.616 18.85-13.116-13.793-13.117 13.794 2.616-18.85-18.964-1.65m16.368-291.815l-10.537-15.856 17.81 6.742 5.824-18.122 5.825 18.12 17.807-6.74-10.537 15.855 16.37 9.717-18.965 1.65 2.616 18.85-13.116-13.793-13.117 13.794 2.616-18.85-18.964-1.65m-89.418 104.883l-10.537-15.853 17.808 6.742 5.825-18.125 5.825 18.125 17.808-6.742-10.536 15.853 16.37 9.72-18.965 1.65 2.615 18.85-13.117-13.795-13.117 13.795 2.617-18.85-18.964-1.65m216.212-37.929l-10.558-15.854 17.822 6.742 5.782-18.125 5.854 18.125 17.772-6.742-10.508 15.854 16.362 9.718-18.97 1.65 2.608 18.85-13.118-13.793-13.117 13.793 2.61-18.85-18.936-1.65m-22.251 73.394l-10.367 6.425 2.914-11.84-9.316-7.863 12.165-.896 4.605-11.29 4.606 11.29 12.165.897-9.317 7.863 2.912 11.84" fill-rule="evenodd" fill="#fff"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
<g transform="translate(74.118) scale(.9375)">
<path fill="#fff" d="M81.137 0h362.276v512H81.137z"/>
<path fill="#bf0a30" d="M-100 0H81.138v512H-100zm543.413 0H624.55v512H443.414zM135.31 247.41l-14.067 4.808 65.456 57.446c4.95 14.764-1.72 19.116-5.97 26.86l71.06-9.02-1.85 71.512 14.718-.423-3.21-70.918 71.13 8.432c-4.402-9.297-8.32-14.233-4.247-29.098l65.414-54.426-11.447-4.144c-9.36-7.222 4.044-34.784 6.066-52.178 0 0-38.195 13.135-40.698 6.262l-9.727-18.685-34.747 38.17c-3.796.91-5.413-.6-6.304-3.808l16.053-79.766-25.42 14.297c-2.128.91-4.256.125-5.658-2.355l-24.45-49.06-25.21 50.95c-1.9 1.826-3.803 2.037-5.382.796l-24.204-13.578 14.53 79.143c-1.156 3.14-3.924 4.025-7.18 2.324l-33.216-37.737c-4.345 6.962-7.29 18.336-13.033 20.885-5.744 2.387-24.98-4.823-37.873-7.637 4.404 15.895 18.176 42.302 9.46 50.957z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 934 B

View File

@@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
<path fill="#ffce00" d="M0 320h640v160.002H0z"/>
<path d="M0 0h640v160H0z"/>
<path fill="#d00" d="M0 160h640v160H0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 220 B

View File

@@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
<g fill-rule="evenodd" stroke-width="1pt">
<path fill="#fff" d="M0 0h640v480H0z"/>
<path fill="#00267f" d="M0 0h213.337v480H0z"/>
<path fill="#f31830" d="M426.662 0H640v480H426.662z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 301 B

View File

@@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
<defs>
<clipPath id="a">
<path fill-opacity=".67" d="M-85.333 0h682.67v512h-682.67z"/>
</clipPath>
</defs>
<g clip-path="url(#a)" transform="translate(80) scale(.94)">
<g stroke-width="1pt">
<path fill="#006" d="M-256 0H768.02v512.01H-256z"/>
<path d="M-256 0v57.244l909.535 454.768H768.02V454.77L-141.515 0H-256zM768.02 0v57.243L-141.515 512.01H-256v-57.243L653.535 0H768.02z" fill="#fff"/>
<path d="M170.675 0v512.01h170.67V0h-170.67zM-256 170.67v170.67H768.02V170.67H-256z" fill="#fff"/>
<path d="M-256 204.804v102.402H768.02V204.804H-256zM204.81 0v512.01h102.4V0h-102.4zM-256 512.01L85.34 341.34h76.324l-341.34 170.67H-256zM-256 0L85.34 170.67H9.016L-256 38.164V0zm606.356 170.67L691.696 0h76.324L426.68 170.67h-76.324zM768.02 512.01L426.68 341.34h76.324L768.02 473.848v38.162z" fill="#c00"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

View File

@@ -1,18 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
<g fill-rule="evenodd" transform="scale(.9375)">
<g stroke-width="1pt">
<path d="M0 0h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0z" fill="#bd3d44"/>
<path d="M0 39.385h972.81V78.77H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0z" fill="#fff"/>
</g>
<path fill="#192f5d" d="M0 0h389.12v275.69H0z"/>
<g fill="#fff">
<path d="M32.427 11.8l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 39.37l3.54 10.896h11.458L70.583 57l3.542 10.897-9.27-6.734-9.269 6.734L59.126 57l-9.269-6.734h11.458zm64.852 0l3.54 10.896h11.457L135.435 57l3.54 10.897-9.268-6.734-9.27 6.734L123.978 57l-9.27-6.734h11.458zm64.855 0l3.54 10.896h11.458L200.29 57l3.541 10.897-9.27-6.734-9.268 6.734L188.833 57l-9.269-6.734h11.457zm64.855 0l3.54 10.896h11.458L265.145 57l3.541 10.897-9.269-6.734-9.27 6.734L253.69 57l-9.27-6.734h11.458zm64.852 0l3.54 10.896h11.457L329.997 57l3.54 10.897-9.268-6.734-9.27 6.734L318.54 57l-9.27-6.734h11.458zM32.427 66.939l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 94.508l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zM32.427 122.078l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 149.647l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458z"/>
<g>
<path d="M32.427 177.217l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 204.786l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458z"/>
</g>
<g>
<path d="M32.427 232.356l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -138,6 +138,7 @@
<script src="libs/app.bundle.min.js?v=139"></script>
<!--#include virtual="title.html" -->
<!--#include virtual="plugin.head.html" -->
<!--#include virtual="static/welcomePageAdditionalContent.html" -->
</head>
<body>
<div id="react"></div>

View File

@@ -24,6 +24,7 @@ var interfaceConfig = {
BRAND_WATERMARK_LINK: '',
SHOW_POWERED_BY: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
DISPLAY_WELCOME_PAGE_CONTENT: true,
APP_NAME: 'Jitsi Meet',
LANG_DETECTION: false, // Allow i18n to detect the system language
INVITATION_POWERED_BY: true,

View File

@@ -28,7 +28,10 @@ target 'JitsiMeet' do
pod 'react-native-locale-detector',
:path => '../node_modules/react-native-locale-detector'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'react-native-calendar-events',
:path => '../node_modules/react-native-calendar-events'
end
post_install do |installer|

View File

@@ -3,6 +3,8 @@ PODS:
- React/Core (= 0.51.0)
- react-native-background-timer (2.0.0):
- React
- react-native-calendar-events (1.4.3):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
- react-native-keep-awake (2.0.6):
@@ -41,12 +43,18 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- RNSound (0.10.4):
- React/Core
- RNSound/Core (= 0.10.4)
- RNSound/Core (0.10.4):
- React/Core
- RNVectorIcons (4.4.2):
- React
- yoga (0.51.0.React)
DEPENDENCIES:
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
@@ -61,6 +69,7 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- RNSound (from `../node_modules/react-native-sound`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -69,6 +78,8 @@ EXTERNAL SOURCES:
:path: ../node_modules/react-native
react-native-background-timer:
:path: ../node_modules/react-native-background-timer
react-native-calendar-events:
:path: ../node_modules/react-native-calendar-events
react-native-fetch-blob:
:path: ../node_modules/react-native-fetch-blob
react-native-keep-awake:
@@ -77,6 +88,8 @@ EXTERNAL SOURCES:
:path: ../node_modules/react-native-locale-detector
react-native-webrtc:
:path: ../node_modules/react-native-webrtc
RNSound:
:path: ../node_modules/react-native-sound
RNVectorIcons:
:path: ../node_modules/react-native-vector-icons
yoga:
@@ -85,13 +98,15 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
React: 541ba768b9855e10cdc76f55427a5cd0653ca806
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: bc044ca9530fc802e7533f247aa08fe1b6bf8dc5
RNSound: d0818fe2435254fe30540fae48a429c5ffb72e09
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
PODFILE CHECKSUM: fabd6b6c27f8e1849f0668db3f403bf536ac8903
PODFILE CHECKSUM: 4a5a310403b99b9c2d619e0b18da89bf0fe5858c
COCOAPODS: 1.4.0

View File

@@ -53,14 +53,24 @@ partial URL (e.g. a room name only) is specified to
`loadURLString:`/`loadURLObject:`. If not set or if set to `nil`, the default
built in JavaScript is used: https://meet.jit.si.
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
NOTE: Must be set (if at all) before `loadURL:`/`loadURLString:` for it to take
effect.
#### pictureInPictureEnabled
Property to get / set whether Picture-in-Picture is enabled. Defaults to `YES`
if `delegate` implements `enterPictureInPicture:`; otherwise, `NO`.
NOTE: Must be set (if at all) before `loadURL:`/`loadURLString:` for it to take
effect.
#### welcomePageEnabled
Property to get/set whether the Welcome page is enabled. If `NO`, a black empty
view will be rendered when not in a conference. Defaults to `NO`.
NOTE: Must be set before `loadURL:`/`loadURLString:` for it to take effect.
NOTE: Must be set (if at all) before `loadURL:`/`loadURLString:` for it to take
effect.
#### loadURL:NSURL
@@ -170,6 +180,16 @@ Called before a conference is left.
The `data` dictionary contains a "url" key with the conference URL.
#### enterPictureInPicture
Called when entering Picture-in-Picture is requested by the user. The app should
now activate its Picture-in-Picture implementation (and resize the associated
`JitsiMeetView`. The latter will automatically detect its new size and adjust
its user interface to a variant appropriate for the small size ordinarily
associated with Picture-in-Picture.)
The `data` dictionary is empty.
#### loadConfigError
Called when loading the main configuration file from the Jitsi Meet deployment
@@ -178,3 +198,17 @@ fails.
The `data` dictionary contains an "error" key with the error and a "url" key
with the conference URL which necessitated the loading of the configuration
file.
### Picture-in-Picture
`JitsiMeetView` will automatically adjust its UI when presented in a
Picture-in-Picture style scenario, in a rectangle too small to accommodate its
"full" UI.
Jitsi Meet SDK does not currently implement native Picture-in-Picture on iOS. If
desired, apps need to implement non-native Picture-in-Picture themselves and
resize `JitsiMeetView`.
If `pictureInPictureEnabled` is set to `YES` or `delegate` implements
`enterPictureInPicture:`, the in-call toolbar will render a button to afford the
user to request entering Picture-in-Picture.

View File

@@ -55,6 +55,8 @@
</dict>
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>Displays the user's meetings in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in conferences with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>

View File

@@ -25,6 +25,8 @@
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic) BOOL pictureInPictureEnabled;
@property (nonatomic) BOOL welcomePageEnabled;
+ (BOOL)application:(UIApplication *_Nonnull)application

View File

@@ -109,7 +109,11 @@ void registerFatalErrorHandler() {
@end
@implementation JitsiMeetView
@implementation JitsiMeetView {
NSNumber *_pictureInPictureEnabled;
}
@dynamic pictureInPictureEnabled;
static RCTBridgeWrapper *bridgeWrapper;
@@ -265,6 +269,7 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
}
props[@"externalAPIScope"] = externalAPIScope;
props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
// XXX If urlObject is nil, then it must appear as undefined in the
@@ -315,6 +320,28 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
[self loadURLObject:urlString ? @{ @"url": urlString } : nil];
}
#pragma pictureInPictureEnabled getter / setter
- (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled {
_pictureInPictureEnabled
= [NSNumber numberWithBool:pictureInPictureEnabled];
}
- (BOOL) pictureInPictureEnabled {
if (_pictureInPictureEnabled) {
return [_pictureInPictureEnabled boolValue];
}
// The SDK/JitsiMeetView client/consumer did not explicitly enable/disable
// Picture-in-Picture. However, we may automatically deduce their
// intentions: we need the support of the client in order to implement
// Picture-in-Picture on iOS (in contrast to Android) so if the client
// appears to have provided the support then we can assume that they did it
// with the intention to have Picture-in-Picture enabled.
return self.delegate
&& [self.delegate respondsToSelector:@selector(enterPictureInPicture:)];
}
#pragma mark Private methods
/**

View File

@@ -55,6 +55,17 @@
*/
- (void)conferenceWillLeave:(NSDictionary *)data;
/**
* Called when entering Picture-in-Picture is requested by the user. The app
* should now activate its Picture-in-Picture implementation (and resize the
* associated `JitsiMeetView`. The latter will automatically detect its new size
* and adjust its user interface to a variant appropriate for the small size
* ordinarily associated with Picture-in-Picture.)
*
* The `data` dictionary is empty.
*/
- (void)enterPictureInPicture:(NSDictionary *)data;
/**
* Called when loading the main configuration file from the Jitsi Meet
* deployment file.

View File

@@ -46,46 +46,20 @@
"showSpeakerStats": "Show speaker stats"
},
"welcomepage":{
"disable": "Don't show this page again",
"feature1": {
"content": "No downloads required. __app__ works directly within your browser. Simply share your conference URL with others to get started.",
"title": "Simple to use"
},
"feature2": {
"content": "Multi-party video conferences work with as little as 128Kbps. Screen-sharing and audio-only conferences are possible with far less.",
"title": "Low bandwidth"
},
"feature3": {
"content": "__app__ is licensed under the Apache License. You are free to download, use, modify, and share it as per this license.",
"title": "Open source"
},
"feature4": {
"content": "There are no artificial restrictions on the number of users or conference members. Server power and bandwidth are the only limiting factors.",
"title": "Unlimited users"
},
"feature5": {
"content": "It's easy to share your screen with others. __app__ is ideal for on-line presentations, lectures, and tech support sessions.",
"title": "Screen sharing"
},
"feature6": {
"content": "Need some privacy? __app__ conference rooms can be secured with a password in order to exclude unwanted guests and prevent interruptions.",
"title": "Secure rooms"
},
"feature7": {
"content": "__app__ features Etherpad, a real-time collaborative text editor that's great for meeting minutes, writing articles, and more.",
"title": "Shared notes"
},
"feature8": {
"content": "Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems.",
"title": "Usage statistics"
"appDescription": "Go ahead, video chat with the whole team. In fact, invite everyone you know. __app__ is a fully encrypted, 100% open source video conferencing solution that you can use all day, every day, for free — with no account needed.",
"audioVideoSwitch": {
"audio": "Voice",
"video": "Video"
},
"calendar": "Calendar",
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
"roomname": "Enter room name",
"roomnamePlaceHolder": "room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
"terms": "Terms"
"terms": "Terms",
"title": "More secure, more flexible, and completely free video conferencing"
},
"startupoverlay": {
"policyText": " ",
@@ -479,17 +453,24 @@
"qualityButtonTip": "Change received video quality"
},
"dialOut": {
"dial": "Dial",
"dialOut": "Call a number",
"statusMessage": "is now __status__",
"enterPhone": "Enter phone number",
"phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!"
"statusMessage": "is now __status__"
},
"addPeople": {
"add": "Add",
"countryNotSupported": "We do not support this destination yet.",
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
"disabled": "You can't invite people.",
"invite": "Invite",
"loading": "Searching for people and phone numbers",
"loadingNumber": "Validating phone number",
"loadingPeople": "Searching for people to invite",
"noResults": "No matching search results",
"searchPlaceholder": "Search for people and rooms to add",
"title": "Add people to your call",
"noValidNumbers": "Please enter a phone number",
"searchNumbers": "Enter a phone number to invite",
"searchPeople": "Enter a name to invite",
"searchPeopleAndNumbers": "Enter a name or phone number to invite",
"telephone": "Telephone: __number__",
"title": "Invite people to your meeting",
"failedToAdd": "Failed to add members"
},
"inlineDialogFailure": {
@@ -522,17 +503,19 @@
"dialInConferenceID": "PIN: __conferenceID__#",
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
"genericError": "Whoops, something went wrong.",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __pin__#",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
"invitePhoneAlternatives": "To view more phone numbers, click this link: __url__",
"inviteURL": "To join the video meeting, click this link: __url__",
"moreNumbers": "More numbers",
"noNumbers": "No dial-in numbers.",
"noPassword": "None",
"noRoom": "No room was specified to dial-in into.",
"numbers": "Dial-in Numbers",
"password": "Password:",
"title": "Call info",
"tooltip": "Get access info about the meeting"
},
"profileModal": {
"settingsView": {
"alertOk": "OK",
"alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid",
@@ -544,5 +527,16 @@
"serverURL": "Server URL",
"startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted"
},
"calendarSync": {
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"now": "Now"
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
}
}

View File

@@ -471,6 +471,20 @@ class API {
this._sendEvent({ name: 'feedback-submitted' });
}
/**
* Notify external application (if API is enabled) that the screen sharing
* has been turned on/off.
*
* @param {boolean} on - True if screen sharing is enabled.
* @returns {void}
*/
notifyScreenSharingStatusChanged(on: boolean) {
this._sendEvent({
name: 'screen-sharing-status-changed',
on
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -48,7 +48,8 @@ const events = {
'video-conference-joined': 'videoConferenceJoined',
'video-conference-left': 'videoConferenceLeft',
'video-availability-changed': 'videoAvailabilityChanged',
'video-mute-status-changed': 'videoMuteStatusChanged'
'video-mute-status-changed': 'videoMuteStatusChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged'
};
/**
@@ -485,6 +486,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* {{
* roomName: room //the room name of the conference
* }}
* screenSharingStatusChanged - receives event notifications about
* turning on/off the local user screen sharing.
* The listener will receive object with the following structure:
* {{
* on: on //whether screen sharing is on
* }}
* readyToClose - all hangup operations are completed and Jitsi Meet is
* ready to be disposed.
* @returns {void}

View File

@@ -28,6 +28,7 @@ import {
participantPresenceChanged,
showParticipantJoinedNotification
} from '../../react/features/base/participants';
import { destroyLocalTracks } from '../../react/features/base/tracks';
import { openDisplayNamePrompt } from '../../react/features/display-name';
import {
setNotificationsEnabled,
@@ -376,15 +377,6 @@ UI.start = function() {
document.title = interfaceConfig.APP_NAME;
};
/**
* Invokes cleanup of any deferred execution within relevant UI modules.
*
* @returns {void}
*/
UI.stopDaemons = () => {
VideoLayout.resetLargeVideo();
};
/**
* Setup some UI event listeners.
*/
@@ -511,11 +503,6 @@ UI.addUser = function(user) {
APP.store.dispatch(showParticipantJoinedNotification(displayName));
}
if (!config.startAudioMuted
|| config.startAudioMuted > APP.conference.membersCount) {
UIUtil.playSoundNotification('userJoined');
}
// Add Peer's container
VideoLayout.addParticipantContainer(user);
@@ -537,11 +524,6 @@ UI.removeUser = function(id, displayName) {
messageHandler.participantNotification(
displayName, 'notify.somebody', 'disconnected', 'notify.disconnected');
if (!config.startAudioMuted
|| config.startAudioMuted > APP.conference.membersCount) {
UIUtil.playSoundNotification('userLeft');
}
VideoLayout.removeParticipantContainer(id);
};
@@ -1291,6 +1273,18 @@ UI.setLocalRemoteControlActiveChanged = function() {
VideoLayout.setLocalRemoteControlActiveChanged();
};
/**
* Remove media tracks and UI elements so the user no longer sees media in the
* UI. The intent is to provide a feeling that the meeting has ended.
*
* @returns {void}
*/
UI.removeLocalMedia = function() {
APP.store.dispatch(destroyLocalTracks());
VideoLayout.resetLargeVideo();
$('#videospace').hide();
};
// TODO: Export every function separately. For now there is no point of doing
// this because we are importing everything.
export default UI;

View File

@@ -66,6 +66,8 @@ function doExternalAuth(room, lockPassword) {
* @param {string} [roomName] the name of the conference room.
*/
function redirectToTokenAuthService(roomName) {
// FIXME: This method will not preserve the other URL params that were
// originally passed.
UIUtil.redirect(getTokenAuthUrl(roomName, false));
}

View File

@@ -282,6 +282,7 @@ export default class SharedVideoManager {
thumb.setDisplayName('YouTube');
VideoLayout.addRemoteVideoContainer(self.url, thumb);
VideoLayout.resizeThumbnails(false, true);
const iframe = player.getIframe();

View File

@@ -25,8 +25,6 @@ const htmlStr = `
</div>
<div id="chatconversation"></div>
<audio id="chatNotification" src="sounds/incomingMessage.wav"
preload="auto"></audio>
<textarea id="usermsg" autofocus
data-i18n="[placeholder]chat.messagebox"></textarea>
<div id="smileysarea">
@@ -285,7 +283,6 @@ const Chat = {
if (!Chat.isVisible()) {
unreadMessages++;
UIUtil.playSoundNotification('chatNotification');
updateVisualNotification();
}
}

View File

@@ -1,17 +1,18 @@
/* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../../react/features/base/i18n';
import { SettingsMenu } from '../../../../react/features/settings-menu';
/* eslint-enable no-unused-vars */
import { SettingsMenu } from '../../../../react/features/settings';
import UIUtil from '../../util/UIUtil';
/* eslint-enable no-unused-vars */
export default {
init() {
const settingsMenuContainer = document.createElement('div');
@@ -31,8 +32,7 @@ export default {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<SettingsMenu
{ ...props } />
<SettingsMenu { ...props } />
</I18nextProvider>
</Provider>,
settingsMenuContainer

View File

@@ -66,15 +66,6 @@ const UIUtil = {
return el.clientHeight + 1;
},
/**
* Plays the sound given by id.
*
* @param id the identifier of the audio element.
*/
playSoundNotification(id) {
document.getElementById(id).play();
},
/**
* Escapes the given text.
*/
@@ -217,6 +208,14 @@ const UIUtil = {
}
},
/**
* Redirects to a given URL.
*
* @param {string} url - The redirect URL.
* NOTE: Currently used to redirect to 3rd party location for
* authentication. In most cases redirectWithStoredParams action must be
* used instead of this method in order to preserve curent URL params.
*/
redirect(url) {
window.location.href = url;
},

View File

@@ -687,7 +687,11 @@ export class VideoContainer extends LargeContainer {
* @returns {void}
*/
_updateBackground() {
if (browser.isFirefox() || browser.isTemasysPluginUsed()) {
// Do not the background display on browsers that might experience
// performance issues from the presence of the background.
if (browser.isFirefox()
|| browser.isSafariWithWebrtc()
|| browser.isTemasysPluginUsed()) {
return;
}

View File

@@ -21,8 +21,6 @@ let displayName = UIUtil.unescapeHtml(
jitsiLocalStorage.getItem('displayname') || '');
let cameraDeviceId = jitsiLocalStorage.getItem('cameraDeviceId') || '';
let micDeviceId = jitsiLocalStorage.getItem('micDeviceId') || '';
let welcomePageDisabled = JSON.parse(
jitsiLocalStorage.getItem('welcomePageDisabled') || false);
// Currently audio output device change is supported only in Chrome and
// default output always has 'default' device ID
@@ -189,22 +187,5 @@ export default {
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
.then(() =>
jitsiLocalStorage.setItem('audioOutputDeviceId', newId));
},
/**
* Check if welcome page is enabled or not.
* @returns {boolean}
*/
isWelcomePageEnabled() {
return !welcomePageDisabled;
},
/**
* Enable or disable welcome page.
* @param {boolean} enabled if welcome page should be enabled or not
*/
setWelcomePageEnabled(enabled) {
welcomePageDisabled = !enabled;
jitsiLocalStorage.setItem('welcomePageDisabled', welcomePageDisabled);
}
};

View File

@@ -16,24 +16,6 @@ export function createDeferred() {
return deferred;
}
/**
* Reload page.
*/
export function reload() {
window.location.reload();
}
/**
* 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 replace(url) {
window.location.replace(url);
}
/**
* Prints the error and reports it to the global error handler.
*

1539
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,12 +39,12 @@
"i18next-xhr-backend": "1.4.2",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#6fff754a77a56ab52499f3559105a15886942a1e",
"jquery": "2.1.4",
"jquery": "3.3.1",
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f9644b5c7093d61690e36a008aa7fc96a793234d ",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#410ad5e76d33b1422ce34f83719eda7dfd584e22",
"lodash": "4.17.4",
"moment": "2.19.4",
"nuclear-js": "1.4.0",
@@ -55,6 +55,7 @@
"react-i18next": "4.8.0",
"react-native": "0.51.0",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "1.4.3",
"react-native-callstats": "3.27.0",
"react-native-fetch-blob": "0.10.8",
"react-native-img-cache": "1.5.2",
@@ -62,8 +63,9 @@
"react-native-keep-awake": "2.0.6",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#cc76092fc4335488a28a9529c8b50afae2c3ecdc",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.4",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#806435b41fa152a8239ebeb7d002d1c6e979be86",
"react-native-webrtc": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
"react-redux": "5.0.6",
"redux": "3.7.2",
"redux-thunk": "2.2.0",
@@ -99,7 +101,7 @@
"precommit-hook": "3.0.0",
"string-replace-loader": "1.3.0",
"style-loader": "0.19.0",
"uglifyjs-webpack-plugin": "1.1.2",
"uglifyjs-webpack-plugin": "1.2.2",
"whatwg-fetch": "2.0.3",
"webpack": "3.9.1",
"webpack-dev-server": "2.9.5"

View File

@@ -76,26 +76,26 @@ export const VIDEO_MUTE = 'video.mute';
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createApiEvent = function(action, attributes = {}) {
export function createApiEvent(action, attributes = {}) {
return {
action,
attributes,
source: 'jitsi-meet-api'
};
};
}
/**
* Creates an event which indicates that the audio-only mode has been turned
* off.
* Creates an event which indicates that the audio-only mode has been changed.
*
* @param {boolean} enabled - True if audio-only is enabled, false otherwise.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createAudioOnlyDisableEvent = function() {
export function createAudioOnlyChangedEvent(enabled) {
return {
action: 'audio.only.disabled'
action: `audio.only.${enabled ? 'enabled' : 'disabled'}`
};
};
}
/**
* Creates an event which indicates that a device was changed.
@@ -106,7 +106,7 @@ export const createAudioOnlyDisableEvent = function() {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createDeviceChangedEvent = function(mediaType, deviceType) {
export function createDeviceChangedEvent(mediaType, deviceType) {
return {
action: 'device.changed',
attributes: {
@@ -114,7 +114,7 @@ export const createDeviceChangedEvent = function(mediaType, deviceType) {
'media_type': mediaType
}
};
};
}
/**
* Creates an event which specifies that the feedback dialog has been opened.
@@ -122,11 +122,11 @@ export const createDeviceChangedEvent = function(mediaType, deviceType) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createFeedbackOpenEvent = function() {
export function createFeedbackOpenEvent() {
return {
action: 'feedback.opened'
};
};
}
/**
* Creates an event which indicates that the invite dialog was closed. This is
@@ -136,11 +136,11 @@ export const createFeedbackOpenEvent = function() {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createInviteDialogClosedEvent = function() {
export function createInviteDialogClosedEvent() {
return {
action: 'invite.dialog.closed'
};
};
}
/**
* Creates a "page reload" event.
@@ -152,17 +152,16 @@ export const createInviteDialogClosedEvent = function() {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createPageReloadScheduledEvent
= function(reason, timeout, details) {
return {
action: 'page.reload.scheduled',
attributes: {
reason,
timeout,
...details
}
};
export function createPageReloadScheduledEvent(reason, timeout, details) {
return {
action: 'page.reload.scheduled',
attributes: {
reason,
timeout,
...details
}
};
}
/**
* Creates a "pinned" or "unpinned" event.
@@ -173,17 +172,16 @@ export const createPageReloadScheduledEvent
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createPinnedEvent
= function(action, participantId, attributes) {
return {
type: TYPE_TRACK,
action,
actionSubject: 'participant',
objectType: 'participant',
objectId: participantId,
attributes
};
};
export function createPinnedEvent(action, participantId, attributes) {
return {
type: TYPE_TRACK,
action,
actionSubject: 'participant',
objectType: 'participant',
objectId: participantId,
attributes
};
}
/**
* Creates an event which indicates that a button in the profile panel was
@@ -194,16 +192,15 @@ export const createPinnedEvent
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createProfilePanelButtonEvent
= function(buttonName, attributes = {}) {
return {
action: 'clicked',
actionSubject: buttonName,
attributes,
source: 'profile.panel',
type: TYPE_UI
};
export function createProfilePanelButtonEvent(buttonName, attributes = {}) {
return {
action: 'clicked',
actionSubject: buttonName,
attributes,
source: 'profile.panel',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that a specific button on one of the
@@ -215,14 +212,14 @@ export const createProfilePanelButtonEvent
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createRecordingDialogEvent = function(dialogName, buttonName) {
export function createRecordingDialogEvent(dialogName, buttonName) {
return {
action: 'clicked',
actionSubject: buttonName,
source: `${dialogName}.recording.dialog`,
type: TYPE_UI
};
};
}
/**
* Creates an event which specifies that the "confirm" button on the remote
@@ -233,7 +230,7 @@ export const createRecordingDialogEvent = function(dialogName, buttonName) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createRemoteMuteConfirmedEvent = function(participantId) {
export function createRemoteMuteConfirmedEvent(participantId) {
return {
action: 'clicked',
actionSubject: 'remote.mute.dialog.confirm.button',
@@ -243,7 +240,7 @@ export const createRemoteMuteConfirmedEvent = function(participantId) {
source: 'remote.mute.dialog',
type: TYPE_UI
};
};
}
/**
* Creates an event which indicates that one of the buttons in the "remote
@@ -254,16 +251,15 @@ export const createRemoteMuteConfirmedEvent = function(participantId) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createRemoteVideoMenuButtonEvent
= function(buttonName, attributes) {
return {
action: 'clicked',
actionSubject: buttonName,
attributes,
source: 'remote.video.menu',
type: TYPE_UI
};
export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
return {
action: 'clicked',
actionSubject: buttonName,
attributes,
source: 'remote.video.menu',
type: TYPE_UI
};
}
/**
* Creates an event indicating that an action related to screen sharing
@@ -273,12 +269,12 @@ export const createRemoteVideoMenuButtonEvent
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createScreenSharingEvent = function(action) {
export function createScreenSharingEvent(action) {
return {
action,
actionSubject: 'screen.sharing'
};
};
}
/**
* The local participant failed to send a "selected endpoint" message to the
@@ -288,7 +284,7 @@ export const createScreenSharingEvent = function(action) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createSelectParticipantFailedEvent = function(error) {
export function createSelectParticipantFailedEvent(error) {
const event = {
action: 'select.participant.failed'
};
@@ -298,7 +294,7 @@ export const createSelectParticipantFailedEvent = function(error) {
}
return event;
};
}
/**
* Creates an event associated with the "shared video" feature.
@@ -308,13 +304,13 @@ export const createSelectParticipantFailedEvent = function(error) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createSharedVideoEvent = function(action, attributes = {}) {
export function createSharedVideoEvent(action, attributes = {}) {
return {
action,
attributes,
actionSubject: 'shared.video'
};
};
}
/**
* Creates an event associated with a shortcut being pressed, released or
@@ -331,17 +327,19 @@ export const createSharedVideoEvent = function(action, attributes = {}) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createShortcutEvent
= function(shortcut, action = ACTION_SHORTCUT_TRIGGERED, attributes = {}) {
return {
action,
actionSubject: 'keyboard.shortcut',
actionSubjectId: shortcut,
attributes,
source: 'keyboard.shortcut',
type: TYPE_UI
};
export function createShortcutEvent(
shortcut,
action = ACTION_SHORTCUT_TRIGGERED,
attributes = {}) {
return {
action,
actionSubject: 'keyboard.shortcut',
actionSubjectId: shortcut,
attributes,
source: 'keyboard.shortcut',
type: TYPE_UI
};
}
/**
* Creates an event which indicates the "start audio only" configuration.
@@ -350,14 +348,14 @@ export const createShortcutEvent
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createStartAudioOnlyEvent = function(audioOnly) {
export function createStartAudioOnlyEvent(audioOnly) {
return {
action: 'start.audio.only',
attributes: {
enabled: audioOnly
}
};
};
}
/**
* Creates an event which indicates the "start muted" configuration.
@@ -372,17 +370,19 @@ export const createStartAudioOnlyEvent = function(audioOnly) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createStartMutedConfigurationEvent
= function(source, audioMute, videoMute) {
return {
action: 'start.muted.configuration',
attributes: {
source,
'audio_mute': audioMute,
'video_mute': videoMute
}
};
export function createStartMutedConfigurationEvent(
source,
audioMute,
videoMute) {
return {
action: 'start.muted.configuration',
attributes: {
source,
'audio_mute': audioMute,
'video_mute': videoMute
}
};
}
/**
* Creates an event which indicates the delay for switching between simulcast
@@ -392,12 +392,12 @@ export const createStartMutedConfigurationEvent
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createStreamSwitchDelayEvent = function(attributes) {
export function createStreamSwitchDelayEvent(attributes) {
return {
action: 'stream.switch.delay',
attributes
};
};
}
/**
* Automatically changing the mute state of a media track in order to match
@@ -409,7 +409,7 @@ export const createStreamSwitchDelayEvent = function(attributes) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createSyncTrackStateEvent = function(mediaType, muted) {
export function createSyncTrackStateEvent(mediaType, muted) {
return {
action: 'sync.track.state',
attributes: {
@@ -417,7 +417,7 @@ export const createSyncTrackStateEvent = function(mediaType, muted) {
muted
}
};
};
}
/**
* Creates an event associated with a toolbar button being clicked/pressed. By
@@ -431,7 +431,7 @@ export const createSyncTrackStateEvent = function(mediaType, muted) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createToolbarEvent = function(buttonName, attributes = {}) {
export function createToolbarEvent(buttonName, attributes = {}) {
return {
action: 'clicked',
actionSubject: buttonName,
@@ -439,7 +439,7 @@ export const createToolbarEvent = function(buttonName, attributes = {}) {
source: 'toolbar.button',
type: TYPE_UI
};
};
}
/**
* Creates an event which indicates that a local track was muted.
@@ -452,7 +452,7 @@ export const createToolbarEvent = function(buttonName, attributes = {}) {
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export const createTrackMutedEvent = function(mediaType, reason, muted = true) {
export function createTrackMutedEvent(mediaType, reason, muted = true) {
return {
action: 'track.muted',
attributes: {
@@ -461,4 +461,22 @@ export const createTrackMutedEvent = function(mediaType, reason, muted = true) {
reason
}
};
};
}
/**
* Creates an event for an action on the welcome page.
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createWelcomePageEvent(action, actionSubject, attributes = {}) {
return {
action,
actionSubject,
attributes,
source: 'welcomePage'
};
}

View File

@@ -1,19 +0,0 @@
/**
* The type of (redux) action which signals the request
* to hide the app settings modal.
*
* {
* type: HIDE_APP_SETTINGS
* }
*/
export const HIDE_APP_SETTINGS = Symbol('HIDE_APP_SETTINGS');
/**
* The type of (redux) action which signals the request
* to show the app settings modal where available.
*
* {
* type: SHOW_APP_SETTINGS
* }
*/
export const SHOW_APP_SETTINGS = Symbol('SHOW_APP_SETTINGS');

View File

@@ -1,29 +0,0 @@
// @flow
import { HIDE_APP_SETTINGS, SHOW_APP_SETTINGS } from './actionTypes';
/**
* Redux-signals the request to hide the app settings modal.
*
* @returns {{
* type: HIDE_APP_SETTINGS
* }}
*/
export function hideAppSettings() {
return {
type: HIDE_APP_SETTINGS
};
}
/**
* Redux-signals the request to open the app settings modal.
*
* @returns {{
* type: SHOW_APP_SETTINGS
* }}
*/
export function showAppSettings() {
return {
type: SHOW_APP_SETTINGS
};
}

View File

@@ -1,117 +0,0 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { getSafetyOffset } from '../../base/react';
import { ASPECT_RATIO_WIDE } from '../../base/responsive-ui';
import styles, { CONTAINER_PADDING } from './styles';
/**
* The type of the React {@code Component} props of {@link FormSectionHeader}
*/
type Props = {
/**
* The current aspect ratio of the screen.
*/
_aspectRatio: Symbol,
/**
* The i18n key of the text label of the section.
*/
i18nLabel: string,
/**
* An external style object passed to the component.
*/
style: Object,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
/**
* Implements a React {@code Component} which renders a section header on a
* form. This calculates the available safe view as well.
*/
class FormSectionHeader extends Component<Props> {
/**
* Initializes a new {@code FormSectionHeader} instance.
*
* @param {Object} props - Component properties.
*/
constructor(props) {
super(props);
this._getSafetyMargin = this._getSafetyMargin.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @override
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<View
style = { [
styles.formSectionTitle,
this.props.style,
this._getSafetyMargin()
] } >
<Text>
{ t(this.props.i18nLabel) }
</Text>
</View>
);
}
_getSafetyMargin: () => Object;
/**
* Calculates the safety margin for this header. See comment in
* functions.js.
*
* @private
* @returns {Object}
*/
_getSafetyMargin() {
if (this.props._aspectRatio === ASPECT_RATIO_WIDE) {
const safeOffset
= Math.max(getSafetyOffset() - CONTAINER_PADDING, 0);
return {
marginLeft: safeOffset,
marginRight: safeOffset
};
}
return undefined;
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props of
* {@code FormSectionHeader}.
*
* @param {Object} state - The redux state.
* @protected
* @returns {Object}
*/
export function _mapStateToProps(state: Object) {
return {
_aspectRatio: state['features/base/responsive-ui'].aspectRatio
};
}
export default translate(connect(_mapStateToProps)(FormSectionHeader));

View File

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

View File

@@ -1,133 +0,0 @@
import {
BoxModel,
ColorPalette,
createStyleSheet
} from '../../base/styles';
export const ANDROID_UNDERLINE_COLOR = 'transparent';
export const CONTAINER_PADDING = 2 * BoxModel.padding;
export const HEADER_COLOR = ColorPalette.blue;
export const HEADER_PADDING = BoxModel.padding;
const TEXT_SIZE = 17;
/**
* The styles of the React {@code Components} of the feature
* {@code app-settings}.
*/
export default createStyleSheet({
/**
* The back button style.
*/
backIcon: {
alignSelf: 'center',
fontSize: 26,
padding: 8,
paddingRight: 22
},
/**
* Standardized style for a field container {@code View}.
*/
fieldContainer: {
alignItems: 'center',
flexDirection: 'row',
minHeight: 65
},
/**
* Standard container for a {@code View} containing a field label.
*/
fieldLabelContainer: {
alignItems: 'center',
flexDirection: 'row',
marginRight: 5
},
/**
* Field container style for all but last row {@code View}.
*/
fieldSeparator: {
borderBottomWidth: 1,
borderColor: 'rgba(0, 0, 0, 0.1)'
},
/**
* Style for the {@code View} containing each
* field values (the actual field).
*/
fieldValueContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end'
},
formSectionTitle: {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
marginTop: 5,
padding: 5
},
/**
* Page header {@code View}.
*/
headerContainer: {
alignItems: 'center',
backgroundColor: HEADER_COLOR,
flexDirection: 'row',
justifyContent: 'flex-start',
padding: HEADER_PADDING
},
/**
* The title {@code Text} of the header.
*/
headerTitle: {
color: ColorPalette.white,
fontSize: 22
},
/**
* Style of the ScrollView to be able to scroll the content.
*/
scrollView: {
flex: 1
},
/**
* The back button style on the settings screen.
*/
settingsBackButton: {
color: ColorPalette.white
},
/**
* The top level container {@code View}.
*/
settingsContainer: {
backgroundColor: ColorPalette.white,
flex: 1,
flexDirection: 'column',
margin: 0,
padding: CONTAINER_PADDING,
paddingTop: 0
},
/**
* Global {@code Text} color for the page.
*/
text: {
color: ColorPalette.black,
fontSize: TEXT_SIZE
},
/**
* Standard text input field style.
*/
textInputField: {
flex: 1,
fontSize: TEXT_SIZE,
textAlign: 'right'
}
});

View File

@@ -1,31 +0,0 @@
// @flow
import {
HIDE_APP_SETTINGS,
SHOW_APP_SETTINGS
} from './actionTypes';
import { ReducerRegistry } from '../base/redux';
const DEFAULT_STATE = {
visible: false
};
ReducerRegistry.register(
'features/app-settings', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case HIDE_APP_SETTINGS:
return {
...state,
visible: false
};
case SHOW_APP_SETTINGS:
return {
...state,
visible: true
};
}
return state;
});

View File

@@ -4,7 +4,6 @@ import { setRoom } from '../base/conference';
import { configWillLoad, loadConfigError, setConfig } from '../base/config';
import { setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { getProfile } from '../base/profile';
import { parseURIString } from '../base/util';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
@@ -15,7 +14,7 @@ declare var APP: Object;
* Triggers an in-app navigation to a specific route. Allows navigation to be
* abstracted between the mobile/React Native and Web/React applications.
*
* @param {(string|undefined)} uri - The URI to which to navigate. It may be a
* @param {string|undefined} uri - The URI to which to navigate. It may be a
* full URL with an HTTP(S) scheme, a full or partial URI with the app-specific
* scheme, or a mere room name.
* @returns {Function}
@@ -25,6 +24,48 @@ export function appNavigate(uri: ?string) {
_appNavigateToOptionalLocation(dispatch, getState, parseURIString(uri));
}
/**
* Redirects to another page generated by replacing the path in the original URL
* with the given path.
*
* @param {(string)} pathname - The path to navigate to.
* @returns {Function}
*/
export function redirectWithStoredParams(pathname: string) {
return (dispatch: Dispatch<*>, getState: Function) => {
const { locationURL } = getState()['features/base/connection'];
const newLocationURL = new URL(locationURL.href);
newLocationURL.pathname = pathname;
window.location.assign(newLocationURL.toString());
};
}
/**
* Reloads the page by restoring the original URL.
*
* @returns {Function}
*/
export function reloadWithStoredParams() {
return (dispatch: Dispatch<*>, getState: Function) => {
const { locationURL } = getState()['features/base/connection'];
const windowLocation = window.location;
const oldSearchString = windowLocation.search;
windowLocation.replace(locationURL.toString());
if (window.self !== window.top
&& locationURL.search === oldSearchString) {
// NOTE: Assuming that only the hash or search part of the URL will
// be changed!
// location.reload will not trigger redirect/reload for iframe when
// only the hash params are changed. That's why we need to call
// reload in addition to replace.
windowLocation.reload();
}
};
}
/**
* Triggers an in-app navigation to a specific location URI.
*
@@ -83,11 +124,10 @@ function _appNavigateToMandatoryLocation(
});
}
const profile = getProfile(getState());
const profile = getState()['features/base/profile'];
return promise.then(() => dispatch(setConfig(
_mergeConfigWithProfile(config, profile)
)));
return promise.then(() =>
dispatch(setConfig(_mergeConfigWithProfile(config, profile))));
}
}

View File

@@ -12,9 +12,10 @@ import {
localParticipantJoined,
localParticipantLeft
} from '../../base/participants';
import { getProfile } from '../../base/profile';
import '../../base/profile';
import { Fragment, RouteRegistry } from '../../base/react';
import { MiddlewareRegistry, ReducerRegistry } from '../../base/redux';
import { SoundCollection } from '../../base/sounds';
import { PersistenceRegistry } from '../../base/storage';
import { toURLString } from '../../base/util';
import { OverlayContainer } from '../../overlay';
@@ -123,7 +124,7 @@ export class AbstractApp extends Component {
*/
componentWillMount() {
this._init.then(() => {
const { dispatch } = this._getStore();
const { dispatch, getState } = this._getStore();
dispatch(appWillMount(this));
@@ -144,7 +145,7 @@ export class AbstractApp extends Component {
}
// Profile is the new React compatible settings.
const profile = getProfile(this._getStore().getState());
const profile = getState()['features/base/profile'];
if (profile) {
localParticipant.email
@@ -274,6 +275,7 @@ export class AbstractApp extends Component {
<Provider store = { this._getStore() }>
<Fragment>
{ this._createElement(component) }
<SoundCollection />
<OverlayContainer />
</Fragment>
</Provider>
@@ -381,7 +383,8 @@ export class AbstractApp extends Component {
return (
this.props.defaultURL
|| getProfile(this._getStore().getState()).serverURL
|| this._getStore().getState()['features/base/profile']
.serverURL
|| DEFAULT_URL);
}
@@ -500,7 +503,7 @@ export class AbstractApp extends Component {
/**
* Navigates this {@code AbstractApp} to (i.e. opens) a specific URL.
*
* @param {string|Object} url - The URL to navigate this {@code AbstractApp}
* @param {Object|string} url - The URL to navigate this {@code AbstractApp}
* to (i.e. the URL to open).
* @protected
* @returns {void}

View File

@@ -17,6 +17,7 @@ import '../../mobile/callkit';
import '../../mobile/external-api';
import '../../mobile/full-screen';
import '../../mobile/permissions';
import '../../mobile/picture-in-picture';
import '../../mobile/proximity';
import '../../mobile/wake-lock';
@@ -36,6 +37,13 @@ export class App extends AbstractApp {
static propTypes = {
...AbstractApp.propTypes,
/**
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
* button is rendered in the {@link Conference} view to afford entering
* Picture-in-Picture.
*/
pictureInPictureEnabled: PropTypes.bool,
/**
* Whether the Welcome page is enabled. If {@code true}, the Welcome
* page is rendered when the {@link App} is not at a location (URL)

View File

@@ -3,6 +3,7 @@ import React from 'react';
import '../../base/responsive-ui';
import { getLocationContextRoot } from '../../base/util';
import '../../chat';
import '../../room-lock';
import { AbstractApp } from './AbstractApp';

View File

@@ -3,7 +3,7 @@
import {
ACTION_PINNED,
ACTION_UNPINNED,
createAudioOnlyDisableEvent,
createAudioOnlyChangedEvent,
createPinnedEvent,
sendAnalytics
} from '../../analytics';
@@ -20,6 +20,7 @@ import UIEvents from '../../../../service/UI/UIEvents';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceLeft,
createConference,
setAudioOnly,
setLastN,
@@ -32,8 +33,10 @@ import {
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
SET_RECEIVE_VIDEO_QUALITY
SET_RECEIVE_VIDEO_QUALITY,
SET_ROOM
} from './actionTypes';
import { JITSI_CONFERENCE_URL_KEY } from './constants';
import {
_addLocalTracksToConference,
_handleParticipantError,
@@ -77,6 +80,9 @@ MiddlewareRegistry.register(store => next => action => {
case SET_RECEIVE_VIDEO_QUALITY:
return _setReceiveVideoQuality(store, next, action);
case SET_ROOM:
return _setRoom(store, next, action);
case TRACK_ADDED:
case TRACK_REMOVED:
return _trackAddedOrRemoved(store, next, action);
@@ -126,10 +132,22 @@ function _connectionEstablished({ dispatch }, next, action) {
function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
const result = next(action);
if (getState()['features/base/conference'].audioOnly) {
sendAnalytics(createAudioOnlyDisableEvent());
logger.log('Audio only disabled');
dispatch(setAudioOnly(false));
const state = getState();
const { audioOnly } = state['features/base/conference'];
const { startAudioOnly } = state['features/base/profile'];
// FIXME: Consider implementing a standalone audio-only feature that handles
// all these state changes.
if (audioOnly) {
if (!startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(false));
logger.log('Audio only disabled');
dispatch(setAudioOnly(false));
}
} else if (startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(true));
logger.log('Audio only enabled');
dispatch(setAudioOnly(true));
}
return result;
@@ -318,6 +336,62 @@ function _setReceiveVideoQuality({ dispatch, getState }, next, action) {
return next(action);
}
/**
* Notifies the feature {@code base/conference} that the redix action
* {@link SET_ROOM} is being dispatched within a specific redux store.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code SET_ROOM} which is being
* dispatched in the specified store.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setRoom({ dispatch, getState }, next, action) {
const result = next(action);
// By the time SET_ROOM is dispatched, base/connection's locationURL and
// base/conference's leaving should be the only conference-related sources
// of truth.
const state = getState();
const {
leaving,
...stateFeaturesBaseConference
} = state['features/base/conference'];
const { locationURL } = state['features/base/connection'];
const dispatchConferenceLeft = new Set();
// Figure out which of the JitsiConferences referenced by base/conference
// have not dispatched or are not likely to dispatch CONFERENCE_FAILED and
// CONFERENCE_LEFT.
// eslint-disable-next-line guard-for-in
for (const p in stateFeaturesBaseConference) {
const v = stateFeaturesBaseConference[p];
// Does the value of the base/conference's property look like a
// JitsiConference?
if (v && typeof v === 'object') {
const url = v[JITSI_CONFERENCE_URL_KEY];
if (url && v !== leaving && url !== locationURL) {
dispatchConferenceLeft.add(v);
}
}
}
// Dispatch CONFERENCE_LEFT for the JitsiConferences referenced by
// base/conference which have not dispatched or are not likely to dispatch
// CONFERENCE_FAILED or CONFERENCE_LEFT.
for (const conference of dispatchConferenceLeft) {
dispatch(conferenceLeft(conference));
}
return result;
}
/**
* Synchronizes local tracks from state with local tracks in JitsiConference
* instance.

View File

@@ -36,6 +36,7 @@ const WHITELISTED_KEYS = [
'desktopSharingChromeExtId',
'desktopSharingChromeMinExtVersion',
'desktopSharingChromeSources',
'desktopSharingFrameRate',
'desktopSharingFirefoxDisabled',
'desktopSharingSources',
'disable1On1Mode',
@@ -63,10 +64,12 @@ const WHITELISTED_KEYS = [
'failICE',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'nick',
'openBridgeChannel',

View File

@@ -3,7 +3,7 @@
import _ from 'lodash';
import type { Dispatch } from 'redux';
import { conferenceWillLeave } from '../conference';
import { conferenceLeft, conferenceWillLeave } from '../conference';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { parseStandardURIString } from '../util';
@@ -281,7 +281,16 @@ export function disconnect() {
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_));
promise = conference_.leave();
promise
= conference_.leave()
.catch(() => {
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
} else {
promise = Promise.resolve();
}

View File

@@ -1,6 +1,87 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M512 342h64v180l150 90-32 52-182-110v-212zM554 128c212 0 384 172 384 384s-172 384-384 384c-106 0-200-42-270-112l60-62c54 54 128 88 210 88 166 0 300-132 300-298s-134-298-300-298-298 132-298 298h128l-172 172-4-6-166-166h128c0-212 172-384 384-384z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"restore"
],
"defaultCode": 59571,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "history, restore",
"id": 385,
"order": 930,
"prevSize": 24,
"code": 59571,
"name": "restore"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 385
},
{
"icon": {
"paths": [
"M426 256l256 256-256 256-60-60 196-196-196-196z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"navigate_next"
],
"defaultCode": 58377,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "chevron_right, navigate_next",
"id": 153,
"order": 927,
"prevSize": 24,
"code": 58377,
"name": "navigate_next"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
},
{
"icon": {
"paths": [
"M128 256h768v86h-768v-86zM128 554v-84h768v84h-768zM128 768v-86h768v86h-768z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"menu"
],
"defaultCode": 58834,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "menu",
"id": 489,
"order": 926,
"prevSize": 24,
"code": 58834,
"name": "menu"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 1
},
{
"icon": {
"paths": [
@@ -24,9 +105,9 @@
"code": 58820,
"name": "arrow_back"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 45
"setIdx": 1,
"setId": 1,
"iconIdx": 2
},
{
"icon": {
@@ -51,9 +132,9 @@
"code": 58376,
"name": "navigate_before"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 152
"setIdx": 1,
"setId": 1,
"iconIdx": 3
},
{
"icon": {
@@ -80,7 +161,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
"iconIdx": 4
},
{
"icon": {
@@ -107,7 +188,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 1
"iconIdx": 5
},
{
"icon": {
@@ -127,14 +208,14 @@
"properties": {
"ligatures": "timer",
"id": 760,
"order": 916,
"order": 928,
"prevSize": 24,
"code": 58405,
"name": "timer"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 2
"iconIdx": 6
},
{
"icon": {
@@ -161,7 +242,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 3
"iconIdx": 7
},
{
"icon": {
@@ -188,7 +269,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 4
"iconIdx": 8
},
{
"icon": {
@@ -215,7 +296,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 5
"iconIdx": 9
},
{
"icon": {
@@ -242,7 +323,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 6
"iconIdx": 10
},
{
"icon": {
@@ -271,7 +352,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 7
"iconIdx": 11
},
{
"icon": {
@@ -298,7 +379,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 8
"iconIdx": 12
},
{
"icon": {
@@ -325,7 +406,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 9
"iconIdx": 13
},
{
"icon": {
@@ -354,7 +435,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 10
"iconIdx": 14
},
{
"icon": {
@@ -383,7 +464,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 11
"iconIdx": 15
},
{
"icon": {
@@ -412,7 +493,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 12
"iconIdx": 16
},
{
"icon": {
@@ -441,7 +522,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 13
"iconIdx": 17
},
{
"icon": {
@@ -470,7 +551,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 14
"iconIdx": 18
},
{
"icon": {
@@ -496,7 +577,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 15
"iconIdx": 19
},
{
"icon": {
@@ -522,7 +603,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 16
"iconIdx": 20
},
{
"icon": {
@@ -548,7 +629,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 17
"iconIdx": 21
},
{
"icon": {
@@ -574,7 +655,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 18
"iconIdx": 22
},
{
"icon": {
@@ -600,7 +681,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 19
"iconIdx": 23
},
{
"icon": {
@@ -626,7 +707,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 20
"iconIdx": 24
},
{
"icon": {
@@ -652,7 +733,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 21
"iconIdx": 25
},
{
"icon": {
@@ -678,7 +759,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 22
"iconIdx": 26
},
{
"icon": {
@@ -704,7 +785,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 23
"iconIdx": 27
},
{
"icon": {
@@ -730,7 +811,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 24
"iconIdx": 28
},
{
"icon": {
@@ -756,7 +837,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 25
"iconIdx": 29
},
{
"icon": {
@@ -782,7 +863,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 26
"iconIdx": 30
},
{
"icon": {
@@ -808,7 +889,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 27
"iconIdx": 31
},
{
"icon": {
@@ -834,7 +915,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 28
"iconIdx": 32
},
{
"icon": {
@@ -860,7 +941,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 29
"iconIdx": 33
},
{
"icon": {
@@ -886,7 +967,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 30
"iconIdx": 34
},
{
"icon": {
@@ -912,7 +993,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 31
"iconIdx": 35
},
{
"icon": {
@@ -938,7 +1019,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 32
"iconIdx": 36
},
{
"icon": {
@@ -964,7 +1045,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 33
"iconIdx": 37
},
{
"icon": {
@@ -990,7 +1071,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 34
"iconIdx": 38
},
{
"icon": {
@@ -1016,7 +1097,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 35
"iconIdx": 39
},
{
"icon": {
@@ -1042,7 +1123,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 36
"iconIdx": 40
},
{
"icon": {
@@ -1068,7 +1149,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 37
"iconIdx": 41
},
{
"icon": {
@@ -1094,7 +1175,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 38
"iconIdx": 42
},
{
"icon": {
@@ -1120,7 +1201,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 39
"iconIdx": 43
},
{
"icon": {
@@ -1146,7 +1227,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 40
"iconIdx": 44
},
{
"icon": {
@@ -1172,7 +1253,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 41
"iconIdx": 45
},
{
"icon": {
@@ -1198,7 +1279,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 42
"iconIdx": 46
},
{
"icon": {
@@ -1224,7 +1305,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 43
"iconIdx": 47
},
{
"icon": {
@@ -1253,7 +1334,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 44
"iconIdx": 48
},
{
"icon": {
@@ -1283,7 +1364,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 45
"iconIdx": 49
},
{
"icon": {
@@ -1313,7 +1394,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 46
"iconIdx": 50
},
{
"icon": {
@@ -1339,7 +1420,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 47
"iconIdx": 51
},
{
"icon": {
@@ -1365,7 +1446,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 48
"iconIdx": 52
},
{
"icon": {
@@ -1391,7 +1472,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 49
"iconIdx": 53
}
],
"height": 1024,

View File

@@ -0,0 +1,82 @@
// @flow
import moment from 'moment';
import i18next from './i18next';
// MomentJS uses static language bundle loading, so in order to support dynamic
// language selection in the app we need to load all bundles that we support in
// the app.
require('moment/locale/bg');
require('moment/locale/de');
require('moment/locale/eo');
require('moment/locale/es');
require('moment/locale/fr');
require('moment/locale/hy-am');
require('moment/locale/it');
require('moment/locale/nb');
// OC is not available. Please submit OC translation to the MomentJS project.
require('moment/locale/pl');
require('moment/locale/pt');
require('moment/locale/pt-br');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sl');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/zh-cn');
/**
* Returns a localized date formatter initialized with a specific {@code Date}
* or timestamp ({@code number}).
*
* @private
* @param {Date | number} dateOrTimeStamp - The date or unix timestamp (ms)
* to format.
* @returns {Object}
*/
export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
return moment(dateOrTimeStamp).locale(_getSupportedLocale());
}
/**
* Returns a localized duration formatter initialized with a
* specific duration ({@code number}).
*
* @private
* @param {number} duration - The duration (ms)
* to format.
* @returns {Object}
*/
export function getLocalizedDurationFormatter(duration: number) {
return moment.duration(duration).locale(_getSupportedLocale());
}
/**
* A lenient locale matcher to match language and dialect if possible.
*
* @private
* @returns {string}
*/
function _getSupportedLocale() {
const i18nLocale = i18next.language;
let supportedLocale;
if (i18nLocale) {
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$');
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase());
if (localeResult) {
const currentLocaleRegexp
= new RegExp(
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`);
supportedLocale
= moment.locales().find(lang => currentLocaleRegexp.exec(lang));
}
}
return supportedLocale || 'en';
}

View File

@@ -1,4 +1,4 @@
export * from './dateUtil';
export * from './functions';
// TODO Eventually (e.g. when the non-React Web app is rewritten into React), it

View File

@@ -254,7 +254,7 @@ class CalleeInfo extends Component<Props, State> {
if (this.state.renderAudio && this.state.ringing) {
return (
<Audio
ref = { this._setAudio }
setRef = { this._setAudio }
src = './sounds/ring.ogg' />
);
}

View File

@@ -77,10 +77,10 @@ function _maybeSetCalleeInfoVisible({ dispatch, getState }, next, action) {
const result = next(action);
const state = getState();
const stateFeaturesJWT = state['features/base/jwt'];
const stateFeaturesBaseJWT = state['features/base/jwt'];
let calleeInfoVisible;
if (stateFeaturesJWT.callee) {
if (stateFeaturesBaseJWT.callee) {
const { conference, leaving, room } = state['features/base/conference'];
// XXX The CalleeInfo is to be displayed/visible as soon as possible
@@ -112,7 +112,7 @@ function _maybeSetCalleeInfoVisible({ dispatch, getState }, next, action) {
// However, the CallDialog is not to be displayed/visible again
// after all remote participants leave.
if (calleeInfoVisible
&& stateFeaturesJWT.calleeInfoVisible === false) {
&& stateFeaturesBaseJWT.calleeInfoVisible === false) {
calleeInfoVisible = false;
}
break;

View File

@@ -1,3 +1,5 @@
// @flow
import { NativeModules } from 'react-native';
import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
@@ -33,7 +35,7 @@ const SOCK_STREAM = 1; /* stream socket */
*
* @class
*/
export default function _RTCPeerConnection(...args) {
export default function _RTCPeerConnection(...args: any[]) {
/* eslint-disable indent, no-invalid-this */
@@ -50,6 +52,8 @@ export default function _RTCPeerConnection(...args) {
// _RTCPeerConnection's prototype may (or may not, I don't know) work but I
// don't want to try because the following approach appears to work and I
// understand it.
// $FlowFixMe
Object.defineProperty(this, 'onaddstream', {
configurable: true,
enumerable: true,
@@ -67,6 +71,14 @@ export default function _RTCPeerConnection(...args) {
_RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype);
_RTCPeerConnection.prototype.constructor = _RTCPeerConnection;
_RTCPeerConnection.prototype.addIceCandidate
= _makePromiseAware(RTCPeerConnection.prototype.addIceCandidate, 1, 0);
_RTCPeerConnection.prototype.createAnswer
= _makePromiseAware(RTCPeerConnection.prototype.createAnswer, 0, 1);
_RTCPeerConnection.prototype.createOffer
= _makePromiseAware(RTCPeerConnection.prototype.createOffer, 0, 1);
_RTCPeerConnection.prototype._invokeOnaddstream = function(...args) {
const onaddstream = this._onaddstream;
@@ -90,6 +102,9 @@ _RTCPeerConnection.prototype._queueOnaddstream = function(...args) {
this._onaddstreamQueue.push(Array.from(args));
};
_RTCPeerConnection.prototype.setLocalDescription
= _makePromiseAware(RTCPeerConnection.prototype.setLocalDescription, 1, 0);
_RTCPeerConnection.prototype.setRemoteDescription = function(
sessionDescription,
successCallback,
@@ -128,6 +143,49 @@ function _LOGE(...args) {
console && console.error && console.error(...args);
}
/**
* Makes a {@code Promise}-returning function out of a specific void function
* with {@code successCallback} and {@code failureCallback}.
*
* @param {Function} f - The (void) function with {@code successCallback} and
* {@code failureCallback}.
* @param {number} beforeCallbacks - The number of arguments before
* {@code successCallback} and {@code failureCallback}.
* @param {number} afterCallbacks - The number of arguments after
* {@code successCallback} and {@code failureCallback}.
* @returns {Promise}
*/
function _makePromiseAware(
f: Function,
beforeCallbacks: number,
afterCallbacks: number) {
return function(...args) {
return new Promise((resolve, reject) => {
if (args.length <= beforeCallbacks + afterCallbacks) {
args.splice(beforeCallbacks, 0, resolve, reject);
}
let fPromise;
try {
// eslint-disable-next-line no-invalid-this
fPromise = f.apply(this, args);
} catch (e) {
reject(e);
}
// If the super implementation returns a Promise from the deprecated
// invocation by any chance, try to make sense of it.
if (fPromise) {
const { then } = fPromise;
typeof then === 'function'
&& then.call(fPromise, resolve, reject);
}
});
};
}
/**
* Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription}
* implementation which uses the deprecated, callback-based version to the

View File

@@ -149,6 +149,38 @@ function _visitNode(node, callback) {
document.cookie = '';
}
// document.implementation
//
// Required by:
// - jQuery
if (typeof document.implementation === 'undefined') {
document.implementation = {};
}
// document.implementation.createHTMLDocument
//
// Required by:
// - jQuery
if (typeof document.implementation.createHTMLDocument === 'undefined') {
document.implementation.createHTMLDocument = function(title = '') {
const htmlDocument
= new DOMParser().parseFromString(
`<html>
<head><title>${title}</title></head>
<body></body>
</html>`,
'text/xml');
Object.defineProperty(htmlDocument, 'body', {
get() {
return htmlDocument.getElementsByTagName('body')[0];
}
});
return htmlDocument;
};
}
// Element.querySelector
//
// Required by:
@@ -372,6 +404,6 @@ function _visitNode(node, callback) {
global.clearTimeout = BackgroundTimer.clearTimeout.bind(BackgroundTimer);
global.clearInterval = BackgroundTimer.clearInterval.bind(BackgroundTimer);
global.setInterval = BackgroundTimer.setInterval.bind(BackgroundTimer);
global.setTimeout = BackgroundTimer.setTimeout.bind(BackgroundTimer);
global.setTimeout = (fn, ms = 0) => BackgroundTimer.setTimeout(fn, ms);
})(global || window || this); // eslint-disable-line no-invalid-this

View File

@@ -15,15 +15,18 @@ import RTCPeerConnection from './RTCPeerConnection';
if (typeof global.MediaStreamTrack === 'undefined') {
global.MediaStreamTrack = MediaStreamTrack;
}
if (typeof global.RTCIceCandidate === 'undefined') {
global.RTCIceCandidate = RTCIceCandidate;
}
if (typeof global.RTCPeerConnection === 'undefined') {
global.RTCPeerConnection = RTCPeerConnection;
}
if (typeof global.webkitRTCPeerConnection === 'undefined') {
global.webkitRTCPeerConnection = RTCPeerConnection;
}
if (typeof global.RTCSessionDescription === 'undefined') {
global.RTCSessionDescription = RTCSessionDescription;
}
if (typeof global.RTCIceCandidate === 'undefined') {
global.RTCIceCandidate = RTCIceCandidate;
}
const navigator = global.navigator;

View File

@@ -1,116 +1,121 @@
// @flow
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Component } from 'react';
/**
* Describes audio element interface used in the base/media feature for audio
* playback.
*/
export type AudioElement = {
pause: () => void,
play: () => void,
setSinkId?: string => void
};
/**
* {@code AbstractAudio} component's property types.
*/
type Props = {
/**
* A callback which will be called with {@code AbstractAudio} instance once
* the audio element is loaded.
*/
setRef?: ?AudioElement => void,
/**
* The URL of a media resource to use in the element.
*
* NOTE on react-native sound files are imported through 'require' and then
* passed as the 'src' parameter which means their type will be 'any'.
*
* @type {Object | string}
*/
src: Object | string,
stream: Object
}
/**
* The React {@link Component} which is similar to Web's
* {@code HTMLAudioElement}.
*/
export default class AbstractAudio extends Component<*> {
export default class AbstractAudio extends Component<Props> {
/**
* The (reference to the) {@link ReactElement} which actually implements
* this {@code AbstractAudio}.
* The {@link AudioElement} instance which implements the audio playback
* functionality.
*/
_ref: ?Object;
_setRef: Function;
/**
* {@code AbstractAudio} component's property types.
*
* @static
*/
static propTypes = {
/**
* The URL of a media resource to use in the element.
*
* @type {string}
*/
src: PropTypes.string,
stream: PropTypes.object
};
_audioElementImpl: ?AudioElement;
/**
* Initializes a new {@code AbstractAudio} instance.
*
* @param {Object} props - The read-only properties with which the new
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Object) {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._setRef = this._setRef.bind(this);
// Bind event handlers so they are only bound once per instance.
this.setAudioElementImpl = this.setAudioElementImpl.bind(this);
}
pause: () => void;
/**
* Attempts to pause the playback of the media.
*
* @public
* @returns {void}
*/
pause() {
this._ref && typeof this._ref.pause === 'function' && this._ref.pause();
pause(): void {
this._audioElementImpl && this._audioElementImpl.pause();
}
play: () => void;
/**
* Attempts to being the playback of the media.
*
* @public
* @returns {void}
*/
play() {
this._ref && typeof this._ref.play === 'function' && this._ref.play();
play(): void {
this._audioElementImpl && this._audioElementImpl.play();
}
setAudioElementImpl: ?AudioElement => void;
/**
* Renders this {@code AbstractAudio} as a React {@link Component} of a
* specific type.
* Set the (reference to the) {@link AudioElement} object which implements
* the audio playback functionality.
*
* @param {string|ReactClass} type - The type of the React {@code Component}
* which is to be rendered.
* @param {Object|undefined} props - The read-only React {@code Component}
* properties, if any, to render. If {@code undefined}, the props of this
* instance will be rendered.
* @param {AudioElement} element - The {@link AudioElement} instance
* which implements the audio playback functionality.
* @protected
* @returns {ReactElement}
*/
_render(type, props) {
const {
children,
/* eslint-disable no-unused-vars */
// The following properties are consumed by React itself so they are
// to not be propagated.
ref,
/* eslint-enable no-unused-vars */
...filteredProps
} = props || this.props;
return (
React.createElement(
type,
{
...filteredProps,
ref: this._setRef
},
children));
}
/**
* Set the (reference to the) {@link ReactElement} which actually implements
* this {@code AbstractAudio}.
*
* @param {Object} ref - The (reference to the) {@code ReactElement} which
* actually implements this {@code AbstractAudio}.
* @private
* @returns {void}
*/
_setRef(ref) {
this._ref = ref;
setAudioElementImpl(element: ?AudioElement): void {
this._audioElementImpl = element;
// setRef
const { setRef } = this.props;
// $FlowFixMe
typeof setRef === 'function' && setRef(element ? this : null);
}
setSinkId: string => void;
/**
* Sets the sink ID (output device ID) on the underlying audio element.
* NOTE: Currently, implemented only on Web.
*
* @param {string} sinkId - The sink ID (output device ID).
* @returns {void}
*/
setSinkId(sinkId: string): void {
this._audioElementImpl
&& typeof this._audioElementImpl.setSinkId === 'function'
&& this._audioElementImpl.setSinkId(sinkId);
}
}

View File

@@ -1,19 +1,66 @@
/* @flow */
import Sound from 'react-native-sound';
import AbstractAudio from '../AbstractAudio';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The React Native/mobile {@link Component} which is similar to Web's
* {@code HTMLAudioElement} and wraps around react-native-webrtc's
* {@link RTCView}.
*/
export default class Audio extends AbstractAudio {
/**
* {@code Audio} component's property types.
*
* @static
* Reference to 'react-native-sound} {@link Sound} instance.
*/
static propTypes = AbstractAudio.propTypes;
_sound: Sound
/**
* A callback passed to the 'react-native-sound''s {@link Sound} instance,
* called when loading sound is finished.
*
* @param {Object} error - The error object passed by
* the 'react-native-sound' library.
* @returns {void}
* @private
*/
_soundLoadedCallback(error) {
if (error) {
logger.error('Failed to load sound', error);
} else {
this.setAudioElementImpl(this._sound);
}
}
/**
* Will load the sound, after the component did mount.
*
* @returns {void}
*/
componentDidMount() {
this._sound
= this.props.src
? new Sound(
this.props.src,
this._soundLoadedCallback.bind(this))
: null;
}
/**
* Will dispose sound resources (if any) when component is about to unmount.
*
* @returns {void}
*/
componentWillUnmount() {
if (this._sound) {
this.setAudioElementImpl(null);
this._sound.release();
this._sound = null;
}
}
/**
* Implements React's {@link Component#render()}.

View File

@@ -19,10 +19,10 @@ export default StyleSheet.create({
*/
videoCover: {
backgroundColor: ColorPalette.black,
height: '100%',
bottom: 0,
left: 0,
position: 'absolute',
top: 0,
width: '100%'
right: 0,
top: 0
}
});

View File

@@ -1,6 +1,9 @@
/* @flow */
// @flow
import React from 'react';
import AbstractAudio from '../AbstractAudio';
import type { AudioElement } from '../AbstractAudio';
/**
* The React/Web {@link Component} which is similar to and wraps around
@@ -8,11 +11,28 @@ import AbstractAudio from '../AbstractAudio';
*/
export default class Audio extends AbstractAudio {
/**
* {@code Audio} component's property types.
*
* @static
* Set to <code>true</code> when the whole file is loaded.
*/
static propTypes = AbstractAudio.propTypes;
_audioFileLoaded: boolean;
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
_ref: ?AudioElement;
/**
* Creates new <code>Audio</code> element instance with given props.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onCanPlayThrough = this._onCanPlayThrough.bind(this);
this._setRef = this._setRef.bind(this);
}
/**
* Implements React's {@link Component#render()}.
@@ -21,6 +41,67 @@ export default class Audio extends AbstractAudio {
* @returns {ReactElement}
*/
render() {
return super._render('audio');
return (
<audio
onCanPlayThrough = { this._onCanPlayThrough }
preload = 'auto'
// $FlowFixMe
ref = { this._setRef }
src = { this.props.src } />
);
}
/**
* If audio element reference has been set and the file has been
* loaded then {@link setAudioElementImpl} will be called to eventually add
* the audio to the Redux store.
*
* @private
* @returns {void}
*/
_maybeSetAudioElementImpl() {
if (this._ref && this._audioFileLoaded) {
this.setAudioElementImpl(this._ref);
}
}
_onCanPlayThrough: () => void;
/**
* Called when 'canplaythrough' event is triggered on the audio element,
* which means that the whole file has been loaded.
*
* @private
* @returns {void}
*/
_onCanPlayThrough() {
this._audioFileLoaded = true;
this._maybeSetAudioElementImpl();
}
_setRef: (?AudioElement) => void;
/**
* Sets the reference to the HTML audio element.
*
* @param {HTMLAudioElement} audioElement - The HTML audio element instance.
* @private
* @returns {void}
*/
_setRef(audioElement: ?AudioElement) {
this._ref = audioElement;
if (audioElement) {
this._maybeSetAudioElementImpl();
} else {
// AbstractAudioElement is supposed to trigger "removeAudio" only if
// it was previously added, so it's safe to just call it.
this.setAudioElementImpl(null);
// Reset the loaded flag, as the audio element is being removed from
// the DOM tree.
this._audioFileLoaded = false;
}
}
}

View File

@@ -28,6 +28,20 @@ export const LOCAL_PARTICIPANT_DEFAULT_ID = 'local';
*/
export const MAX_DISPLAY_NAME_LENGTH = 50;
/**
* The identifier of the sound to be played when new remote participant joins
* the room.
* @type {string}
*/
export const PARTICIPANT_JOINED_SOUND_ID = 'PARTICIPANT_JOINED_SOUND';
/**
* The identifier of the sound to be played when remote participant leaves
* the room.
* @type {string}
*/
export const PARTICIPANT_LEFT_SOUND_ID = 'PARTICIPANT_LEFT_SOUND';
/**
* The set of possible XMPP MUC roles for conference participants.
*

View File

@@ -2,11 +2,13 @@
import UIEvents from '../../../../service/UI/UIEvents';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../app';
import {
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../conference';
import { MiddlewareRegistry } from '../redux';
import { playSound, registerSound, unregisterSound } from '../sounds';
import { localParticipantIdChanged } from './actions';
import {
@@ -14,13 +16,23 @@ import {
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_DISPLAY_NAME_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED
} from './actionTypes';
import { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants';
import {
LOCAL_PARTICIPANT_DEFAULT_ID,
PARTICIPANT_JOINED_SOUND_ID,
PARTICIPANT_LEFT_SOUND_ID
} from './constants';
import {
getAvatarURLByParticipantId,
getLocalParticipant
getLocalParticipant,
getParticipantCount
} from './functions';
import {
PARTICIPANT_JOINED_SRC,
PARTICIPANT_LEFT_SRC
} from './sounds';
declare var APP: Object;
@@ -34,7 +46,18 @@ declare var APP: Object;
MiddlewareRegistry.register(store => next => action => {
const { conference } = store.getState()['features/base/conference'];
if (action.type === PARTICIPANT_JOINED
|| action.type === PARTICIPANT_LEFT) {
_maybePlaySounds(store, action);
}
switch (action.type) {
case APP_WILL_MOUNT:
_registerSounds(store);
break;
case APP_WILL_UNMOUNT:
_unregisterSounds(store);
break;
case CONFERENCE_JOINED:
store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
break;
@@ -100,3 +123,59 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Plays sounds when participants join/leave conference.
*
* @param {Store} store - The Redux store.
* @param {Action} action - The Redux action. Should be either
* {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
* @private
* @returns {void}
*/
function _maybePlaySounds({ getState, dispatch }, action) {
const state = getState();
const { startAudioMuted } = state['features/base/config'];
// We're not playing sounds for local participant
// nor when the user is joining past the "startAudioMuted" limit.
// The intention there was to not play user joined notification in big
// conferences where 100th person is joining.
if (!action.participant.local
&& (!startAudioMuted
|| getParticipantCount(state) < startAudioMuted)) {
if (action.type === PARTICIPANT_JOINED) {
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
} else if (action.type === PARTICIPANT_LEFT) {
dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
}
}
}
/**
* Registers sounds related with the participants feature.
*
* @param {Store} store - The Redux store.
* @private
* @returns {void}
*/
function _registerSounds({ dispatch }) {
dispatch(
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_SRC));
dispatch(
registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_SRC));
}
/**
* Unregisters sounds related with the participants feature.
*
* @param {Store} store - The Redux store.
* @private
* @returns {void}
*/
function _unregisterSounds({ dispatch }) {
dispatch(
unregisterSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_SRC));
dispatch(
unregisterSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_SRC));
}

View File

@@ -0,0 +1,13 @@
/**
* Points to the sound file which will be played when new participant joins
* the conference.
*/
export const PARTICIPANT_JOINED_SRC
= require('../../../../sounds/joined.wav');
/**
* Points to the sound file which will be played when any participant leaves
* the conference.
*/
export const PARTICIPANT_LEFT_SRC
= require('../../../../sounds/left.wav');

View File

@@ -0,0 +1,11 @@
/**
* Points to the sound file which will be played when new participant joins
* the conference.
*/
export const PARTICIPANT_JOINED_SRC = 'sounds/joined.wav';
/**
* Points to the sound file which will be played when any participant leaves
* the conference.
*/
export const PARTICIPANT_LEFT_SRC = 'sounds/left.wav';

View File

@@ -1,15 +0,0 @@
/* @flow */
/**
* Retreives the current profile settings from redux store. The profile
* is persisted to localStorage so it's a good candidate to store settings
* in it.
*
* @param {Object} state - The Redux state.
* @returns {Object}
*/
export function getProfile(state: Object) {
const profileStateSlice = state['features/base/profile'];
return profileStateSlice ? profileStateSlice.profile || {} : {};
}

View File

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

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