Compare commits
45 Commits
2610
...
bgrozev-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cc272fbd1 | ||
|
|
2334eb9967 | ||
|
|
04bd4a9038 | ||
|
|
eb8f34cee8 | ||
|
|
b9379f5996 | ||
|
|
40d7d0c9cb | ||
|
|
357f173e85 | ||
|
|
7da26042b3 | ||
|
|
c86c7beb24 | ||
|
|
1020a54a33 | ||
|
|
c84abd543e | ||
|
|
4b17c6f015 | ||
|
|
cb973b61aa | ||
|
|
b096622995 | ||
|
|
ae0bf876a8 | ||
|
|
bba480f329 | ||
|
|
4dbcaf851f | ||
|
|
04dff9059b | ||
|
|
26cd2f17f6 | ||
|
|
60e03e3dec | ||
|
|
bfb45ed0e8 | ||
|
|
e325199075 | ||
|
|
4e4713c3e2 | ||
|
|
ff8386e931 | ||
|
|
8f520086e5 | ||
|
|
5cde674eff | ||
|
|
c018252eee | ||
|
|
c8cab1560c | ||
|
|
d218abfd97 | ||
|
|
9e0fee6c7d | ||
|
|
5dca9e08f4 | ||
|
|
d3a1f7d4f7 | ||
|
|
80bdf908ca | ||
|
|
0d3b4eedf8 | ||
|
|
824a8a8864 | ||
|
|
45c1438fe6 | ||
|
|
8caeabf6b4 | ||
|
|
466561f99f | ||
|
|
8dc866fab3 | ||
|
|
1c8b8e031b | ||
|
|
796489dc77 | ||
|
|
a30412ba65 | ||
|
|
8b35ea8ad5 | ||
|
|
e05f2a9027 | ||
|
|
0d5cc8898d |
19
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -412,6 +402,7 @@ rules file:
|
||||
# WebRTC
|
||||
|
||||
-keep class org.webrtc.** { *; }
|
||||
-dontwarn org.chromium.build.BuildHooksAndroid
|
||||
|
||||
# Jisti Meet SDK
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
@@ -103,4 +105,10 @@ public class MainActivity extends JitsiMeetActivity {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,7 @@ 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;
|
||||
@@ -53,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.
|
||||
@@ -107,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())
|
||||
@@ -115,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(
|
||||
@@ -266,13 +275,15 @@ public class JitsiMeetView extends FrameLayout {
|
||||
* @param params {@code WritableMap} optional ancillary data for the event.
|
||||
*/
|
||||
private static void sendEvent(
|
||||
String eventName, @Nullable WritableMap params) {
|
||||
String eventName,
|
||||
@Nullable WritableMap params) {
|
||||
if (reactInstanceManager != null) {
|
||||
ReactContext reactContext
|
||||
= reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null) {
|
||||
reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.getJSModule(
|
||||
DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
}
|
||||
@@ -498,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -754,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,
|
||||
|
||||
@@ -245,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.
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -36,9 +36,15 @@
|
||||
.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";
|
||||
}
|
||||
|
||||
@@ -28,11 +28,8 @@
|
||||
@import 'font-awesome';
|
||||
/* Fonts END */
|
||||
|
||||
@import 'flag-icon';
|
||||
|
||||
/* Modules BEGIN */
|
||||
|
||||
@import 'dial-out';
|
||||
@import 'aui_reset';
|
||||
@import 'base';
|
||||
@import 'utils';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
background-color: #fff;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
padding: 35px 0;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
|
||||
@@ -14,7 +13,7 @@
|
||||
color: $unsupportedBrowserTextColor;
|
||||
margin: auto;
|
||||
max-width: 40em;
|
||||
padding-bottom: 40px;
|
||||
padding: 35px 0 40px 0;
|
||||
text-align: center;
|
||||
width: 75%;
|
||||
|
||||
|
||||
7
debian/jitsi-meet-prosody.postinst
vendored
@@ -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
|
||||
|
||||
BIN
fonts/jitsi.eot
@@ -12,6 +12,7 @@
|
||||
<glyph unicode="" 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="" 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="" glyph-name="navigate_before" d="M658 708l-196-196 196-196-60-60-256 256 256 256z" />
|
||||
<glyph unicode="" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
|
||||
<glyph unicode="" 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="" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
|
||||
<glyph unicode="" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
|
||||
@@ -22,6 +23,7 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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 |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.woff
@@ -1,6 +1,60 @@
|
||||
{
|
||||
"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": [
|
||||
@@ -24,9 +78,9 @@
|
||||
"code": 58834,
|
||||
"name": "menu"
|
||||
},
|
||||
"setIdx": 0,
|
||||
"setId": 2,
|
||||
"iconIdx": 489
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 1
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -53,7 +107,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 0
|
||||
"iconIdx": 2
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -80,7 +134,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 1
|
||||
"iconIdx": 3
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -107,7 +161,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 2
|
||||
"iconIdx": 4
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -134,7 +188,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 3
|
||||
"iconIdx": 5
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -154,14 +208,14 @@
|
||||
"properties": {
|
||||
"ligatures": "timer",
|
||||
"id": 760,
|
||||
"order": 916,
|
||||
"order": 928,
|
||||
"prevSize": 24,
|
||||
"code": 58405,
|
||||
"name": "timer"
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 4
|
||||
"iconIdx": 6
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -188,7 +242,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 5
|
||||
"iconIdx": 7
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -215,7 +269,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 6
|
||||
"iconIdx": 8
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -242,7 +296,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 7
|
||||
"iconIdx": 9
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -269,7 +323,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 8
|
||||
"iconIdx": 10
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -298,7 +352,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 9
|
||||
"iconIdx": 11
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -325,7 +379,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 10
|
||||
"iconIdx": 12
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -352,7 +406,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 11
|
||||
"iconIdx": 13
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -381,7 +435,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 12
|
||||
"iconIdx": 14
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -410,7 +464,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 13
|
||||
"iconIdx": 15
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -439,7 +493,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 14
|
||||
"iconIdx": 16
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -468,7 +522,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 15
|
||||
"iconIdx": 17
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -497,7 +551,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 16
|
||||
"iconIdx": 18
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -523,7 +577,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 17
|
||||
"iconIdx": 19
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -549,7 +603,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 18
|
||||
"iconIdx": 20
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -575,7 +629,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 19
|
||||
"iconIdx": 21
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -601,7 +655,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 20
|
||||
"iconIdx": 22
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -627,7 +681,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 21
|
||||
"iconIdx": 23
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -653,7 +707,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 22
|
||||
"iconIdx": 24
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -679,7 +733,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 23
|
||||
"iconIdx": 25
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -705,7 +759,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 24
|
||||
"iconIdx": 26
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -731,7 +785,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 25
|
||||
"iconIdx": 27
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -757,7 +811,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 26
|
||||
"iconIdx": 28
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -783,7 +837,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 27
|
||||
"iconIdx": 29
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -809,7 +863,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 28
|
||||
"iconIdx": 30
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -835,7 +889,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 29
|
||||
"iconIdx": 31
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -861,7 +915,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 30
|
||||
"iconIdx": 32
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -887,7 +941,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 31
|
||||
"iconIdx": 33
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -913,7 +967,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 32
|
||||
"iconIdx": 34
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -939,7 +993,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 33
|
||||
"iconIdx": 35
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -965,7 +1019,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 34
|
||||
"iconIdx": 36
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -991,7 +1045,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 35
|
||||
"iconIdx": 37
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1017,7 +1071,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 36
|
||||
"iconIdx": 38
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1043,7 +1097,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 37
|
||||
"iconIdx": 39
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1069,7 +1123,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 38
|
||||
"iconIdx": 40
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1095,7 +1149,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 39
|
||||
"iconIdx": 41
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1121,7 +1175,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 40
|
||||
"iconIdx": 42
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1147,7 +1201,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 41
|
||||
"iconIdx": 43
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1173,7 +1227,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 42
|
||||
"iconIdx": 44
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1199,7 +1253,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 43
|
||||
"iconIdx": 45
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1225,7 +1279,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 44
|
||||
"iconIdx": 46
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1251,7 +1305,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 45
|
||||
"iconIdx": 47
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1280,7 +1334,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 46
|
||||
"iconIdx": 48
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1310,7 +1364,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 47
|
||||
"iconIdx": 49
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1340,7 +1394,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 48
|
||||
"iconIdx": 50
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1366,7 +1420,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 49
|
||||
"iconIdx": 51
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1392,7 +1446,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 50
|
||||
"iconIdx": 52
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1418,7 +1472,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 51
|
||||
"iconIdx": 53
|
||||
}
|
||||
],
|
||||
"height": 1024,
|
||||
|
||||
BIN
images/calendar@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/calendar@3x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"audio": "Voice",
|
||||
"video": "Video"
|
||||
},
|
||||
"calendar": "Calendar",
|
||||
"go": "GO",
|
||||
"join": "JOIN",
|
||||
"privacy": "Privacy",
|
||||
@@ -452,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": {
|
||||
@@ -495,11 +503,13 @@
|
||||
"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",
|
||||
@@ -517,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,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);
|
||||
|
||||
@@ -529,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);
|
||||
};
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ export default class SharedVideoManager {
|
||||
|
||||
thumb.setDisplayName('YouTube');
|
||||
VideoLayout.addRemoteVideoContainer(self.url, thumb);
|
||||
VideoLayout.resizeThumbnails(false, true);
|
||||
|
||||
const iframe = player.getIframe();
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
110
package-lock.json
generated
@@ -1160,7 +1160,7 @@
|
||||
"resolved": "https://registry.npmjs.org/autosize/-/autosize-1.18.13.tgz",
|
||||
"integrity": "sha1-7ljP8O2dzVDB/xB+culUAxyRl78=",
|
||||
"requires": {
|
||||
"jquery": "2.1.4"
|
||||
"jquery": "3.3.1"
|
||||
}
|
||||
},
|
||||
"aws-sign2": {
|
||||
@@ -3754,14 +3754,14 @@
|
||||
}
|
||||
},
|
||||
"duplexify": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz",
|
||||
"integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==",
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz",
|
||||
"integrity": "sha512-JzYSLYMhoVVBe8+mbHQ4KgpvHpm0DZpJuL8PY93Vyv1fW7jYJ90LoXa1di/CVbJM+TgMs91rbDapE/RNIfnJsA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"end-of-stream": "1.4.1",
|
||||
"inherits": "2.0.3",
|
||||
"readable-stream": "2.3.4",
|
||||
"readable-stream": "2.3.5",
|
||||
"stream-shift": "1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -3772,9 +3772,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz",
|
||||
"integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
|
||||
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
@@ -4982,7 +4982,7 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "2.0.3",
|
||||
"readable-stream": "2.3.4"
|
||||
"readable-stream": "2.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
@@ -4992,9 +4992,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz",
|
||||
"integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
|
||||
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
@@ -5069,7 +5069,7 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "2.0.3",
|
||||
"readable-stream": "2.3.4"
|
||||
"readable-stream": "2.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
@@ -5079,9 +5079,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz",
|
||||
"integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
|
||||
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
@@ -6980,9 +6980,9 @@
|
||||
"version": "github:jitsi/jitsi-meet-logger#6fff754a77a56ab52499f3559105a15886942a1e"
|
||||
},
|
||||
"jquery": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-2.1.4.tgz",
|
||||
"integrity": "sha1-IoveaYoMYUMdwmMKahVPFYkNIxc="
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz",
|
||||
"integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="
|
||||
},
|
||||
"jquery-contextmenu": {
|
||||
"version": "2.4.5",
|
||||
@@ -7011,7 +7011,7 @@
|
||||
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
|
||||
},
|
||||
"js-utils": {
|
||||
"version": "github:jitsi/js-utils#fa6eb93d1792b75e25bf84b97e51672e70d900de",
|
||||
"version": "github:jitsi/js-utils#d4b78721b754a15c2588b7b0c621a9fff6fa1363",
|
||||
"requires": {
|
||||
"bowser": "1.9.1"
|
||||
},
|
||||
@@ -7220,12 +7220,12 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#0dcc8a04915c4fdb6aecabf1b036df58e7fe02fc",
|
||||
"version": "github:jitsi/lib-jitsi-meet#410ad5e76d33b1422ce34f83719eda7dfd584e22",
|
||||
"requires": {
|
||||
"async": "0.9.0",
|
||||
"current-executing-script": "0.1.3",
|
||||
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#6fff754a77a56ab52499f3559105a15886942a1e",
|
||||
"js-utils": "github:jitsi/js-utils#fa6eb93d1792b75e25bf84b97e51672e70d900de",
|
||||
"js-utils": "github:jitsi/js-utils#d4b78721b754a15c2588b7b0c621a9fff6fa1363",
|
||||
"react-native-callstats": "3.27.0",
|
||||
"sdp-interop": "0.1.12",
|
||||
"sdp-simulcast": "0.2.1",
|
||||
@@ -7929,7 +7929,7 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"concat-stream": "1.6.0",
|
||||
"duplexify": "3.5.3",
|
||||
"duplexify": "3.5.4",
|
||||
"end-of-stream": "1.4.1",
|
||||
"flush-write-stream": "1.0.2",
|
||||
"from2": "2.3.0",
|
||||
@@ -8623,7 +8623,7 @@
|
||||
"requires": {
|
||||
"cyclist": "0.2.2",
|
||||
"inherits": "2.0.3",
|
||||
"readable-stream": "2.3.4"
|
||||
"readable-stream": "2.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
@@ -8633,9 +8633,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.4.tgz",
|
||||
"integrity": "sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==",
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
|
||||
"integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
@@ -9552,7 +9552,7 @@
|
||||
"integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"duplexify": "3.5.3",
|
||||
"duplexify": "3.5.4",
|
||||
"inherits": "2.0.3",
|
||||
"pump": "2.0.1"
|
||||
}
|
||||
@@ -9845,6 +9845,11 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz",
|
||||
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
|
||||
},
|
||||
"react-native-calendar-events": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz",
|
||||
"integrity": "sha1-KYBOi0TWlG5pq1ogkC2USe0xXEc="
|
||||
},
|
||||
"react-native-callstats": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-callstats/-/react-native-callstats-3.27.0.tgz",
|
||||
@@ -9905,6 +9910,11 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-prompt/-/react-native-prompt-1.0.0.tgz",
|
||||
"integrity": "sha1-QeDsKqfdjxLzo+6Dr51jxLZw+KE="
|
||||
},
|
||||
"react-native-sound": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.4.tgz",
|
||||
"integrity": "sha512-V9v4CjKgv8ekQRLOJSoKA7pxJ03F4Ih3T/RfMIlMWLktz7v/O4sdJPjRBLOzZRqAnr9FWTLbSk1ZCjioXh3mjQ=="
|
||||
},
|
||||
"react-native-vector-icons": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-4.4.2.tgz",
|
||||
@@ -9938,7 +9948,7 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "github:jitsi/react-native-webrtc#806435b41fa152a8239ebeb7d002d1c6e979be86",
|
||||
"version": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
|
||||
"requires": {
|
||||
"base64-js": "1.2.3",
|
||||
"event-target-shim": "1.1.1",
|
||||
@@ -10724,6 +10734,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"serialize-javascript": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.4.0.tgz",
|
||||
"integrity": "sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=",
|
||||
"dev": true
|
||||
},
|
||||
"serve-favicon": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.3.2.tgz",
|
||||
@@ -11664,20 +11680,48 @@
|
||||
"optional": true
|
||||
},
|
||||
"uglifyjs-webpack-plugin": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.1.2.tgz",
|
||||
"integrity": "sha512-k07cmJTj+8vZMSc3BaQ9uW7qVl2MqDts4ti4KaNACXEcXSw2vQM2S8olSk/CODxvcSFGvUHzNSqA8JQlhgUJPw==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.2.tgz",
|
||||
"integrity": "sha512-CG/NvzXfemUAm5Y4Guh5eEaJYHtkG7kKNpXEJHp9QpxsFVB5/qKvYWoMaq4sa99ccZ0hM3MK8vQV9XPZB4357A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cacache": "10.0.4",
|
||||
"find-cache-dir": "1.0.0",
|
||||
"schema-utils": "0.3.0",
|
||||
"schema-utils": "0.4.5",
|
||||
"serialize-javascript": "1.4.0",
|
||||
"source-map": "0.6.1",
|
||||
"uglify-es": "3.3.9",
|
||||
"webpack-sources": "1.1.0",
|
||||
"worker-farm": "1.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.2.1.tgz",
|
||||
"integrity": "sha1-KKarxJOiq+D7TIUHrK7bQ/pVBnE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "1.0.0",
|
||||
"fast-json-stable-stringify": "2.0.0",
|
||||
"json-schema-traverse": "0.3.1"
|
||||
}
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.1.0.tgz",
|
||||
"integrity": "sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=",
|
||||
"dev": true
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz",
|
||||
"integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "6.2.1",
|
||||
"ajv-keywords": "3.1.0"
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
|
||||
10
package.json
@@ -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#0dcc8a04915c4fdb6aecabf1b036df58e7fe02fc",
|
||||
"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"
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
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';
|
||||
@@ -274,6 +275,7 @@ export class AbstractApp extends Component {
|
||||
<Provider store = { this._getStore() }>
|
||||
<Fragment>
|
||||
{ this._createElement(component) }
|
||||
<SoundCollection />
|
||||
<OverlayContainer />
|
||||
</Fragment>
|
||||
</Provider>
|
||||
@@ -501,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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
@@ -330,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.
|
||||
|
||||
@@ -64,6 +64,7 @@ const WHITELISTED_KEYS = [
|
||||
'failICE',
|
||||
'firefox_fake_device',
|
||||
'forceJVB121Ratio',
|
||||
'gatherStats',
|
||||
'hiddenDomain',
|
||||
'hosts',
|
||||
'iAmRecorder',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,60 @@
|
||||
{
|
||||
"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": [
|
||||
@@ -24,9 +78,9 @@
|
||||
"code": 58834,
|
||||
"name": "menu"
|
||||
},
|
||||
"setIdx": 0,
|
||||
"setId": 2,
|
||||
"iconIdx": 489
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 1
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -53,7 +107,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 0
|
||||
"iconIdx": 2
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -80,7 +134,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 1
|
||||
"iconIdx": 3
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -107,7 +161,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 2
|
||||
"iconIdx": 4
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -134,7 +188,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 3
|
||||
"iconIdx": 5
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -154,14 +208,14 @@
|
||||
"properties": {
|
||||
"ligatures": "timer",
|
||||
"id": 760,
|
||||
"order": 916,
|
||||
"order": 928,
|
||||
"prevSize": 24,
|
||||
"code": 58405,
|
||||
"name": "timer"
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 4
|
||||
"iconIdx": 6
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -188,7 +242,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 5
|
||||
"iconIdx": 7
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -215,7 +269,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 6
|
||||
"iconIdx": 8
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -242,7 +296,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 7
|
||||
"iconIdx": 9
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -269,7 +323,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 8
|
||||
"iconIdx": 10
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -298,7 +352,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 9
|
||||
"iconIdx": 11
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -325,7 +379,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 10
|
||||
"iconIdx": 12
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -352,7 +406,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 11
|
||||
"iconIdx": 13
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -381,7 +435,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 12
|
||||
"iconIdx": 14
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -410,7 +464,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 13
|
||||
"iconIdx": 15
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -439,7 +493,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 14
|
||||
"iconIdx": 16
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -468,7 +522,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 15
|
||||
"iconIdx": 17
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -497,7 +551,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 16
|
||||
"iconIdx": 18
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -523,7 +577,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 17
|
||||
"iconIdx": 19
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -549,7 +603,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 18
|
||||
"iconIdx": 20
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -575,7 +629,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 19
|
||||
"iconIdx": 21
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -601,7 +655,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 20
|
||||
"iconIdx": 22
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -627,7 +681,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 21
|
||||
"iconIdx": 23
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -653,7 +707,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 22
|
||||
"iconIdx": 24
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -679,7 +733,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 23
|
||||
"iconIdx": 25
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -705,7 +759,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 24
|
||||
"iconIdx": 26
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -731,7 +785,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 25
|
||||
"iconIdx": 27
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -757,7 +811,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 26
|
||||
"iconIdx": 28
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -783,7 +837,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 27
|
||||
"iconIdx": 29
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -809,7 +863,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 28
|
||||
"iconIdx": 30
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -835,7 +889,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 29
|
||||
"iconIdx": 31
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -861,7 +915,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 30
|
||||
"iconIdx": 32
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -887,7 +941,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 31
|
||||
"iconIdx": 33
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -913,7 +967,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 32
|
||||
"iconIdx": 34
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -939,7 +993,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 33
|
||||
"iconIdx": 35
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -965,7 +1019,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 34
|
||||
"iconIdx": 36
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -991,7 +1045,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 35
|
||||
"iconIdx": 37
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1017,7 +1071,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 36
|
||||
"iconIdx": 38
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1043,7 +1097,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 37
|
||||
"iconIdx": 39
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1069,7 +1123,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 38
|
||||
"iconIdx": 40
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1095,7 +1149,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 39
|
||||
"iconIdx": 41
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1121,7 +1175,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 40
|
||||
"iconIdx": 42
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1147,7 +1201,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 41
|
||||
"iconIdx": 43
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1173,7 +1227,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 42
|
||||
"iconIdx": 44
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1199,7 +1253,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 43
|
||||
"iconIdx": 45
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1225,7 +1279,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 44
|
||||
"iconIdx": 46
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1251,7 +1305,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 45
|
||||
"iconIdx": 47
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1280,7 +1334,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 46
|
||||
"iconIdx": 48
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1310,7 +1364,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 47
|
||||
"iconIdx": 49
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1340,7 +1394,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 48
|
||||
"iconIdx": 50
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1366,7 +1420,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 49
|
||||
"iconIdx": 51
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1392,7 +1446,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 50
|
||||
"iconIdx": 52
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -1418,7 +1472,7 @@
|
||||
},
|
||||
"setIdx": 1,
|
||||
"setId": 1,
|
||||
"iconIdx": 51
|
||||
"iconIdx": 53
|
||||
}
|
||||
],
|
||||
"height": 1024,
|
||||
|
||||
82
react/features/base/i18n/dateUtil.js
Normal 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';
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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' />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -161,7 +161,6 @@ function _makePromiseAware(
|
||||
afterCallbacks: number) {
|
||||
return function(...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (args.length <= beforeCallbacks + afterCallbacks) {
|
||||
args.splice(beforeCallbacks, 0, resolve, reject);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
13
react/features/base/participants/sounds.native.js
Normal 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');
|
||||
11
react/features/base/participants/sounds.web.js
Normal 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';
|
||||
@@ -0,0 +1,295 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
SectionList,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import styles, { UNDERLAY_COLOR } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* Function to be invoked when an item is pressed. The item's URL is passed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* Function to be invoked when pull-to-refresh is performed.
|
||||
*/
|
||||
onRefresh: Function,
|
||||
|
||||
/**
|
||||
* Sections to be rendered in the following format:
|
||||
*
|
||||
* [
|
||||
* {
|
||||
* title: string, <- section title
|
||||
* key: string, <- unique key for the section
|
||||
* data: [ <- Array of items in the section
|
||||
* {
|
||||
* colorBase: string, <- the color base of the avatar
|
||||
* title: string, <- item title
|
||||
* url: string, <- item url
|
||||
* lines: Array<string> <- additional lines to be rendered
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ]
|
||||
*/
|
||||
sections: Array<Object>
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a general section list to display items that have a URL
|
||||
* property and navigates to (probably) meetings, such as the recent list
|
||||
* or the meeting list components.
|
||||
*/
|
||||
export default class NavigateSectionList extends Component<Props> {
|
||||
/**
|
||||
* Constructor of the NavigateSectionList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._getAvatarColor = this._getAvatarColor.bind(this);
|
||||
this._getItemKey = this._getItemKey.bind(this);
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this._renderItem = this._renderItem.bind(this);
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
this._renderItemLines = this._renderItemLines.bind(this);
|
||||
this._renderSection = this._renderSection.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's Component.render function.
|
||||
* Note: we don't use the refreshing value yet, because refreshing of these
|
||||
* lists is super quick, no need to complicate the code - yet.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { sections } = this.props;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style = { styles.container } >
|
||||
<SectionList
|
||||
keyExtractor = { this._getItemKey }
|
||||
onRefresh = { this._onRefresh }
|
||||
refreshing = { false }
|
||||
renderItem = { this._renderItem }
|
||||
renderSectionHeader = { this._renderSection }
|
||||
sections = { sections }
|
||||
style = { styles.list } />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty section object.
|
||||
*
|
||||
* @private
|
||||
* @param {string} title - The title of the section.
|
||||
* @param {string} key - The key of the section. It must be unique.
|
||||
* @returns {Object}
|
||||
*/
|
||||
static createSection(title, key) {
|
||||
return {
|
||||
data: [],
|
||||
key,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
_getAvatarColor: string => Object
|
||||
|
||||
/**
|
||||
* Returns a style (color) based on the string that determines the
|
||||
* color of the avatar.
|
||||
*
|
||||
* @param {string} colorBase - The string that is the base of the color.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getAvatarColor(colorBase) {
|
||||
if (!colorBase) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nameHash = 0;
|
||||
|
||||
for (let i = 0; i < colorBase.length; i++) {
|
||||
nameHash += colorBase.codePointAt(i);
|
||||
}
|
||||
|
||||
return styles[`avatarColor${(nameHash % 5) + 1}`];
|
||||
}
|
||||
|
||||
_getItemKey: (Object, number) => string;
|
||||
|
||||
/**
|
||||
* Generates a unique id to every item.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} item - The item.
|
||||
* @param {number} index - The item index.
|
||||
* @returns {string}
|
||||
*/
|
||||
_getItemKey(item, index) {
|
||||
return `${index}-${item.key}`;
|
||||
}
|
||||
|
||||
_onPress: string => Function
|
||||
|
||||
/**
|
||||
* Returns a function that is used in the onPress callback of the items.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The URL of the item to navigate to.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onPress(url) {
|
||||
return () => {
|
||||
const { disabled, onPress } = this.props;
|
||||
|
||||
!disabled && url && typeof onPress === 'function' && onPress(url);
|
||||
};
|
||||
}
|
||||
|
||||
_onRefresh: () => void
|
||||
|
||||
/**
|
||||
* Invokes the onRefresh callback if present.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
const { onRefresh } = this.props;
|
||||
|
||||
if (typeof onRefresh === 'function') {
|
||||
onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
_renderItem: Object => Object;
|
||||
|
||||
/**
|
||||
* Renders a single item in the list.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} listItem - The item to render.
|
||||
* @returns {Component}
|
||||
*/
|
||||
_renderItem(listItem) {
|
||||
const { item } = listItem;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress = { this._onPress(item.url) }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<View style = { styles.listItem }>
|
||||
<View style = { styles.avatarContainer } >
|
||||
<View
|
||||
style = { [
|
||||
styles.avatar,
|
||||
this._getAvatarColor(item.colorBase)
|
||||
] } >
|
||||
<Text style = { styles.avatarContent }>
|
||||
{ item.title.substr(0, 1).toUpperCase() }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { [
|
||||
styles.listItemText,
|
||||
styles.listItemTitle
|
||||
] }>
|
||||
{ item.title }
|
||||
</Text>
|
||||
{
|
||||
this._renderItemLines(item.lines)
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
* Renders a single line from the additional lines.
|
||||
*
|
||||
* @private
|
||||
* @param {string} line - The line text.
|
||||
* @param {number} index - The index of the line.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderItemLine(line, index) {
|
||||
if (!line) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
key = { index }
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.listItemText }>
|
||||
{ line }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
_renderItemLines: (Array<string>) => Array<React$Node>;
|
||||
|
||||
/**
|
||||
* Renders the additional item lines, if any.
|
||||
*
|
||||
* @private
|
||||
* @param {Array<string>} lines - The lines to render.
|
||||
* @returns {Array<React$Node>}
|
||||
*/
|
||||
_renderItemLines(lines) {
|
||||
if (lines && lines.length) {
|
||||
return lines.map((line, index) =>
|
||||
this._renderItemLine(line, index)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_renderSection: Object => Object
|
||||
|
||||
/**
|
||||
* Renders a section title.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} section - The section being rendered.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSection(section) {
|
||||
return (
|
||||
<View style = { styles.listSection }>
|
||||
<Text style = { styles.listSectionText }>
|
||||
{ section.section.title }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as Container } from './Container';
|
||||
export { default as Header } from './Header';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
export { default as Link } from './Link';
|
||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
||||
export { default as SideBar } from './SideBar';
|
||||
|
||||
@@ -4,20 +4,20 @@ import {
|
||||
createStyleSheet
|
||||
} from '../../../styles';
|
||||
|
||||
const AVATAR_OPACITY = 0.4;
|
||||
const AVATAR_SIZE = 65;
|
||||
const HEADER_COLOR = ColorPalette.blue;
|
||||
|
||||
// Header height is from iOS guidelines. Also, this looks good.
|
||||
const HEADER_HEIGHT = 44;
|
||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||
|
||||
export const HEADER_PADDING = BoxModel.padding;
|
||||
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
|
||||
export const SIDEBAR_WIDTH = 250;
|
||||
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
|
||||
|
||||
/**
|
||||
* The styles of the generic React {@code Components} of the app.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
const HEADER_STYLES = {
|
||||
/**
|
||||
* Platform specific header button (e.g. back, menu...etc).
|
||||
*/
|
||||
@@ -68,8 +68,124 @@ export default createStyleSheet({
|
||||
height: HEADER_HEIGHT,
|
||||
justifyContent: 'flex-start',
|
||||
padding: HEADER_PADDING
|
||||
}
|
||||
};
|
||||
|
||||
const SECTION_LIST_STYLES = {
|
||||
/**
|
||||
* The style of the actual avatar.
|
||||
*/
|
||||
avatar: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
|
||||
borderRadius: AVATAR_SIZE,
|
||||
height: AVATAR_SIZE,
|
||||
justifyContent: 'center',
|
||||
width: AVATAR_SIZE
|
||||
},
|
||||
|
||||
/**
|
||||
* List of styles of the avatar of a remote meeting (not the default
|
||||
* server). The number of colors are limited because they should match
|
||||
* nicely.
|
||||
*/
|
||||
avatarColor1: {
|
||||
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor2: {
|
||||
backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor3: {
|
||||
backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor4: {
|
||||
backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor5: {
|
||||
backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the avatar container that makes the avatar rounded.
|
||||
*/
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
padding: 5
|
||||
},
|
||||
|
||||
/**
|
||||
* Simple {@code Text} content of the avatar (the actual initials).
|
||||
*/
|
||||
avatarContent: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0)',
|
||||
color: OVERLAY_FONT_COLOR,
|
||||
fontSize: 32,
|
||||
fontWeight: '100',
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* The top level container style of the list.
|
||||
*/
|
||||
container: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
list: {
|
||||
flex: 1,
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
listItem: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 5
|
||||
},
|
||||
|
||||
listItemDetails: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: 5
|
||||
},
|
||||
|
||||
listItemText: {
|
||||
color: OVERLAY_FONT_COLOR,
|
||||
fontSize: 14
|
||||
},
|
||||
|
||||
listItemTitle: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 16
|
||||
},
|
||||
|
||||
listSection: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
padding: 5
|
||||
},
|
||||
|
||||
listSectionText: {
|
||||
color: OVERLAY_FONT_COLOR,
|
||||
fontSize: 14,
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
|
||||
touchableView: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
};
|
||||
|
||||
const SIDEBAR_STYLES = {
|
||||
/**
|
||||
* The topmost container of the side bar.
|
||||
*/
|
||||
@@ -105,4 +221,14 @@ export default createStyleSheet({
|
||||
sideMenuShadowTouchable: {
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of the generic components
|
||||
* in the app.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
...HEADER_STYLES,
|
||||
...SECTION_LIST_STYLES,
|
||||
...SIDEBAR_STYLES
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { MultiSelectStateless } from '@atlaskit/multi-select';
|
||||
import AKInlineDialog from '@atlaskit/inline-dialog';
|
||||
import Spinner from '@atlaskit/spinner';
|
||||
import _debounce from 'lodash/debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
@@ -28,11 +27,22 @@ class MultiSelectAutocomplete extends Component {
|
||||
*/
|
||||
isDisabled: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Text to display while a query is executing.
|
||||
*/
|
||||
loadingMessage: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The text to show when no matches are found.
|
||||
*/
|
||||
noMatchesFound: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The function called immediately before a selection has been actually
|
||||
* selected. Provides an opportunity to do any formatting.
|
||||
*/
|
||||
onItemSelected: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The function called when the selection changes.
|
||||
*/
|
||||
@@ -113,14 +123,14 @@ class MultiSelectAutocomplete extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the selected items.
|
||||
* Sets the items to display as selected.
|
||||
*
|
||||
* @param {Array<Object>} selectedItems - The list of items to display as
|
||||
* having been selected.
|
||||
* @returns {void}
|
||||
*/
|
||||
clear() {
|
||||
this.setState({
|
||||
selectedItems: []
|
||||
});
|
||||
setSelectedItems(selectedItems = []) {
|
||||
this.setState({ selectedItems });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,8 +150,10 @@ class MultiSelectAutocomplete extends Component {
|
||||
<MultiSelectStateless
|
||||
filterValue = { this.state.filterValue }
|
||||
isDisabled = { isDisabled }
|
||||
isLoading = { this.state.loading }
|
||||
isOpen = { this.state.isOpen }
|
||||
items = { this.state.items }
|
||||
loadingMessage = { this.props.loadingMessage }
|
||||
noMatchesFound = { noMatchesFound }
|
||||
onFilterChange = { this._onFilterChange }
|
||||
onRemoved = { this._onSelectionChange }
|
||||
@@ -150,7 +162,6 @@ class MultiSelectAutocomplete extends Component {
|
||||
selectedItems = { this.state.selectedItems }
|
||||
shouldFitContainer = { shouldFitContainer }
|
||||
shouldFocus = { shouldFocus } />
|
||||
{ this._renderLoadingIndicator() }
|
||||
{ this._renderError() }
|
||||
</div>
|
||||
);
|
||||
@@ -169,7 +180,8 @@ class MultiSelectAutocomplete extends Component {
|
||||
error: this.state.error && Boolean(filterValue),
|
||||
filterValue,
|
||||
isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
|
||||
items: filterValue ? this.state.items : []
|
||||
items: filterValue ? this.state.items : [],
|
||||
loading: Boolean(filterValue)
|
||||
});
|
||||
if (filterValue) {
|
||||
this._sendQuery(filterValue);
|
||||
@@ -201,7 +213,7 @@ class MultiSelectAutocomplete extends Component {
|
||||
if (existing) {
|
||||
selectedItems = selectedItems.filter(k => k !== existing);
|
||||
} else {
|
||||
selectedItems.push(item);
|
||||
selectedItems.push(this.props.onItemSelected(item));
|
||||
}
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
@@ -236,33 +248,6 @@ class MultiSelectAutocomplete extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the loading indicator.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
_renderLoadingIndicator() {
|
||||
if (!(this.state.loading
|
||||
&& !this.state.items.length
|
||||
&& this.state.filterValue.length)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = ( // eslint-disable-line no-extra-parens
|
||||
<div className = 'autocomplete-loading'>
|
||||
<Spinner
|
||||
isCompleting = { false }
|
||||
size = 'medium' />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<AKInlineDialog
|
||||
content = { content }
|
||||
isOpen = { true } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a query to the resourceClient.
|
||||
*
|
||||
@@ -275,7 +260,6 @@ class MultiSelectAutocomplete extends Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
error: false
|
||||
});
|
||||
|
||||
@@ -288,7 +272,6 @@ class MultiSelectAutocomplete extends Component {
|
||||
.then(results => {
|
||||
if (this.state.filterValue !== filterValue) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: false
|
||||
});
|
||||
|
||||
|
||||
54
react/features/base/sounds/actionTypes.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* The type of a feature/internal/protected (redux) action to add an audio
|
||||
* element to the sounds collection state.
|
||||
*
|
||||
* {
|
||||
* type: _ADD_AUDIO_ELEMENT,
|
||||
* ref: AudioElement,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const _ADD_AUDIO_ELEMENT = Symbol('_ADD_AUDIO_ELEMENT');
|
||||
|
||||
/**
|
||||
* The type of feature/internal/protected (redux) action to remove an audio
|
||||
* element for given sound identifier from the sounds collection state.
|
||||
*
|
||||
* {
|
||||
* type: _REMOVE_AUDIO_ELEMENT,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const _REMOVE_AUDIO_ELEMENT = Symbol('_REMOVE_AUDIO_ELEMENT');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to play a sound from the sounds collection.
|
||||
*
|
||||
* {
|
||||
* type: PLAY_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const PLAY_SOUND = Symbol('PLAY_SOUND');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to register a new sound with the sounds
|
||||
* collection.
|
||||
*
|
||||
* {
|
||||
* type: REGISTER_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const REGISTER_SOUND = Symbol('REGISTER_SOUND');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to unregister an existing sound from the sounds
|
||||
* collection.
|
||||
*
|
||||
* {
|
||||
* type: UNREGISTER_SOUND,
|
||||
* soundId: string
|
||||
* }
|
||||
*/
|
||||
export const UNREGISTER_SOUND = Symbol('UNREGISTER_SOUND');
|
||||
118
react/features/base/sounds/actions.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// @flow
|
||||
|
||||
import type { AudioElement } from '../media';
|
||||
|
||||
import {
|
||||
_ADD_AUDIO_ELEMENT,
|
||||
_REMOVE_AUDIO_ELEMENT,
|
||||
PLAY_SOUND,
|
||||
REGISTER_SOUND,
|
||||
UNREGISTER_SOUND
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Adds {@link AudioElement} instance to the base/sounds feature state for the
|
||||
* {@link Sound} instance identified by the given id. After this action the
|
||||
* sound can be played by dispatching the {@link PLAY_SOUND} action.
|
||||
*
|
||||
* @param {string} soundId - The sound identifier for which the audio element
|
||||
* will be stored.
|
||||
* @param {AudioElement} audioElement - The audio element which implements the
|
||||
* audio playback functionality and which is backed by the sound resource
|
||||
* corresponding to the {@link Sound} with the given id.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: PLAY_SOUND,
|
||||
* audioElement: AudioElement,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function _addAudioElement(soundId: string, audioElement: AudioElement) {
|
||||
return {
|
||||
type: _ADD_AUDIO_ELEMENT,
|
||||
audioElement,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The opposite of {@link _addAudioElement} which removes {@link AudioElement}
|
||||
* for given sound from base/sounds state. It means that the audio resource has
|
||||
* been disposed and the sound can no longer be played.
|
||||
*
|
||||
* @param {string} soundId - The {@link Sound} instance identifier for which the
|
||||
* audio element is being removed.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* type: _REMOVE_AUDIO_ELEMENT,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function _removeAudioElement(soundId: string) {
|
||||
return {
|
||||
type: _REMOVE_AUDIO_ELEMENT,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts playback of the sound identified by the given sound id. The action
|
||||
* will have effect only if the audio resource has been loaded already.
|
||||
*
|
||||
* @param {string} soundId - The id of the sound to be played (the same one
|
||||
* which was used in {@link registerSound} to register the sound).
|
||||
* @returns {{
|
||||
* type: PLAY_SOUND,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function playSound(soundId: string): Object {
|
||||
return {
|
||||
type: PLAY_SOUND,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new sound for given id and a source object which can be either a
|
||||
* path or a raw object depending on the platform (native vs web). It will make
|
||||
* the {@link SoundCollection} render extra HTMLAudioElement which will make it
|
||||
* available for playback through the {@link playSound} action.
|
||||
*
|
||||
* @param {string} soundId - The global identifier which identify the sound
|
||||
* created for given source object.
|
||||
* @param {Object|string} src - Either path to an audio file or a raw object
|
||||
* which specifies the audio resource that will be associated with the given
|
||||
* {@code soundId}.
|
||||
* @returns {{
|
||||
* type: REGISTER_SOUND,
|
||||
* soundId: string,
|
||||
* src: (Object | string)
|
||||
* }}
|
||||
*/
|
||||
export function registerSound(soundId: string, src: Object | string): Object {
|
||||
return {
|
||||
type: REGISTER_SOUND,
|
||||
soundId,
|
||||
src
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the sound identified by the given id. It will make the
|
||||
* {@link SoundCollection} component stop rendering the corresponding
|
||||
* {@code HTMLAudioElement} which then should result in the audio resource
|
||||
* disposal.
|
||||
*
|
||||
* @param {string} soundId - The identifier of the {@link Sound} to be removed.
|
||||
* @returns {{
|
||||
* type: UNREGISTER_SOUND,
|
||||
* soundId: string
|
||||
* }}
|
||||
*/
|
||||
export function unregisterSound(soundId: string): Object {
|
||||
return {
|
||||
type: UNREGISTER_SOUND,
|
||||
soundId
|
||||
};
|
||||
}
|
||||
160
react/features/base/sounds/components/SoundCollection.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Audio } from '../../media';
|
||||
import type { AudioElement } from '../../media';
|
||||
import { Fragment } from '../../react';
|
||||
|
||||
import { _addAudioElement, _removeAudioElement } from '../actions';
|
||||
import type { Sound } from '../reducer';
|
||||
|
||||
/**
|
||||
* {@link SoundCollection}'s properties.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Dispatches {@link _ADD_AUDIO_ELEMENT} Redux action which will store the
|
||||
* {@link AudioElement} for a sound in the Redux store.
|
||||
*/
|
||||
_addAudioElement: Function,
|
||||
|
||||
/**
|
||||
* Dispatches {@link _REMOVE_AUDIO_ELEMENT} Redux action which will remove
|
||||
* the sound's {@link AudioElement} from the Redux store.
|
||||
*/
|
||||
_removeAudioElement: Function,
|
||||
|
||||
/**
|
||||
* It's the 'base/sounds' reducer's state mapped to a property. It's used to
|
||||
* render audio elements for every registered sound.
|
||||
*/
|
||||
_sounds: Map<string, Sound>
|
||||
}
|
||||
|
||||
/**
|
||||
* Collections of all global sounds used by the app for playing audio
|
||||
* notifications in response to various events. It renders <code>Audio</code>
|
||||
* element for each sound registered in the base/sounds feature. When the audio
|
||||
* resource is loaded it will emit add/remove audio element actions which will
|
||||
* attach the element to the corresponding {@link Sound} instance in the Redux
|
||||
* state. When that happens the sound can be played using the {@link playSound}
|
||||
* action.
|
||||
*/
|
||||
class SoundCollection extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
let key = 0;
|
||||
const sounds = [];
|
||||
|
||||
for (const [ soundId, sound ] of this.props._sounds.entries()) {
|
||||
sounds.push(
|
||||
React.createElement(
|
||||
Audio, {
|
||||
key,
|
||||
setRef: this._setRef.bind(this, soundId),
|
||||
src: sound.src
|
||||
}));
|
||||
key += 1;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
sounds
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the (reference to the) {@link AudioElement} object which implements
|
||||
* the audio playback functionality.
|
||||
*
|
||||
* @param {string} soundId - The sound Id for the audio element for which
|
||||
* the callback is being executed.
|
||||
* @param {AudioElement} element - The {@link AudioElement} instance
|
||||
* which implements the audio playback functionality.
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_setRef(soundId: string, element: ?AudioElement) {
|
||||
if (element) {
|
||||
this.props._addAudioElement(soundId, element);
|
||||
} else {
|
||||
this.props._removeAudioElement(soundId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to {@code SoundCollection}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _sounds: Map<string, Sound>
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_sounds: state['features/base/sounds']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dispatching of some actions to React component props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _addAudioElement: void,
|
||||
* _removeAudioElement: void
|
||||
* }}
|
||||
*/
|
||||
export function _mapDispatchToProps(dispatch: Function) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches action to store the {@link AudioElement} for
|
||||
* a {@link Sound} identified by given <tt>soundId</tt> in the Redux
|
||||
* store, so that the playback can be controlled through the Redux
|
||||
* actions.
|
||||
*
|
||||
* @param {string} soundId - A global identifier which will be used to
|
||||
* identify the {@link Sound} instance for which an audio element will
|
||||
* be added.
|
||||
* @param {AudioElement} audioElement - The {@link AudioElement}
|
||||
* instance that will be stored in the Redux state of the base/sounds
|
||||
* feature, as part of the {@link Sound} object. At that point the sound
|
||||
* will be ready for playback.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_addAudioElement(soundId: string, audioElement: AudioElement) {
|
||||
dispatch(_addAudioElement(soundId, audioElement));
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches action to remove {@link AudioElement} from the Redux
|
||||
* store for specific {@link Sound}, because it is no longer part of
|
||||
* the DOM tree and the audio resource will be released.
|
||||
*
|
||||
* @param {string} soundId - The id of the {@link Sound} instance for
|
||||
* which an {@link AudioElement} will be removed from the Redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_removeAudioElement(soundId: string) {
|
||||
dispatch(_removeAudioElement(soundId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(SoundCollection);
|
||||
1
react/features/base/sounds/components/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as SoundCollection } from './SoundCollection';
|
||||
6
react/features/base/sounds/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
46
react/features/base/sounds/middleware.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
import { PLAY_SOUND } from './actionTypes';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Implements the entry point of the middleware of the feature base/media.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case PLAY_SOUND:
|
||||
_playSound(store, action.soundId);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Plays sound from audio element registered in the Redux store.
|
||||
*
|
||||
* @param {Store} store - The Redux store instance.
|
||||
* @param {string} soundId - Audio element identifier.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _playSound({ getState }, soundId) {
|
||||
const sounds = getState()['features/base/sounds'];
|
||||
const sound = sounds.get(soundId);
|
||||
|
||||
if (sound) {
|
||||
if (sound.audioElement) {
|
||||
sound.audioElement.play();
|
||||
} else {
|
||||
logger.warn(`PLAY_SOUND: sound not loaded yet for id: ${soundId}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`PLAY_SOUND: no sound found for id: ${soundId}`);
|
||||
}
|
||||
}
|
||||
140
react/features/base/sounds/reducer.js
Normal file
@@ -0,0 +1,140 @@
|
||||
// @flow
|
||||
|
||||
import type { AudioElement } from '../media';
|
||||
import { assign, ReducerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
_ADD_AUDIO_ELEMENT,
|
||||
_REMOVE_AUDIO_ELEMENT,
|
||||
REGISTER_SOUND,
|
||||
UNREGISTER_SOUND
|
||||
} from './actionTypes';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The structure use by this reducer to describe a sound.
|
||||
*/
|
||||
export type Sound = {
|
||||
|
||||
/**
|
||||
* The HTMLAudioElement which implements the audio playback functionality.
|
||||
* Becomes available once the sound resource gets loaded and the sound can
|
||||
* not be played until that happens.
|
||||
*/
|
||||
audioElement?: AudioElement,
|
||||
|
||||
/**
|
||||
* This field describes the source of the audio resource to be played. It
|
||||
* can be either a path to the file or an object depending on the platform
|
||||
* (native vs web).
|
||||
*/
|
||||
src: Object | string
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial/default state of the feature {@code base/sounds}. It is a {@code Map}
|
||||
* of globally stored sounds.
|
||||
*
|
||||
* @type {Map<string, Sound>}
|
||||
*/
|
||||
const DEFAULT_STATE = new Map();
|
||||
|
||||
/**
|
||||
* The base/sounds feature's reducer.
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/base/sounds',
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case _ADD_AUDIO_ELEMENT:
|
||||
case _REMOVE_AUDIO_ELEMENT:
|
||||
return _addOrRemoveAudioElement(state, action);
|
||||
|
||||
case REGISTER_SOUND:
|
||||
return _registerSound(state, action);
|
||||
|
||||
case UNREGISTER_SOUND:
|
||||
return _unregisterSound(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds or removes {@link AudioElement} associated with a {@link Sound}.
|
||||
*
|
||||
* @param {Map<string, Sound>} state - The current Redux state of this feature.
|
||||
* @param {_ADD_AUDIO_ELEMENT | _REMOVE_AUDIO_ELEMENT} action - The action to be
|
||||
* handled.
|
||||
* @private
|
||||
* @returns {Map<string, Sound>}
|
||||
*/
|
||||
function _addOrRemoveAudioElement(state, action) {
|
||||
const isAddAction = action.type === _ADD_AUDIO_ELEMENT;
|
||||
const nextState = new Map(state);
|
||||
const { soundId } = action;
|
||||
|
||||
const sound = nextState.get(soundId);
|
||||
|
||||
if (sound) {
|
||||
if (isAddAction) {
|
||||
nextState.set(soundId,
|
||||
assign(sound, {
|
||||
audioElement: action.audioElement
|
||||
}));
|
||||
} else {
|
||||
nextState.set(soundId,
|
||||
assign(sound, {
|
||||
audioElement: undefined
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
const actionName
|
||||
= isAddAction ? '_ADD_AUDIO_ELEMENT' : '_REMOVE_AUDIO_ELEMENT';
|
||||
|
||||
logger.error(`${actionName}: no sound for id: ${soundId}`);
|
||||
}
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new {@link Sound} for given id and source. It will make
|
||||
* the {@link SoundCollection} component render HTMLAudioElement for given
|
||||
* source making it available for playback through the redux actions.
|
||||
*
|
||||
* @param {Map<string, Sound>} state - The current Redux state of the sounds
|
||||
* features.
|
||||
* @param {REGISTER_SOUND} action - The register sound action.
|
||||
* @private
|
||||
* @returns {Map<string, Sound>}
|
||||
*/
|
||||
function _registerSound(state, action) {
|
||||
const nextState = new Map(state);
|
||||
|
||||
nextState.set(action.soundId, {
|
||||
src: action.src
|
||||
});
|
||||
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a {@link Sound} which will make the {@link SoundCollection}
|
||||
* component stop rendering the corresponding HTMLAudioElement. This will
|
||||
* result further in the audio resource disposal.
|
||||
*
|
||||
* @param {Map<string, Sound>} state - The current Redux state of this feature.
|
||||
* @param {UNREGISTER_SOUND} action - The unregister sound action.
|
||||
* @private
|
||||
* @returns {Map<string, Sound>}
|
||||
*/
|
||||
function _unregisterSound(state, action) {
|
||||
const nextState = new Map(state);
|
||||
|
||||
nextState.delete(action.soundId);
|
||||
|
||||
return nextState;
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The app linking scheme.
|
||||
* TODO: This should be read from the manifest files later.
|
||||
*/
|
||||
export const APP_LINK_SCHEME = 'org.jitsi.meet:';
|
||||
|
||||
/**
|
||||
* The {@link RegExp} pattern of the authority of a URI.
|
||||
*
|
||||
@@ -19,10 +25,14 @@ const _URI_PATH_PATTERN = '([^?#]*)';
|
||||
/**
|
||||
* The {@link RegExp} pattern of the protocol of a URI.
|
||||
*
|
||||
* @private
|
||||
* FIXME: The URL class exposed by JavaScript will not include the colon in
|
||||
* the protocol field. Also in other places (at the time of this writing:
|
||||
* the UnsupportedMobileBrowser.js) the APP_LINK_SCHEME does not include
|
||||
* the double dots, so things are inconsistent.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
|
||||
export const URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
|
||||
|
||||
/**
|
||||
* Fixes the hier-part of a specific URI (string) so that the URI is well-known.
|
||||
@@ -41,7 +51,7 @@ function _fixURIStringHierPart(uri) {
|
||||
// hipchat.com
|
||||
let regex
|
||||
= new RegExp(
|
||||
`^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
|
||||
`^${URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
|
||||
'gi');
|
||||
let match: Array<string> | null = regex.exec(uri);
|
||||
|
||||
@@ -49,7 +59,7 @@ function _fixURIStringHierPart(uri) {
|
||||
// enso.me
|
||||
regex
|
||||
= new RegExp(
|
||||
`^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
|
||||
`^${URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
|
||||
'gi');
|
||||
match = regex.exec(uri);
|
||||
}
|
||||
@@ -81,7 +91,7 @@ function _fixURIStringHierPart(uri) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function _fixURIStringScheme(uri: string) {
|
||||
const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi');
|
||||
const regex = new RegExp(`^${URI_PROTOCOL_PATTERN}+`, 'gi');
|
||||
const match: Array<string> | null = regex.exec(uri);
|
||||
|
||||
if (match) {
|
||||
@@ -185,7 +195,7 @@ export function parseStandardURIString(str: string) {
|
||||
str = str.replace(/\s/g, '');
|
||||
|
||||
// protocol
|
||||
regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi');
|
||||
regex = new RegExp(`^${URI_PROTOCOL_PATTERN}`, 'gi');
|
||||
match = regex.exec(str);
|
||||
if (match) {
|
||||
obj.protocol = match[1].toLowerCase();
|
||||
@@ -327,12 +337,12 @@ function _standardURIToString(thiz: ?Object) {
|
||||
* the one accepted by the constructor of Web's ExternalAPI is supported on both
|
||||
* mobile/React Native and Web/React.
|
||||
*
|
||||
* @param {string|Object} obj - The URL to return a {@code String}
|
||||
* @param {Object|string} obj - The URL to return a {@code String}
|
||||
* representation of.
|
||||
* @returns {string} - A {@code String} representation of the specified
|
||||
* {@code obj} which is supposed to represent a URL.
|
||||
*/
|
||||
export function toURLString(obj: ?(string | Object)): ?string {
|
||||
export function toURLString(obj: ?(Object | string)): ?string {
|
||||
let str;
|
||||
|
||||
switch (typeof obj) {
|
||||
@@ -395,7 +405,7 @@ export function urlObjectToString(o: Object): ?string {
|
||||
// XXX The value of domain in supposed to be host/hostname
|
||||
// and, optionally, pathname. Make sure it is not taken for
|
||||
// a pathname only.
|
||||
_fixURIStringScheme(`org.jitsi.meet://${domain}`));
|
||||
_fixURIStringScheme(`${APP_LINK_SCHEME}//${domain}`));
|
||||
|
||||
// authority
|
||||
if (host) {
|
||||
|
||||
17
react/features/calendar-sync/actionTypes.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Action to update the current calendar entry list in the store.
|
||||
*/
|
||||
export const NEW_CALENDAR_ENTRY_LIST = Symbol('NEW_CALENDAR_ENTRY_LIST');
|
||||
|
||||
/**
|
||||
* Action to add a new known domain to the list.
|
||||
*/
|
||||
export const NEW_KNOWN_DOMAIN = Symbol('NEW_KNOWN_DOMAIN');
|
||||
|
||||
/**
|
||||
* Action to refresh (re-fetch) the entry list.
|
||||
*/
|
||||
export const REFRESH_CALENDAR_ENTRY_LIST
|
||||
= Symbol('REFRESH_CALENDAR_ENTRY_LIST');
|
||||
51
react/features/calendar-sync/actions.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
import {
|
||||
NEW_CALENDAR_ENTRY_LIST,
|
||||
NEW_KNOWN_DOMAIN,
|
||||
REFRESH_CALENDAR_ENTRY_LIST
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sends an action to add a new known domain if not present yet.
|
||||
*
|
||||
* @param {string} domainName - The new domain.
|
||||
* @returns {{
|
||||
* type: NEW_KNOWN_DOMAIN,
|
||||
* domainName: string
|
||||
* }}
|
||||
*/
|
||||
export function maybeAddNewKnownDomain(domainName: string) {
|
||||
return {
|
||||
type: NEW_KNOWN_DOMAIN,
|
||||
domainName
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an action to refresh the entry list (fetches new data).
|
||||
*
|
||||
* @returns {{
|
||||
* type: REFRESH_CALENDAR_ENTRY_LIST
|
||||
* }}
|
||||
*/
|
||||
export function refreshCalendarEntryList() {
|
||||
return {
|
||||
type: REFRESH_CALENDAR_ENTRY_LIST
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an action to update the current calendar list in redux.
|
||||
*
|
||||
* @param {Array<Object>} events - The new list.
|
||||
* @returns {{
|
||||
* type: NEW_CALENDAR_ENTRY_LIST,
|
||||
* events: Array<Object>
|
||||
* }}
|
||||
*/
|
||||
export function updateCalendarEntryList(events: Array<Object>) {
|
||||
return {
|
||||
type: NEW_CALENDAR_ENTRY_LIST,
|
||||
events
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { appNavigate } from '../../app';
|
||||
import { getURLWithoutParams } from '../../base/connection';
|
||||
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
||||
import { Icon } from '../../base/font-icons';
|
||||
import { ASPECT_RATIO_NARROW } from '../../base/responsive-ui';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const ALERT_MILLISECONDS = 5 * 60 * 1000;
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The current aspect ratio of the screen.
|
||||
*/
|
||||
_aspectRatio: Symbol,
|
||||
|
||||
/**
|
||||
* The URL of the current conference without params.
|
||||
*/
|
||||
_currentConferenceURL: string,
|
||||
|
||||
/**
|
||||
* The calendar event list.
|
||||
*/
|
||||
_eventList: Array<Object>,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The event object to display the notification for.
|
||||
*/
|
||||
event?: Object
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display a permanent badge-like notification on the
|
||||
* conference screen when another meeting is about to start.
|
||||
*/
|
||||
class ConferenceNotification extends Component<Props, State> {
|
||||
updateIntervalId: number;
|
||||
|
||||
/**
|
||||
* Constructor of the ConferenceNotification component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
event: undefined
|
||||
};
|
||||
|
||||
this._getNotificationContentStyle
|
||||
= this._getNotificationContentStyle.bind(this);
|
||||
this._getNotificationPosition
|
||||
= this._getNotificationPosition.bind(this);
|
||||
this._maybeDisplayNotification
|
||||
= this._maybeDisplayNotification.bind(this);
|
||||
this._onGoToNext = this._onGoToNext.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentDidMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.updateIntervalId = setInterval(
|
||||
this._maybeDisplayNotification,
|
||||
10 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillUnmount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.updateIntervalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the React Components's render method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { event } = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
if (event) {
|
||||
return (
|
||||
<View
|
||||
style = { [
|
||||
styles.notificationContainer,
|
||||
this._getNotificationPosition()
|
||||
] } >
|
||||
<View
|
||||
style = { this._getNotificationContentStyle() }>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onGoToNext } >
|
||||
<View style = { styles.touchableView }>
|
||||
<View
|
||||
style = {
|
||||
styles.notificationTextContainer
|
||||
}>
|
||||
<Text style = { styles.notificationText }>
|
||||
{ t('calendarSync.nextMeeting') }
|
||||
</Text>
|
||||
<Text style = { styles.notificationText }>
|
||||
{
|
||||
getLocalizedDateFormatter(
|
||||
event.startDate
|
||||
).fromNow()
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style = {
|
||||
styles.notificationIconContainer
|
||||
}>
|
||||
<Icon
|
||||
name = 'navigate_next'
|
||||
style = { styles.notificationIcon } />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_getNotificationContentStyle: () => Array<Object>
|
||||
|
||||
/**
|
||||
* Decides the color of the notification and some additional
|
||||
* styles based on notificationPosition.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_getNotificationContentStyle() {
|
||||
const { event } = this.state;
|
||||
const { _aspectRatio } = this.props;
|
||||
const now = Date.now();
|
||||
const style = [
|
||||
styles.notificationContent
|
||||
];
|
||||
|
||||
if (event && event.startDate < now && event.endDate > now) {
|
||||
style.push(styles.notificationContentPast);
|
||||
} else {
|
||||
style.push(styles.notificationContentNext);
|
||||
}
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
style.push(styles.notificationContentSide);
|
||||
} else {
|
||||
style.push(styles.notificationContentTop);
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
_getNotificationPosition: () => Object;
|
||||
|
||||
/**
|
||||
* Decides the position of the notification.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getNotificationPosition() {
|
||||
const { _aspectRatio } = this.props;
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
return styles.notificationContainerSide;
|
||||
}
|
||||
|
||||
return styles.notificationContainerTop;
|
||||
}
|
||||
|
||||
_maybeDisplayNotification: () => void;
|
||||
|
||||
/**
|
||||
* Periodically checks if there is an event in the calendar for which we
|
||||
* need to show a notification.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_maybeDisplayNotification() {
|
||||
const { _currentConferenceURL, _eventList } = this.props;
|
||||
let eventToShow;
|
||||
|
||||
if (_eventList && _eventList.length) {
|
||||
const now = Date.now();
|
||||
|
||||
for (const event of _eventList) {
|
||||
if (event.url !== _currentConferenceURL) {
|
||||
if ((!eventToShow
|
||||
&& event.startDate > now
|
||||
&& event.startDate < now + ALERT_MILLISECONDS)
|
||||
|| (event.startDate < now && event.endDate > now)
|
||||
) {
|
||||
eventToShow = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
event: eventToShow
|
||||
});
|
||||
}
|
||||
|
||||
_onGoToNext: () => void;
|
||||
|
||||
/**
|
||||
* Opens the meeting URL that the notification shows.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The URL to open.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGoToNext() {
|
||||
const { event } = this.state;
|
||||
|
||||
if (event && event.url) {
|
||||
this.props.dispatch(appNavigate(event.url));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _aspectRatio: Symbol,
|
||||
* _currentConferenceURL: string,
|
||||
* _eventList: Array
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
return {
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_currentConferenceURL:
|
||||
locationURL ? getURLWithoutParams(locationURL)._url : '',
|
||||
_eventList: state['features/calendar-sync'].events
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(ConferenceNotification));
|
||||
245
react/features/calendar-sync/components/MeetingList.native.js
Normal file
@@ -0,0 +1,245 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { refreshCalendarEntryList } from '../actions';
|
||||
|
||||
import { appNavigate } from '../../app';
|
||||
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
|
||||
import { NavigateSectionList } from '../../base/react';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates if the list is disabled or not.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Tells the component if it's being displayed at the moment, or not.
|
||||
* Note: as an example, on Android it can happen that the component
|
||||
* is rendered but not displayed, because components like ViewPagerAndroid
|
||||
* render their children even if they are not visible at the moment.
|
||||
*/
|
||||
displayed: boolean,
|
||||
|
||||
/**
|
||||
* The calendar event list.
|
||||
*/
|
||||
_eventList: Array<Object>,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display a list of events from the (mobile) user's calendar.
|
||||
*/
|
||||
class MeetingList extends Component<Props> {
|
||||
_initialLoaded: boolean
|
||||
|
||||
/**
|
||||
* Default values for the component's props.
|
||||
*/
|
||||
static defaultProps = {
|
||||
_eventList: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor of the MeetingList component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onPress = this._onPress.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this._toDisplayableItem = this._toDisplayableItem.bind(this);
|
||||
this._toDisplayableList = this._toDisplayableList.bind(this);
|
||||
this._toDateString = this._toDateString.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillReceiveProps function.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillReceiveProps(newProps) {
|
||||
// This is a conditional logic to refresh the calendar entries (thus
|
||||
// to request access to calendar) on component first receives a
|
||||
// displayed=true prop - to avoid requesting calendar access on
|
||||
// app start.
|
||||
if (!this._initialLoaded
|
||||
&& newProps.displayed
|
||||
&& !this.props.displayed) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
this._initialLoaded = true;
|
||||
dispatch(refreshCalendarEntryList());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the React Components's render method.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { disabled } = this.props;
|
||||
|
||||
return (
|
||||
<NavigateSectionList
|
||||
disabled = { disabled }
|
||||
onPress = { this._onPress }
|
||||
onRefresh = { this._onRefresh }
|
||||
sections = { this._toDisplayableList() } />
|
||||
);
|
||||
}
|
||||
|
||||
_onPress: string => Function
|
||||
|
||||
/**
|
||||
* Handles the list's navigate action.
|
||||
*
|
||||
* @private
|
||||
* @param {string} url - The url string to navigate to.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress(url) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(appNavigate(url));
|
||||
}
|
||||
|
||||
_onRefresh: () => void
|
||||
|
||||
/**
|
||||
* Callback to execute when the list is doing a pull-to-refresh.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRefresh() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(refreshCalendarEntryList());
|
||||
}
|
||||
|
||||
_toDisplayableItem: Object => Object
|
||||
|
||||
/**
|
||||
* Creates a displayable object from an event.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event - The calendar event.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_toDisplayableItem(event) {
|
||||
return {
|
||||
key: `${event.id}-${event.startDate}`,
|
||||
lines: [
|
||||
event.url,
|
||||
this._toDateString(event)
|
||||
],
|
||||
title: event.title,
|
||||
url: event.url
|
||||
};
|
||||
}
|
||||
|
||||
_toDisplayableList: () => Array<Object>
|
||||
|
||||
/**
|
||||
* Transforms the event list to a displayable list
|
||||
* with sections.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_toDisplayableList() {
|
||||
const { _eventList, t } = this.props;
|
||||
const now = Date.now();
|
||||
const nowSection = NavigateSectionList.createSection(
|
||||
t('calendarSync.now'),
|
||||
'now'
|
||||
);
|
||||
const nextSection = NavigateSectionList.createSection(
|
||||
t('calendarSync.next'),
|
||||
'next'
|
||||
);
|
||||
const laterSection = NavigateSectionList.createSection(
|
||||
t('calendarSync.later'),
|
||||
'later'
|
||||
);
|
||||
|
||||
for (const event of _eventList) {
|
||||
const displayableEvent = this._toDisplayableItem(event);
|
||||
|
||||
if (event.startDate < now && event.endDate > now) {
|
||||
nowSection.data.push(displayableEvent);
|
||||
} else if (event.startDate > now) {
|
||||
if (nextSection.data.length
|
||||
&& nextSection.data[0].startDate !== event.startDate) {
|
||||
laterSection.data.push(displayableEvent);
|
||||
} else {
|
||||
nextSection.data.push(displayableEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sectionList = [];
|
||||
|
||||
for (const section of [
|
||||
nowSection,
|
||||
nextSection,
|
||||
laterSection
|
||||
]) {
|
||||
if (section.data.length) {
|
||||
sectionList.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
return sectionList;
|
||||
}
|
||||
|
||||
_toDateString: Object => string
|
||||
|
||||
/**
|
||||
* Generates a date (interval) string for a given event.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event - The event.
|
||||
* @returns {string}
|
||||
*/
|
||||
_toDateString(event) {
|
||||
/* eslint-disable max-len */
|
||||
const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
|
||||
const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
|
||||
|
||||
return `${startDateTime} - ${endTime}`;
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {{
|
||||
* _eventList: Array
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_eventList: state['features/calendar-sync'].events
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(MeetingList));
|
||||
2
react/features/calendar-sync/components/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as MeetingList } from './MeetingList';
|
||||
export { default as ConferenceNotification } from './ConferenceNotification';
|
||||
126
react/features/calendar-sync/components/styles.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import { createStyleSheet } from '../../base/styles';
|
||||
|
||||
const NOTIFICATION_SIZE = 55;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Component}s of the feature meeting-list i.e.
|
||||
* {@code MeetingList}.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* The top level container of the notification.
|
||||
*/
|
||||
notificationContainer: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
position: 'absolute'
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the container when the notification is displayed
|
||||
* on the side (narrow view).
|
||||
*/
|
||||
notificationContainerSide: {
|
||||
top: 100
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the container when the notification is displayed
|
||||
* on the top (wide view).
|
||||
*/
|
||||
notificationContainerTop: {
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The top level container of the notification.
|
||||
*/
|
||||
notificationContent: {
|
||||
alignSelf: 'flex-start',
|
||||
flexDirection: 'row',
|
||||
height: NOTIFICATION_SIZE,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 10
|
||||
},
|
||||
|
||||
/**
|
||||
* Color for upcoming meeting notification.
|
||||
*/
|
||||
notificationContentNext: {
|
||||
backgroundColor: '#eeb231'
|
||||
},
|
||||
|
||||
/**
|
||||
* Color for already ongoing meeting notifications.
|
||||
*/
|
||||
notificationContentPast: {
|
||||
backgroundColor: 'red'
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the content when the notification is displayed
|
||||
* on the side (narrow view).
|
||||
*/
|
||||
notificationContentSide: {
|
||||
borderBottomRightRadius: NOTIFICATION_SIZE,
|
||||
borderTopRightRadius: NOTIFICATION_SIZE
|
||||
},
|
||||
|
||||
/**
|
||||
* Additional style for the content when the notification is displayed
|
||||
* on the top (wide view).
|
||||
*/
|
||||
notificationContentTop: {
|
||||
borderBottomLeftRadius: NOTIFICATION_SIZE / 2,
|
||||
borderBottomRightRadius: NOTIFICATION_SIZE / 2,
|
||||
paddingHorizontal: 20
|
||||
},
|
||||
|
||||
/**
|
||||
* The icon of the notification.
|
||||
*/
|
||||
notificationIcon: {
|
||||
color: 'white',
|
||||
fontSize: 25
|
||||
},
|
||||
|
||||
/**
|
||||
* The container that contains the icon.
|
||||
*/
|
||||
notificationIconContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: NOTIFICATION_SIZE,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* A single line of text of the notification.
|
||||
*/
|
||||
notificationText: {
|
||||
color: 'white',
|
||||
fontSize: 13
|
||||
},
|
||||
|
||||
/**
|
||||
* The container for all the lines if the norification.
|
||||
*/
|
||||
notificationTextContainer: {
|
||||
flexDirection: 'column',
|
||||
height: NOTIFICATION_SIZE,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* The touchable component.
|
||||
*/
|
||||
touchableView: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
211
react/features/calendar-sync/middleware.js
Normal file
@@ -0,0 +1,211 @@
|
||||
// @flow
|
||||
import Logger from 'jitsi-meet-logger';
|
||||
import RNCalendarEvents from 'react-native-calendar-events';
|
||||
|
||||
import { SET_ROOM } from '../base/conference';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
|
||||
import { maybeAddNewKnownDomain, updateCalendarEntryList } from './actions';
|
||||
import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
|
||||
|
||||
const FETCH_END_DAYS = 10;
|
||||
const FETCH_START_DAYS = -1;
|
||||
const MAX_LIST_LENGTH = 10;
|
||||
const logger = Logger.getLogger(__filename);
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
_ensureDefaultServer(store);
|
||||
_fetchCalendarEntries(store, false);
|
||||
break;
|
||||
case REFRESH_CALENDAR_ENTRY_LIST:
|
||||
_fetchCalendarEntries(store, true);
|
||||
break;
|
||||
case SET_ROOM:
|
||||
_parseAndAddDomain(store);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Ensures calendar access if possible and resolves the promise if it's granted.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} promptForPermission - Flag to tell the app if it should
|
||||
* prompt for a calendar permission if it wasn't granted yet.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _ensureCalendarAccess(promptForPermission) {
|
||||
return new Promise((resolve, reject) => {
|
||||
RNCalendarEvents.authorizationStatus()
|
||||
.then(status => {
|
||||
if (status === 'authorized') {
|
||||
resolve();
|
||||
} else if (promptForPermission) {
|
||||
RNCalendarEvents.authorizeEventStore()
|
||||
.then(result => {
|
||||
if (result === 'authorized') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(result);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
reject(status);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures presence of the default server in the known domains list.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _ensureDefaultServer(store) {
|
||||
const state = store.getState();
|
||||
const defaultURL = parseURIString(
|
||||
state['features/app'].app._getDefaultURL()
|
||||
);
|
||||
|
||||
store.dispatch(maybeAddNewKnownDomain(defaultURL.host));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user's calendar and updates the stored entries if need be.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} store - The redux store.
|
||||
* @param {boolean} promptForPermission - Flag to tell the app if it should
|
||||
* prompt for a calendar permission if it wasn't granted yet.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _fetchCalendarEntries(store, promptForPermission) {
|
||||
_ensureCalendarAccess(promptForPermission)
|
||||
.then(() => {
|
||||
const startDate = new Date();
|
||||
const endDate = new Date();
|
||||
|
||||
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
|
||||
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
|
||||
|
||||
RNCalendarEvents.fetchAllEvents(
|
||||
startDate.getTime(),
|
||||
endDate.getTime(),
|
||||
[]
|
||||
)
|
||||
.then(events => {
|
||||
const { knownDomains } = store.getState()['features/calendar-sync'];
|
||||
const eventList = [];
|
||||
|
||||
if (events && events.length) {
|
||||
for (const event of events) {
|
||||
const jitsiURL = _getURLFromEvent(event, knownDomains);
|
||||
const now = Date.now();
|
||||
|
||||
if (jitsiURL) {
|
||||
const eventStartDate = Date.parse(event.startDate);
|
||||
const eventEndDate = Date.parse(event.endDate);
|
||||
|
||||
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
|
||||
logger.warn(
|
||||
'Skipping calendar event due to invalid dates',
|
||||
event.title,
|
||||
event.startDate,
|
||||
event.endDate
|
||||
);
|
||||
} else if (eventEndDate > now) {
|
||||
eventList.push({
|
||||
endDate: eventEndDate,
|
||||
id: event.id,
|
||||
startDate: eventStartDate,
|
||||
title: event.title,
|
||||
url: jitsiURL
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
|
||||
a.startDate - b.startDate
|
||||
).slice(0, MAX_LIST_LENGTH)));
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Error fetching calendar.', error);
|
||||
});
|
||||
})
|
||||
.catch(reason => {
|
||||
logger.error('Error accessing calendar.', reason);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives a jitsi URL from an event if present.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} event - The event to parse.
|
||||
* @param {Array<string>} knownDomains - The known domain names.
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
function _getURLFromEvent(event, knownDomains) {
|
||||
const linkTerminatorPattern = '[^\\s<>$]';
|
||||
/* eslint-disable max-len */
|
||||
const urlRegExp
|
||||
= new RegExp(`http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`, 'gi');
|
||||
/* eslint-enable max-len */
|
||||
const schemeRegExp
|
||||
= new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
|
||||
const fieldsToSearch = [
|
||||
event.title,
|
||||
event.url,
|
||||
event.location,
|
||||
event.notes,
|
||||
event.description
|
||||
];
|
||||
let matchArray;
|
||||
|
||||
for (const field of fieldsToSearch) {
|
||||
if (typeof field === 'string') {
|
||||
if (
|
||||
(matchArray
|
||||
= urlRegExp.exec(field) || schemeRegExp.exec(field))
|
||||
!== null
|
||||
) {
|
||||
return matchArray[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreives the domain name of a room upon join and stores it
|
||||
* in the known domain list, if not present yet.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _parseAndAddDomain(store) {
|
||||
const { locationURL } = store.getState()['features/base/connection'];
|
||||
|
||||
store.dispatch(maybeAddNewKnownDomain(locationURL.host));
|
||||
}
|
||||
69
react/features/calendar-sync/reducer.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
import { PersistenceRegistry } from '../base/storage';
|
||||
|
||||
import { NEW_CALENDAR_ENTRY_LIST, NEW_KNOWN_DOMAIN } from './actionTypes';
|
||||
|
||||
/**
|
||||
* ZB: this is an object, as further data is to come here, like:
|
||||
* - known domain list
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
events: [],
|
||||
knownDomains: []
|
||||
};
|
||||
|
||||
const MAX_DOMAIN_LIST_SIZE = 10;
|
||||
|
||||
const STORE_NAME = 'features/calendar-sync';
|
||||
|
||||
PersistenceRegistry.register(STORE_NAME, {
|
||||
knownDomains: true
|
||||
});
|
||||
|
||||
ReducerRegistry.register(
|
||||
STORE_NAME,
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case NEW_CALENDAR_ENTRY_LIST:
|
||||
return {
|
||||
...state,
|
||||
events: action.events
|
||||
};
|
||||
|
||||
case NEW_KNOWN_DOMAIN:
|
||||
return _maybeAddNewDomain(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds a new domain to the known domain list if not present yet.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} action - The redux action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _maybeAddNewDomain(state, action) {
|
||||
let { domainName } = action;
|
||||
const { knownDomains } = state;
|
||||
|
||||
if (domainName && domainName.length) {
|
||||
domainName = domainName.toLowerCase();
|
||||
if (knownDomains.indexOf(domainName) === -1) {
|
||||
knownDomains.push(domainName);
|
||||
|
||||
// Ensure the list doesn't exceed a/the maximum size.
|
||||
knownDomains.splice(0, knownDomains.length - MAX_DOMAIN_LIST_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
knownDomains
|
||||
};
|
||||
}
|
||||
7
react/features/chat/constants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* The audio ID of the audio element for which the {@link playAudio} action is
|
||||
* triggered when new chat message is received.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const INCOMING_MSG_SOUND_ID = 'INCOMING_MSG_SOUND';
|
||||
3
react/features/chat/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './constants';
|
||||
|
||||
import './middleware';
|
||||
67
react/features/chat/middleware.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
import { CONFERENCE_JOINED } from '../base/conference';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
|
||||
import { INCOMING_MSG_SOUND_ID } from './constants';
|
||||
import { INCOMING_MSG_SOUND_SRC } from './sounds';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Implements the middleware of the chat feature.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
// Register the chat message sound on Web only because there's no chat
|
||||
// on mobile.
|
||||
typeof APP === 'undefined'
|
||||
|| store.dispatch(
|
||||
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_SRC));
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
// Unregister the chat message sound on Web because it's registered
|
||||
// there only.
|
||||
typeof APP === 'undefined'
|
||||
|| store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
typeof APP === 'undefined'
|
||||
|| _addChatMsgListener(action.conference, store);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers listener for {@link JitsiConferenceEvents.MESSAGE_RECEIVED} which
|
||||
* will play a sound on the event, given that the chat is not currently visible.
|
||||
*
|
||||
* @param {JitsiConference} conference - The conference instance on which the
|
||||
* new event listener will be registered.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _addChatMsgListener(conference, { dispatch }) {
|
||||
// XXX Currently, there's no need to remove the listener, because the
|
||||
// JitsiConference instance cannot be reused. Hence, the listener will be
|
||||
// gone with the JitsiConference instance.
|
||||
conference.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
() => {
|
||||
APP.UI.isChatVisible()
|
||||
|| dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
});
|
||||
}
|
||||
6
react/features/chat/sounds.web.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* The audio source for the incoming chat message sound.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const INCOMING_MSG_SOUND_SRC = 'sounds/incomingMessage.wav';
|
||||
@@ -12,6 +12,7 @@ import { DialogContainer } from '../../base/dialog';
|
||||
import { CalleeInfoContainer } from '../../base/jwt';
|
||||
import { Container, LoadingIndicator, TintedView } from '../../base/react';
|
||||
import { createDesiredLocalTracks } from '../../base/tracks';
|
||||
import { ConferenceNotification } from '../../calendar-sync';
|
||||
import { Filmstrip } from '../../filmstrip';
|
||||
import { LargeVideo } from '../../large-video';
|
||||
import { setToolboxVisible, Toolbox } from '../../toolbox';
|
||||
@@ -233,6 +234,8 @@ class Conference extends Component<Props> {
|
||||
<Filmstrip />
|
||||
</View>
|
||||
|
||||
<ConferenceNotification />
|
||||
|
||||
{/*
|
||||
* The dialogs are in the topmost stacking layers.
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Audio } from '../../base/media';
|
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.wav';
|
||||
|
||||
@@ -77,9 +78,8 @@ class AudioOutputPreview extends Component {
|
||||
<a onClick = { this._onClick }>
|
||||
{ this.props.t('deviceSelection.testAudio') }
|
||||
</a>
|
||||
<audio
|
||||
preload = 'auto'
|
||||
ref = { this._setAudioElement }
|
||||
<Audio
|
||||
setRef = { this._setAudioElement }
|
||||
src = { TEST_SOUND_PATH } />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* The type of the action which signals a check for a dial-out phone number has
|
||||
* succeeded.
|
||||
*
|
||||
* {
|
||||
* type: PHONE_NUMBER_CHECKED,
|
||||
* response: Object
|
||||
* }
|
||||
*/
|
||||
export const PHONE_NUMBER_CHECKED
|
||||
= Symbol('PHONE_NUMBER_CHECKED');
|
||||
|
||||
/**
|
||||
* The type of the action which signals a cancel of the dial-out operation.
|
||||
*
|
||||
* {
|
||||
* type: DIAL_OUT_CANCELED,
|
||||
* response: Object
|
||||
* }
|
||||
*/
|
||||
export const DIAL_OUT_CANCELED
|
||||
= Symbol('DIAL_OUT_CANCELED');
|
||||
|
||||
/**
|
||||
* The type of the action which signals a request for dial-out country codes has
|
||||
* succeeded.
|
||||
*
|
||||
* {
|
||||
* type: DIAL_OUT_CODES_UPDATED,
|
||||
* response: Object
|
||||
* }
|
||||
*/
|
||||
export const DIAL_OUT_CODES_UPDATED
|
||||
= Symbol('DIAL_OUT_CODES_UPDATED');
|
||||
|
||||
/**
|
||||
* The type of the action which signals a failure in some of dial-out service
|
||||
* requests.
|
||||
*
|
||||
* {
|
||||
* type: DIAL_OUT_SERVICE_FAILED,
|
||||
* response: Object
|
||||
* }
|
||||
*/
|
||||
export const DIAL_OUT_SERVICE_FAILED
|
||||
= Symbol('DIAL_OUT_SERVICE_FAILED');
|
||||
@@ -1,102 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
DIAL_OUT_CANCELED,
|
||||
DIAL_OUT_CODES_UPDATED,
|
||||
DIAL_OUT_SERVICE_FAILED,
|
||||
PHONE_NUMBER_CHECKED
|
||||
} from './actionTypes';
|
||||
|
||||
declare var $: Function;
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Dials the given number.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function cancel() {
|
||||
return {
|
||||
type: DIAL_OUT_CANCELED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dials the given number.
|
||||
*
|
||||
* @param {string} dialNumber - The number to dial.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function dial(dialNumber: string) {
|
||||
return (dispatch: Dispatch<*>, getState: Function) => {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
conference.dial(dialNumber);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an ajax request for dial-out country codes.
|
||||
*
|
||||
* @param {string} dialNumber - The dial number to check for validity.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function checkDialNumber(dialNumber: string) {
|
||||
return (dispatch: Dispatch<*>, getState: Function) => {
|
||||
const { dialOutAuthUrl } = getState()['features/base/config'];
|
||||
|
||||
if (!dialOutAuthUrl) {
|
||||
// no auth url, let's say it is valid
|
||||
const response = {};
|
||||
|
||||
response.allow = true;
|
||||
dispatch({
|
||||
type: PHONE_NUMBER_CHECKED,
|
||||
response
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fullUrl = `${dialOutAuthUrl}?phone=${dialNumber}`;
|
||||
|
||||
$.getJSON(fullUrl)
|
||||
.success(response =>
|
||||
dispatch({
|
||||
type: PHONE_NUMBER_CHECKED,
|
||||
response
|
||||
}))
|
||||
.error(error =>
|
||||
dispatch({
|
||||
type: DIAL_OUT_SERVICE_FAILED,
|
||||
error
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an ajax request for dial-out country codes.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function updateDialOutCodes() {
|
||||
return (dispatch: Dispatch<*>, getState: Function) => {
|
||||
const { dialOutCodesUrl } = getState()['features/base/config'];
|
||||
|
||||
if (!dialOutCodesUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.getJSON(dialOutCodesUrl)
|
||||
.success(response =>
|
||||
dispatch({
|
||||
type: DIAL_OUT_CODES_UPDATED,
|
||||
response
|
||||
}))
|
||||
.error(error =>
|
||||
dispatch({
|
||||
type: DIAL_OUT_SERVICE_FAILED,
|
||||
error
|
||||
}));
|
||||
};
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} to render a country flag icon.
|
||||
*/
|
||||
export default class CountryIcon extends Component {
|
||||
/**
|
||||
* {@code CountryIcon}'s property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The css style class name.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The 2-letter country code.
|
||||
*/
|
||||
countryCode: PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const iconClassName
|
||||
= `flag-icon flag-icon-${
|
||||
this.props.countryCode} flag-icon-squared ${
|
||||
this.props.className}`;
|
||||
|
||||
return <span className = { iconClassName } />;
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Dialog } from '../../base/dialog';
|
||||
|
||||
import { cancel, checkDialNumber, dial } from '../actions';
|
||||
import DialOutNumbersForm from './DialOutNumbersForm';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which allows the user to dial out from
|
||||
* the conference.
|
||||
*/
|
||||
class DialOutDialog extends Component {
|
||||
/**
|
||||
* {@code DialOutDialog} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The redux state representing the list of dial-out codes.
|
||||
*/
|
||||
_dialOutCodes: PropTypes.array,
|
||||
|
||||
/**
|
||||
* Property indicating if a dial number is allowed.
|
||||
*/
|
||||
_isDialNumberAllowed: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The function performing the cancel action.
|
||||
*/
|
||||
cancel: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The function performing the phone number validity check.
|
||||
*/
|
||||
checkDialNumber: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The function performing the dial action.
|
||||
*/
|
||||
dial: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code DialOutNumbersForm} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
/**
|
||||
* The number to dial.
|
||||
*/
|
||||
dialNumber: '',
|
||||
|
||||
/**
|
||||
* Indicates if the dial input is currently empty.
|
||||
*/
|
||||
isDialInputEmpty: true
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDialNumberChange = this._onDialNumberChange.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _isDialNumberAllowed } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
okDisabled = { this.state.isDialInputEmpty
|
||||
|| !_isDialNumberAllowed }
|
||||
okTitleKey = 'dialOut.dial'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialOut.dialOut'
|
||||
width = 'small'>
|
||||
{ this._renderContent() }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the dial number in a way to remove all non digital characters
|
||||
* from it (including spaces, brackets, dash, dot, etc.).
|
||||
*
|
||||
* @param {string} dialNumber - The phone number to format.
|
||||
* @private
|
||||
* @returns {string} - The formatted phone number.
|
||||
*/
|
||||
_formatDialNumber(dialNumber) {
|
||||
return dialNumber.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the dialog content.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderContent() {
|
||||
const { _isDialNumberAllowed } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'dial-out-content'>
|
||||
{ _isDialNumberAllowed ? '' : this._renderErrorMessage() }
|
||||
<DialOutNumbersForm
|
||||
onChange = { this._onDialNumberChange } />
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the error message to display if the dial phone number is not
|
||||
* allowed.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderErrorMessage() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'dial-out-error'>
|
||||
{ t('dialOut.phoneNotAllowed') }
|
||||
</div>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the dial out.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - Returns true to indicate that the dialog should be
|
||||
* closed.
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.cancel();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dials the number.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - Returns true to indicate that the dialog should be
|
||||
* closed.
|
||||
*/
|
||||
_onSubmit() {
|
||||
if (this.props._isDialNumberAllowed) {
|
||||
this.props.dial(this.state.dialNumber);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the dialNumber and check for validity.
|
||||
*
|
||||
* @param {string} dialCode - The dial code value.
|
||||
* @param {string} dialInput - The dial input value.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDialNumberChange(dialCode, dialInput) {
|
||||
let formattedDialInput, formattedNumber;
|
||||
|
||||
// if there are no dial out codes it is possible they are disabled
|
||||
// so we get the input as is, it can be just a sip address
|
||||
if (this.props._dialOutCodes) {
|
||||
// We remove all starting zeros from the dial input before attaching
|
||||
// it to the country code.
|
||||
formattedDialInput = dialInput.replace(/^(0+)/, '');
|
||||
|
||||
const dialNumber = `${dialCode}${formattedDialInput}`;
|
||||
|
||||
formattedNumber = this._formatDialNumber(dialNumber);
|
||||
|
||||
this.props.checkDialNumber(formattedNumber);
|
||||
} else {
|
||||
formattedNumber = formattedDialInput = dialInput;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dialNumber: formattedNumber,
|
||||
isDialInputEmpty: !formattedDialInput
|
||||
|| formattedDialInput.length === 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code DialOutDialog}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isDialNumberAllowed: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { dialOutCodes, isDialNumberAllowed } = state['features/dial-out'];
|
||||
|
||||
return {
|
||||
/**
|
||||
* List of dial-out codes.
|
||||
*
|
||||
* @private
|
||||
* @type {array}
|
||||
*/
|
||||
_dialOutCodes: dialOutCodes,
|
||||
|
||||
/**
|
||||
* Property indicating if a dial number is allowed.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
_isDialNumberAllowed: isDialNumberAllowed
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(
|
||||
connect(_mapStateToProps, {
|
||||
cancel,
|
||||
checkDialNumber,
|
||||
dial
|
||||
})(DialOutDialog));
|
||||
@@ -1,369 +0,0 @@
|
||||
import { DropdownMenuStateless as DropdownMenu } from '@atlaskit/dropdown-menu';
|
||||
import { FieldTextStateless as TextField } from '@atlaskit/field-text';
|
||||
import ChevronDownIcon from '@atlaskit/icon/glyph/chevron-down';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import { updateDialOutCodes } from '../actions';
|
||||
import CountryIcon from './CountryIcon';
|
||||
|
||||
/**
|
||||
* The default value of the country if the fetch service is unavailable.
|
||||
*
|
||||
* @type {{
|
||||
* code: string,
|
||||
* dialCode: string,
|
||||
* name: string
|
||||
* }}
|
||||
*/
|
||||
const DEFAULT_COUNTRY = {
|
||||
code: 'US',
|
||||
dialCode: '+1',
|
||||
name: 'United States'
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} responsible for fetching and displaying dial-out
|
||||
* country codes, as well as dialing a phone number.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class DialOutNumbersForm extends Component {
|
||||
/**
|
||||
* {@code DialOutNumbersForm}'s property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The redux state representing the list of dial-out codes.
|
||||
*/
|
||||
_dialOutCodes: PropTypes.array,
|
||||
|
||||
/**
|
||||
* The function called on every dial input change.
|
||||
*/
|
||||
onChange: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Invoked to send an ajax request for dial-out codes.
|
||||
*/
|
||||
updateDialOutCodes: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code DialOutNumbersForm} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dialInput: '',
|
||||
|
||||
/**
|
||||
* Whether or not the dropdown should be open.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isDropdownOpen: false,
|
||||
|
||||
/**
|
||||
* The selected country.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
selectedCountry: DEFAULT_COUNTRY
|
||||
};
|
||||
|
||||
/**
|
||||
* The internal reference to the DOM/HTML element backing the React
|
||||
* {@code Component} text input.
|
||||
*
|
||||
* @private
|
||||
* @type {HTMLInputElement}
|
||||
*/
|
||||
this._dialInputElem = null;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDropdownTriggerInputChange
|
||||
= this._onDropdownTriggerInputChange.bind(this);
|
||||
this._onInputChange = this._onInputChange.bind(this);
|
||||
this._onOpenChange = this._onOpenChange.bind(this);
|
||||
this._onSelect = this._onSelect.bind(this);
|
||||
this._setDialInputElement = this._setDialInputElement.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a request for dial out codes if not already present in the
|
||||
* redux store. If dial out codes are present, sets a default code to
|
||||
* display in the dropdown trigger.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
const dialOutCodes = this.props._dialOutCodes;
|
||||
|
||||
if (dialOutCodes) {
|
||||
this._setDefaultCode(dialOutCodes);
|
||||
} else {
|
||||
this.props.updateDialOutCodes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors for dial out code updates and sets a default code to display in
|
||||
* the dropdown trigger if not already set.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.state.selectedCountry && nextProps._dialOutCodes) {
|
||||
this._setDefaultCode(nextProps._dialOutCodes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, _dialOutCodes } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'form-control'>
|
||||
{ _dialOutCodes ? this._createDropdownMenu(
|
||||
this._formatCountryCodes(_dialOutCodes)) : null }
|
||||
<div className = 'dial-out-input'>
|
||||
<TextField
|
||||
autoFocus = { true }
|
||||
isLabelHidden = { true }
|
||||
label = { 'dial-out-input-field' }
|
||||
onChange = { this._onInputChange }
|
||||
placeholder = { t('dialOut.enterPhone') }
|
||||
ref = { this._setDialInputElement }
|
||||
shouldFitContainer = { true }
|
||||
value = { this.state.dialInput } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code DropdownMenu} instance.
|
||||
*
|
||||
* @param {Array} items - The content to display within the dropdown.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createDropdownMenu(items) {
|
||||
const { code, dialCode } = this.state.selectedCountry;
|
||||
|
||||
return (
|
||||
<div className = 'dropdown-container'>
|
||||
<DropdownMenu
|
||||
isOpen = { this.state.isDropdownOpen }
|
||||
items = { [ { items } ] }
|
||||
onItemActivated = { this._onSelect }
|
||||
onOpenChange = { this._onOpenChange }
|
||||
shouldFitContainer = { false }>
|
||||
{ this._createDropdownTrigger(dialCode, code) }
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a React {@code Component} with a readonly HTMLInputElement as a
|
||||
* trigger for displaying the dropdown menu. The {@code Component} will also
|
||||
* display the currently selected number.
|
||||
*
|
||||
* @param {string} dialCode - The +xx dial code.
|
||||
* @param {string} countryCode - The country 2 letter code.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_createDropdownTrigger(dialCode, countryCode) {
|
||||
return (
|
||||
<div className = 'dropdown'>
|
||||
<CountryIcon
|
||||
className = 'dial-out-flag-icon'
|
||||
countryCode = { `${countryCode}` } />
|
||||
{ /**
|
||||
* FIXME Replace TextField with AtlasKit Button when an issue
|
||||
* with icons shrinking due to button text is fixed.
|
||||
*/ }
|
||||
<TextField
|
||||
className = 'input-control dial-out-code'
|
||||
isLabelHidden = { true }
|
||||
isReadOnly = { true }
|
||||
label = 'dial-out-code'
|
||||
onChange = { this._onDropdownTriggerInputChange }
|
||||
type = 'text'
|
||||
value = { dialCode || '' } />
|
||||
<span className = 'dropdown-trigger-icon'>
|
||||
<ChevronDownIcon
|
||||
label = 'expand'
|
||||
size = 'small' />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the passed in numbers object into an array of objects that can
|
||||
* be parsed by {@code DropdownMenu}.
|
||||
*
|
||||
* @param {Object} countryCodes - The list of country codes.
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_formatCountryCodes(countryCodes) {
|
||||
return countryCodes.map(country => {
|
||||
const countryIcon
|
||||
= <CountryIcon countryCode = { `${country.code}` } />;
|
||||
const countryElement
|
||||
= <span>{countryIcon} { country.name }</span>;
|
||||
|
||||
return {
|
||||
content: `${country.dialCode}`,
|
||||
country,
|
||||
elemBefore: countryElement
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the dialNumber when changes to the dial text or code happen.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDialNumberChange() {
|
||||
const { dialCode } = this.state.selectedCountry;
|
||||
|
||||
this.props.onChange(dialCode, this.state.dialInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a no-op function used to stub out TextField's onChange in order
|
||||
* to prevent TextField from printing prop type validation errors. TextField
|
||||
* is used as a trigger for the dropdown in {@code DialOutNumbersForm} to
|
||||
* get the desired AtlasKit input look for the UI.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDropdownTriggerInputChange() {
|
||||
// Intentionally left empty.
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the dialInput state when the input changes.
|
||||
*
|
||||
* @param {Object} e - The event notifying us of the change.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onInputChange(e) {
|
||||
this.setState({
|
||||
dialInput: e.target.value
|
||||
}, () => {
|
||||
this._onDialNumberChange();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal state to either open or close the dropdown. If the
|
||||
* dropdown is disabled, the state will always be set to false.
|
||||
*
|
||||
* @param {Object} dropdownEvent - The even returned from clicking on the
|
||||
* dropdown trigger.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenChange(dropdownEvent) {
|
||||
this.setState({
|
||||
isDropdownOpen: dropdownEvent.isOpen
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state of the currently selected country code.
|
||||
*
|
||||
* @param {Object} selection - Event from choosing an dropdown option.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelect(selection) {
|
||||
this.setState({
|
||||
isDropdownOpen: false,
|
||||
selectedCountry: selection.item.country
|
||||
}, () => {
|
||||
this._onDialNumberChange();
|
||||
|
||||
this._dialInputElem.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state of the currently selected number by defaulting
|
||||
* to the first available number.
|
||||
*
|
||||
* @param {Object} countryCodes - The list of country codes to choose from
|
||||
* for setting a default code.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setDefaultCode(countryCodes) {
|
||||
this.setState({
|
||||
selectedCountry: countryCodes[0]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the internal reference to the DOM/HTML element backing the React
|
||||
* {@code Component} dial input.
|
||||
*
|
||||
* @param {HTMLInputElement} input - The DOM/HTML element for this
|
||||
* {@code Component}'s text input.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setDialInputElement(input) {
|
||||
this._dialInputElem = input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code DialOutNumbersForm}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _dialOutCodes: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { dialOutCodes } = state['features/dial-out'];
|
||||
|
||||
return {
|
||||
_dialOutCodes: dialOutCodes
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(
|
||||
connect(_mapStateToProps, { updateDialOutCodes })(DialOutNumbersForm));
|
||||
@@ -1 +0,0 @@
|
||||
export { default as DialOutDialog } from './DialOutDialog';
|
||||
@@ -1,53 +0,0 @@
|
||||
import {
|
||||
ReducerRegistry
|
||||
} from '../base/redux';
|
||||
|
||||
import {
|
||||
DIAL_OUT_CANCELED,
|
||||
DIAL_OUT_CODES_UPDATED,
|
||||
DIAL_OUT_SERVICE_FAILED,
|
||||
PHONE_NUMBER_CHECKED
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
dialOutCodes: null,
|
||||
error: null,
|
||||
isDialNumberAllowed: true
|
||||
};
|
||||
|
||||
ReducerRegistry.register(
|
||||
'features/dial-out',
|
||||
(state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case DIAL_OUT_CANCELED: {
|
||||
// if we have already downloaded codes fill them in default state
|
||||
// to skip another ajax query
|
||||
return {
|
||||
...DEFAULT_STATE,
|
||||
dialOutCodes: state.dialOutCodes
|
||||
};
|
||||
}
|
||||
case DIAL_OUT_CODES_UPDATED: {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
dialOutCodes: action.response
|
||||
};
|
||||
}
|
||||
case DIAL_OUT_SERVICE_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
error: action.error
|
||||
};
|
||||
}
|
||||
case PHONE_NUMBER_CHECKED: {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
isDialNumberAllowed: action.response.allow
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||
import { InviteButton } from '../../invite';
|
||||
import { Toolbox } from '../../toolbox';
|
||||
|
||||
@@ -43,6 +44,18 @@ class Filmstrip extends Component<*> {
|
||||
*/
|
||||
_hovered: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not the feature to directly invite people into the
|
||||
* conference is available.
|
||||
*/
|
||||
_isAddToCallAvailable: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not the feature to dial out to number to join the
|
||||
* conference is available.
|
||||
*/
|
||||
_isDialOutAvailable: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not the remote videos should be visible. Will toggle
|
||||
* a class for hiding the videos.
|
||||
@@ -93,6 +106,14 @@ class Filmstrip extends Component<*> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_hideInviteButton,
|
||||
_isAddToCallAvailable,
|
||||
_isDialOutAvailable,
|
||||
_remoteVideosVisible,
|
||||
filmstripOnly
|
||||
} = this.props;
|
||||
|
||||
/**
|
||||
* Note: Appending of {@code RemoteVideo} views is handled through
|
||||
* VideoLayout. The views do not get blown away on render() because
|
||||
@@ -102,12 +123,12 @@ class Filmstrip extends Component<*> {
|
||||
* modified, then the views will get blown away.
|
||||
*/
|
||||
|
||||
const filmstripClassNames = `filmstrip ${this.props._remoteVideosVisible
|
||||
? '' : 'hide-videos'}`;
|
||||
const filmstripClassNames = `filmstrip ${_remoteVideosVisible ? ''
|
||||
: 'hide-videos'}`;
|
||||
|
||||
return (
|
||||
<div className = { filmstripClassNames }>
|
||||
{ this.props.filmstripOnly ? <Toolbox /> : null }
|
||||
{ filmstripOnly ? <Toolbox /> : null }
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'remoteVideos'>
|
||||
@@ -116,9 +137,11 @@ class Filmstrip extends Component<*> {
|
||||
id = 'filmstripLocalVideo'
|
||||
onMouseOut = { this._onMouseOut }
|
||||
onMouseOver = { this._onMouseOver }>
|
||||
{ this.props.filmstripOnly
|
||||
|| this.props._hideInviteButton
|
||||
? null : <InviteButton /> }
|
||||
{ filmstripOnly || _hideInviteButton
|
||||
? null
|
||||
: <InviteButton
|
||||
enableAddPeople = { _isAddToCallAvailable }
|
||||
enableDialOut = { _isDialOutAvailable } /> }
|
||||
<div id = 'filmstripLocalVideoThumbnail' />
|
||||
</div>
|
||||
<div
|
||||
@@ -135,14 +158,6 @@ class Filmstrip extends Component<*> {
|
||||
onMouseOut = { this._onMouseOut }
|
||||
onMouseOver = { this._onMouseOver } />
|
||||
</div>
|
||||
<audio
|
||||
id = 'userJoined'
|
||||
preload = 'auto'
|
||||
src = 'sounds/joined.wav' />
|
||||
<audio
|
||||
id = 'userLeft'
|
||||
preload = 'auto'
|
||||
src = 'sounds/left.wav' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -192,15 +207,34 @@ class Filmstrip extends Component<*> {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _hovered: boolean,
|
||||
* _hideInviteButton: boolean,
|
||||
* _hovered: boolean,
|
||||
* _isAddToCallAvailable: boolean,
|
||||
* _isDialOutAvailable: boolean,
|
||||
* _remoteVideosVisible: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const {
|
||||
enableUserRolesBasedOnToken,
|
||||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
const { hovered } = state['features/filmstrip'];
|
||||
|
||||
const isAddToCallAvailable = !isGuest;
|
||||
const isDialOutAvailable
|
||||
= getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
|
||||
&& conference && conference.isSIPCallingSupported()
|
||||
&& (!enableUserRolesBasedOnToken || !isGuest);
|
||||
|
||||
return {
|
||||
_hovered: state['features/filmstrip'].hovered,
|
||||
_hideInviteButton: state['features/base/config'].iAmRecorder,
|
||||
_hideInviteButton: iAmRecorder
|
||||
|| (!isAddToCallAvailable && !isDialOutAvailable),
|
||||
_hovered: hovered,
|
||||
_isAddToCallAvailable: isAddToCallAvailable,
|
||||
_isDialOutAvailable: isDialOutAvailable,
|
||||
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,12 +11,21 @@ import { getInviteURL } from '../../base/connection';
|
||||
import { Dialog, hideDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { MultiSelectAutocomplete } from '../../base/react';
|
||||
|
||||
import { invitePeopleAndChatRooms, searchDirectory } from '../functions';
|
||||
import { inviteVideoRooms } from '../../videosipgw';
|
||||
|
||||
import {
|
||||
checkDialNumber,
|
||||
invitePeopleAndChatRooms,
|
||||
searchDirectory
|
||||
} from '../functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
const isPhoneNumberRegex
|
||||
= new RegExp(interfaceConfig.PHONE_NUMBER_REGEX || '^[0-9+()-\\s]*$');
|
||||
|
||||
/**
|
||||
* The dialog that allows to invite people to the call.
|
||||
*/
|
||||
@@ -33,6 +42,11 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
*/
|
||||
_conference: PropTypes.object,
|
||||
|
||||
/**
|
||||
* The URL for validating if a phone number can be called.
|
||||
*/
|
||||
_dialOutAuthUrl: PropTypes.string,
|
||||
|
||||
/**
|
||||
* The URL pointing to the service allowing for people invite.
|
||||
*/
|
||||
@@ -58,6 +72,16 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
*/
|
||||
_peopleSearchUrl: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Whether or not to show Add People functionality.
|
||||
*/
|
||||
enableAddPeople: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Whether or not to show Dial Out functionality.
|
||||
*/
|
||||
enableDialOut: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The function closing the dialog.
|
||||
*/
|
||||
@@ -76,33 +100,7 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
|
||||
_multiselect = null;
|
||||
|
||||
_resourceClient = {
|
||||
makeQuery: text => {
|
||||
const {
|
||||
_jwt,
|
||||
_peopleSearchQueryTypes,
|
||||
_peopleSearchUrl
|
||||
} = this.props; // eslint-disable-line no-invalid-this
|
||||
|
||||
return (
|
||||
searchDirectory(
|
||||
_peopleSearchUrl,
|
||||
_jwt,
|
||||
text,
|
||||
_peopleSearchQueryTypes));
|
||||
},
|
||||
|
||||
parseResults: response => response.map(user => {
|
||||
return {
|
||||
content: user.name,
|
||||
elemBefore: <Avatar
|
||||
size = 'medium'
|
||||
src = { user.avatar } />,
|
||||
item: user,
|
||||
value: user.id
|
||||
};
|
||||
})
|
||||
};
|
||||
_resourceClient: Object;
|
||||
|
||||
state = {
|
||||
/**
|
||||
@@ -116,6 +114,12 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
*/
|
||||
addToCallInProgress: false,
|
||||
|
||||
|
||||
// FIXME: Remove usage of Immutable. {@code MultiSelectAutocomplete}
|
||||
// will default to having its internal implementation use a plain array
|
||||
// if no {@link defaultValue} is passed in. As such is the case, this
|
||||
// instance of Immutable.List gets overridden with an array on the first
|
||||
// search.
|
||||
/**
|
||||
* The list of invite items.
|
||||
*/
|
||||
@@ -133,9 +137,17 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._isAddDisabled = this._isAddDisabled.bind(this);
|
||||
this._onItemSelected = this._onItemSelected.bind(this);
|
||||
this._onSelectionChange = this._onSelectionChange.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._parseQueryResults = this._parseQueryResults.bind(this);
|
||||
this._query = this._query.bind(this);
|
||||
this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
|
||||
|
||||
this._resourceClient = {
|
||||
makeQuery: this._query,
|
||||
parseResults: this._parseQueryResults
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +165,7 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
&& !this.state.addToCallInProgress
|
||||
&& !this.state.addToCallError
|
||||
&& this._multiselect) {
|
||||
this._multiselect.clear();
|
||||
this._multiselect.setSelectedItems([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,18 +175,69 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { enableAddPeople, enableDialOut, t } = this.props;
|
||||
let isMultiSelectDisabled = this.state.addToCallInProgress || false;
|
||||
let placeholder;
|
||||
let loadingMessage;
|
||||
let noMatches;
|
||||
|
||||
if (enableAddPeople && enableDialOut) {
|
||||
loadingMessage = 'addPeople.loading';
|
||||
noMatches = 'addPeople.noResults';
|
||||
placeholder = 'addPeople.searchPeopleAndNumbers';
|
||||
} else if (enableAddPeople) {
|
||||
loadingMessage = 'addPeople.loadingPeople';
|
||||
noMatches = 'addPeople.noResults';
|
||||
placeholder = 'addPeople.searchPeople';
|
||||
} else if (enableDialOut) {
|
||||
loadingMessage = 'addPeople.loadingNumber';
|
||||
noMatches = 'addPeople.noValidNumbers';
|
||||
placeholder = 'addPeople.searchNumbers';
|
||||
} else {
|
||||
isMultiSelectDisabled = true;
|
||||
noMatches = 'addPeople.noResults';
|
||||
placeholder = 'addPeople.disabled';
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
okDisabled = { this._isAddDisabled() }
|
||||
okTitleKey = 'addPeople.add'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'addPeople.title'
|
||||
width = 'small'>
|
||||
{ this._renderUserInputForm() }
|
||||
width = 'medium'>
|
||||
<div className = 'add-people-form-wrap'>
|
||||
{ this._renderErrorMessage() }
|
||||
<MultiSelectAutocomplete
|
||||
isDisabled = { isMultiSelectDisabled }
|
||||
loadingMessage = { t(loadingMessage) }
|
||||
noMatchesFound = { t(noMatches) }
|
||||
onItemSelected = { this._onItemSelected }
|
||||
onSelectionChange = { this._onSelectionChange }
|
||||
placeholder = { t(placeholder) }
|
||||
ref = { this._setMultiSelectElement }
|
||||
resourceClient = { this._resourceClient }
|
||||
shouldFitContainer = { true }
|
||||
shouldFocus = { true } />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_getDigitsOnly: (string) => string;
|
||||
|
||||
/**
|
||||
* Removes all non-numeric characters from a string.
|
||||
*
|
||||
* @param {string} text - The string from which to remove all characters
|
||||
* except numbers.
|
||||
* @private
|
||||
* @returns {string} A string with only numbers.
|
||||
*/
|
||||
_getDigitsOnly(text = '') {
|
||||
return text.replace(/\D/g, '');
|
||||
}
|
||||
|
||||
_isAddDisabled: () => boolean;
|
||||
|
||||
/**
|
||||
@@ -189,6 +252,45 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
|| this.state.addToCallInProgress;
|
||||
}
|
||||
|
||||
_isMaybeAPhoneNumber: (string) => boolean;
|
||||
|
||||
/**
|
||||
* Checks whether a string looks like it could be for a phone number.
|
||||
*
|
||||
* @param {string} text - The text to check whether or not it could be a
|
||||
* phone number.
|
||||
* @private
|
||||
* @returns {boolean} True if the string looks like it could be a phone
|
||||
* number.
|
||||
*/
|
||||
_isMaybeAPhoneNumber(text) {
|
||||
if (!isPhoneNumberRegex.test(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const digits = this._getDigitsOnly(text);
|
||||
|
||||
return Boolean(digits.length);
|
||||
}
|
||||
|
||||
_onItemSelected: (Object) => Object;
|
||||
|
||||
/**
|
||||
* Callback invoked when a selection has been made but before it has been
|
||||
* set as selected.
|
||||
*
|
||||
* @param {Object} item - The item that has just been selected.
|
||||
* @private
|
||||
* @returns {Object} The item to display as selected in the input.
|
||||
*/
|
||||
_onItemSelected(item) {
|
||||
if (item.item.type === 'phone') {
|
||||
item.content = item.item.number;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
_onSelectionChange: (Map<*, *>) => void;
|
||||
|
||||
/**
|
||||
@@ -199,55 +301,279 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelectionChange(selectedItems) {
|
||||
const selectedIds = selectedItems.map(o => o.item);
|
||||
|
||||
this.setState({
|
||||
inviteItems: selectedIds
|
||||
inviteItems: selectedItems
|
||||
});
|
||||
}
|
||||
|
||||
_onSubmit: () => void;
|
||||
|
||||
/**
|
||||
* Handles the submit button action.
|
||||
* Invite people and numbers to the conference. The logic works by inviting
|
||||
* numbers, people/rooms, and videosipgw in parallel. All invitees are
|
||||
* stored in an array. As each invite succeeds, the invitee is removed
|
||||
* from the array. After all invites finish, close the modal if there are
|
||||
* no invites left to send. If any are left, that means an invite failed
|
||||
* and an error state should display.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit() {
|
||||
if (!this._isAddDisabled()) {
|
||||
this.setState({
|
||||
addToCallInProgress: true
|
||||
if (this._isAddDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
addToCallInProgress: true
|
||||
});
|
||||
|
||||
let allInvitePromises = [];
|
||||
let invitesLeftToSend = [
|
||||
...this.state.inviteItems
|
||||
];
|
||||
|
||||
// First create all promises for dialing out.
|
||||
if (this.props.enableDialOut && this.props._conference) {
|
||||
const phoneNumbers = invitesLeftToSend.filter(
|
||||
({ item }) => item.type === 'phone');
|
||||
|
||||
// For each number, dial out. On success, remove the number from
|
||||
// {@link invitesLeftToSend}.
|
||||
const phoneInvitePromises = phoneNumbers.map(number => {
|
||||
const numberToInvite = this._getDigitsOnly(number.item.number);
|
||||
|
||||
return this.props._conference.dial(numberToInvite)
|
||||
.then(() => {
|
||||
invitesLeftToSend
|
||||
= invitesLeftToSend.filter(invite =>
|
||||
invite !== number);
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting phone number:', error));
|
||||
|
||||
});
|
||||
|
||||
const vrooms = this.state.inviteItems.filter(
|
||||
i => i.type === 'videosipgw');
|
||||
allInvitePromises = allInvitePromises.concat(phoneInvitePromises);
|
||||
}
|
||||
|
||||
if (this.props.enableAddPeople) {
|
||||
const usersAndRooms = invitesLeftToSend.filter(i =>
|
||||
i.item.type === 'user' || i.item.type === 'room')
|
||||
.map(i => i.item);
|
||||
|
||||
if (usersAndRooms.length) {
|
||||
// Send a request to invite all the rooms and users. On success,
|
||||
// filter all rooms and users from {@link invitesLeftToSend}.
|
||||
const peopleInvitePromise = invitePeopleAndChatRooms(
|
||||
this.props._inviteServiceUrl,
|
||||
this.props._inviteUrl,
|
||||
this.props._jwt,
|
||||
usersAndRooms)
|
||||
.then(() => {
|
||||
invitesLeftToSend = invitesLeftToSend.filter(i =>
|
||||
i.item.type !== 'user' && i.item.type !== 'room');
|
||||
})
|
||||
.catch(error => logger.error(
|
||||
'Error inviting people:', error));
|
||||
|
||||
allInvitePromises.push(peopleInvitePromise);
|
||||
}
|
||||
|
||||
// Sipgw calls are fire and forget. Invite them to the conference
|
||||
// then immediately remove them from {@link invitesLeftToSend}.
|
||||
const vrooms = invitesLeftToSend.filter(i =>
|
||||
i.item.type === 'videosipgw')
|
||||
.map(i => i.item);
|
||||
|
||||
this.props._conference
|
||||
&& vrooms.length > 0
|
||||
&& this.props.inviteVideoRooms(this.props._conference, vrooms);
|
||||
&& this.props.inviteVideoRooms(
|
||||
this.props._conference, vrooms);
|
||||
|
||||
invitePeopleAndChatRooms(
|
||||
this.props._inviteServiceUrl,
|
||||
this.props._inviteUrl,
|
||||
this.props._jwt,
|
||||
this.state.inviteItems.filter(
|
||||
i => i.type === 'user' || i.type === 'room'))
|
||||
.then(
|
||||
/* onFulfilled */ () => {
|
||||
this.setState({
|
||||
addToCallInProgress: false
|
||||
});
|
||||
invitesLeftToSend = invitesLeftToSend.filter(i =>
|
||||
i.item.type !== 'videosipgw');
|
||||
}
|
||||
|
||||
Promise.all(allInvitePromises)
|
||||
.then(() => {
|
||||
// If any invites are left that means something failed to send
|
||||
// so treat it as an error.
|
||||
if (invitesLeftToSend.length) {
|
||||
logger.error(`${invitesLeftToSend.length} invites failed`);
|
||||
|
||||
this.props.hideDialog();
|
||||
},
|
||||
/* onRejected */ () => {
|
||||
this.setState({
|
||||
addToCallInProgress: false,
|
||||
addToCallError: true
|
||||
});
|
||||
|
||||
if (this._multiselect) {
|
||||
this._multiselect.setSelectedItems(invitesLeftToSend);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
addToCallInProgress: false
|
||||
});
|
||||
|
||||
this.props.hideDialog();
|
||||
});
|
||||
}
|
||||
|
||||
_parseQueryResults: (Array<Object>, string) => Array<Object>;
|
||||
|
||||
/**
|
||||
* Processes results from requesting available numbers and people by munging
|
||||
* each result into a format {@code MultiSelectAutocomplete} can use for
|
||||
* display.
|
||||
*
|
||||
* @param {Array} response - The response object from the server for the
|
||||
* query.
|
||||
* @private
|
||||
* @returns {Object[]} Configuration objects for items to display in the
|
||||
* search autocomplete.
|
||||
*/
|
||||
_parseQueryResults(response = []) {
|
||||
const { t } = this.props;
|
||||
const users = response.filter(item => item.type !== 'phone');
|
||||
const userDisplayItems = users.map(user => {
|
||||
return {
|
||||
content: user.name,
|
||||
elemBefore: <Avatar
|
||||
size = 'medium'
|
||||
src = { user.avatar } />,
|
||||
item: user,
|
||||
tag: {
|
||||
elemBefore: <Avatar
|
||||
size = 'xsmall'
|
||||
src = { user.avatar } />
|
||||
},
|
||||
value: user.id
|
||||
};
|
||||
});
|
||||
|
||||
const numbers = response.filter(item => item.type === 'phone');
|
||||
const telephoneIcon = this._renderTelephoneIcon();
|
||||
|
||||
const numberDisplayItems = numbers.map(number => {
|
||||
const numberNotAllowedMessage
|
||||
= number.allowed ? '' : t('addPeople.countryNotSupported');
|
||||
const countryCodeReminder = number.showCountryCodeReminder
|
||||
? t('addPeople.countryReminder') : '';
|
||||
const description
|
||||
= `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
|
||||
|
||||
return {
|
||||
filterValues: [
|
||||
number.originalEntry,
|
||||
number.number
|
||||
],
|
||||
content: t('addPeople.telephone', { number: number.number }),
|
||||
description,
|
||||
isDisabled: !number.allowed,
|
||||
elemBefore: telephoneIcon,
|
||||
item: number,
|
||||
tag: {
|
||||
elemBefore: telephoneIcon
|
||||
},
|
||||
value: number.number
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
...userDisplayItems,
|
||||
...numberDisplayItems
|
||||
];
|
||||
}
|
||||
|
||||
_query: (string) => Promise<Array<Object>>;
|
||||
|
||||
/**
|
||||
* Performs a people and phone number search request.
|
||||
*
|
||||
* @param {string} query - The search text.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_query(query = '') {
|
||||
const text = query.trim();
|
||||
const {
|
||||
_dialOutAuthUrl,
|
||||
_jwt,
|
||||
_peopleSearchQueryTypes,
|
||||
_peopleSearchUrl
|
||||
} = this.props;
|
||||
|
||||
let peopleSearchPromise;
|
||||
|
||||
if (this.props.enableAddPeople) {
|
||||
peopleSearchPromise = searchDirectory(
|
||||
_peopleSearchUrl,
|
||||
_jwt,
|
||||
text,
|
||||
_peopleSearchQueryTypes);
|
||||
} else {
|
||||
peopleSearchPromise = Promise.resolve([]);
|
||||
}
|
||||
|
||||
|
||||
const hasCountryCode = text.startsWith('+');
|
||||
let phoneNumberPromise;
|
||||
|
||||
if (this.props.enableDialOut && this._isMaybeAPhoneNumber(text)) {
|
||||
let numberToVerify = text;
|
||||
|
||||
// When the number to verify does not start with a +, we assume no
|
||||
// proper country code has been entered. In such a case, prepend 1
|
||||
// for the country code. The service currently takes care of
|
||||
// prepending the +.
|
||||
if (!hasCountryCode && !text.startsWith('1')) {
|
||||
numberToVerify = `1${numberToVerify}`;
|
||||
}
|
||||
|
||||
// The validation service works properly when the query is digits
|
||||
// only so ensure only digits get sent.
|
||||
numberToVerify = this._getDigitsOnly(numberToVerify);
|
||||
|
||||
phoneNumberPromise
|
||||
= checkDialNumber(numberToVerify, _dialOutAuthUrl);
|
||||
} else {
|
||||
phoneNumberPromise = Promise.resolve({});
|
||||
}
|
||||
|
||||
return Promise.all([ peopleSearchPromise, phoneNumberPromise ])
|
||||
.then(([ peopleResults, phoneResults ]) => {
|
||||
const results = [
|
||||
...peopleResults
|
||||
];
|
||||
|
||||
/**
|
||||
* This check for phone results is for the day the call to
|
||||
* searching people might return phone results as well. When
|
||||
* that day comes this check will make it so the server checks
|
||||
* are honored and the local appending of the number is not
|
||||
* done. The local appending of the phone number can then be
|
||||
* cleaned up when convenient.
|
||||
*/
|
||||
const hasPhoneResult = peopleResults.find(
|
||||
result => result.type === 'phone');
|
||||
|
||||
if (!hasPhoneResult
|
||||
&& typeof phoneResults.allow === 'boolean') {
|
||||
results.push({
|
||||
allowed: phoneResults.allow,
|
||||
country: phoneResults.country,
|
||||
type: 'phone',
|
||||
number: phoneResults.phone,
|
||||
originalEntry: text,
|
||||
showCountryCodeReminder: !hasCountryCode
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,28 +620,16 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the input form.
|
||||
* Renders a telephone icon.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderUserInputForm() {
|
||||
const { t } = this.props;
|
||||
|
||||
_renderTelephoneIcon() {
|
||||
return (
|
||||
<div className = 'add-people-form-wrap'>
|
||||
{ this._renderErrorMessage() }
|
||||
<MultiSelectAutocomplete
|
||||
isDisabled
|
||||
= { this.state.addToCallInProgress || false }
|
||||
noMatchesFound = { t('addPeople.noResults') }
|
||||
onSelectionChange = { this._onSelectionChange }
|
||||
placeholder = { t('addPeople.searchPlaceholder') }
|
||||
ref = { this._setMultiSelectElement }
|
||||
resourceClient = { this._resourceClient }
|
||||
shouldFitContainer = { true }
|
||||
shouldFocus = { true } />
|
||||
</div>
|
||||
<span className = 'add-telephone-icon'>
|
||||
<i className = 'icon-telephone' />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -341,13 +655,19 @@ class AddPeopleDialog extends Component<*, *> {
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conference: Object,
|
||||
* _dialOutAuthUrl: string,
|
||||
* _inviteServiceUrl: string,
|
||||
* _inviteUrl: string,
|
||||
* _jwt: string,
|
||||
* _peopleSearchQueryTypes: Array<string>,
|
||||
* _peopleSearchUrl: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const {
|
||||
dialOutAuthUrl,
|
||||
inviteServiceUrl,
|
||||
peopleSearchQueryTypes,
|
||||
peopleSearchUrl
|
||||
@@ -355,6 +675,7 @@ function _mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
_conference: conference,
|
||||
_dialOutAuthUrl: dialOutAuthUrl,
|
||||
_inviteServiceUrl: inviteServiceUrl,
|
||||
_inviteUrl: getInviteURL(state),
|
||||
_jwt: state['features/base/jwt'].jwt,
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
/* global interfaceConfig */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Button from '@atlaskit/button';
|
||||
import DropdownMenu from '@atlaskit/dropdown-menu';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { AddPeopleDialog } from '.';
|
||||
import { DialOutDialog } from '../../dial-out';
|
||||
import { isInviteOptionEnabled } from '../functions';
|
||||
|
||||
const DIAL_OUT_OPTION = 'dialout';
|
||||
const ADD_TO_CALL_OPTION = 'addtocall';
|
||||
|
||||
/**
|
||||
* The button that provides different invite options.
|
||||
@@ -27,20 +17,20 @@ class InviteButton extends Component {
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Invoked to open {@code AddPeopleDialog}.
|
||||
*/
|
||||
dispatch: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Indicates if the "Add to call" feature is available.
|
||||
*/
|
||||
_isAddToCallAvailable: PropTypes.bool,
|
||||
enableAddPeople: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Indicates if the "Dial out" feature is available.
|
||||
*/
|
||||
_isDialOutAvailable: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The function opening the dialog.
|
||||
*/
|
||||
openDialog: PropTypes.func,
|
||||
enableDialOut: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
@@ -57,26 +47,8 @@ class InviteButton extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onInviteOptionSelected = this._onInviteOptionSelected.bind(this);
|
||||
this._updateInviteItems = this._updateInviteItems.bind(this);
|
||||
|
||||
this._updateInviteItems(this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillReceiveProps()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @param {Object} nextProps - The read-only props which this Component will
|
||||
* receive.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props._isDialOutAvailable !== nextProps._isDialOutAvailable
|
||||
|| this.props._isAddToCallAvailable
|
||||
!== nextProps._isAddToCallAvailable) {
|
||||
this._updateInviteItems(nextProps);
|
||||
}
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,144 +57,31 @@ class InviteButton extends Component {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
// HACK ALERT: Normally children should not be controlling their own
|
||||
// visibility; parents should control that. However, this component is
|
||||
// in a transitionary state while the Invite Dialog is being redone.
|
||||
// This hack will go away when the Invite Dialog is back.
|
||||
if (!this.state.buttonOption) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { VERTICAL_FILMSTRIP } = interfaceConfig;
|
||||
|
||||
return (
|
||||
<div className = 'filmstrip__invite'>
|
||||
<div className = 'invite-button-group'>
|
||||
<Button
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this.state.buttonOption.action }
|
||||
onClick = { this._onClick }
|
||||
shouldFitContainer = { true }>
|
||||
{ this.state.buttonOption.content }
|
||||
{ this.props.t('addPeople.invite') }
|
||||
</Button>
|
||||
{ this.state.inviteOptions[0].items.length
|
||||
? <DropdownMenu
|
||||
items = { this.state.inviteOptions }
|
||||
onItemActivated = { this._onInviteOptionSelected }
|
||||
position = { VERTICAL_FILMSTRIP
|
||||
? 'bottom right'
|
||||
: 'top right' }
|
||||
shouldFlip = { true }
|
||||
triggerType = 'button' />
|
||||
: null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles selection of the invite options.
|
||||
* Opens {@code AddPeopleDialog}.
|
||||
*
|
||||
* @param { Object } option - The invite option that has been selected from
|
||||
* the dropdown menu.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onInviteOptionSelected(option) {
|
||||
this.state.inviteOptions[0].items.forEach(item => {
|
||||
if (item.content === option.item.content) {
|
||||
item.action();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the invite items list depending on the availability of the
|
||||
* features.
|
||||
*
|
||||
* @param {Object} props - The read-only properties of the component.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateInviteItems(props) {
|
||||
const { INVITE_OPTIONS = [] } = interfaceConfig;
|
||||
const validOptions = INVITE_OPTIONS.filter(option =>
|
||||
(option === DIAL_OUT_OPTION && props._isDialOutAvailable)
|
||||
|| (option === ADD_TO_CALL_OPTION && props._isAddToCallAvailable));
|
||||
|
||||
/* eslint-disable array-callback-return */
|
||||
|
||||
const inviteItems = validOptions.map(option => {
|
||||
switch (option) {
|
||||
case DIAL_OUT_OPTION:
|
||||
return {
|
||||
content: this.props.t('dialOut.dialOut'),
|
||||
action: () => this.props.openDialog(DialOutDialog)
|
||||
};
|
||||
case ADD_TO_CALL_OPTION:
|
||||
return {
|
||||
content: interfaceConfig.ADD_PEOPLE_APP_NAME,
|
||||
action: () => this.props.openDialog(AddPeopleDialog)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/* eslint-enable array-callback-return */
|
||||
|
||||
const buttonOption = inviteItems[0];
|
||||
const dropdownOptions = inviteItems.splice(1, inviteItems.length);
|
||||
|
||||
const nextState = {
|
||||
/**
|
||||
* The configuration for how the invite button should display and
|
||||
* behave on click.
|
||||
*/
|
||||
buttonOption,
|
||||
|
||||
/**
|
||||
* The list of invite options in the dropdown.
|
||||
*/
|
||||
inviteOptions: [
|
||||
{
|
||||
items: dropdownOptions
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (this.state) {
|
||||
this.setState(nextState);
|
||||
} else {
|
||||
// eslint-disable-next-line react/no-direct-mutation-state
|
||||
this.state = nextState;
|
||||
}
|
||||
_onClick() {
|
||||
this.props.dispatch(openDialog(AddPeopleDialog, {
|
||||
enableAddPeople: this.props.enableAddPeople,
|
||||
enableDialOut: this.props.enableDialOut
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code InviteButton}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isAddToCallAvailable: boolean,
|
||||
* _isDialOutAvailable: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { enableUserRolesBasedOnToken } = state['features/base/config'];
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
|
||||
return {
|
||||
_isAddToCallAvailable:
|
||||
!isGuest && isInviteOptionEnabled(ADD_TO_CALL_OPTION),
|
||||
_isDialOutAvailable:
|
||||
getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR
|
||||
&& conference && conference.isSIPCallingSupported()
|
||||
&& isInviteOptionEnabled(DIAL_OUT_OPTION)
|
||||
&& (!enableUserRolesBasedOnToken || !isGuest)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, { openDialog })(
|
||||
InviteButton));
|
||||
export default translate(connect()(InviteButton));
|
||||
|
||||
@@ -7,15 +7,20 @@ import { i18next } from '../../../base/i18n';
|
||||
|
||||
import { DialInSummary } from '../dial-in-summary';
|
||||
|
||||
import NoRoomError from './NoRoomError';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const params = parseURLParams(window.location, true, 'search');
|
||||
const { room } = params;
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<DialInSummary
|
||||
className = 'dial-in-page'
|
||||
clickableNumbers = { false }
|
||||
room = { params.room } />
|
||||
{ room
|
||||
? <DialInSummary
|
||||
className = 'dial-in-page'
|
||||
clickableNumbers = { false }
|
||||
room = { params.room } />
|
||||
: <NoRoomError className = 'dial-in-page' /> }
|
||||
</I18nextProvider>,
|
||||
document.getElementById('react')
|
||||
);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
|
||||
/**
|
||||
* Displays an error message stating no room name was specified to fetch dial-in
|
||||
* numbers for.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class NoRoomError extends Component {
|
||||
/**
|
||||
* {@code NoRoomError} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Additional CSS classnames to append to the root of the component.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: PropTypes.func
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = { this.props.className } >
|
||||
<div>{ t('info.noNumbers') }</div>
|
||||
<div>{ t('info.noRoom') }</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(NoRoomError);
|
||||