Compare commits

..

34 Commits

Author SHA1 Message Date
Bettenbuk Zoltan
8400d01d75 [RN] Add color scheme support to dialog buttons 2019-03-12 12:36:15 +01:00
Bettenbuk Zoltan
d04068344a [RN] Make header button same size as header label 2019-03-12 12:36:15 +01:00
Bettenbuk Zoltan
55a971c0fd [RN] Add color scheme support to header 2019-03-12 12:36:15 +01:00
Bettenbuk Zoltan
20c1b1cfae [RN] Wrap PagedList navigator with SafeAreaView 2019-03-12 12:36:15 +01:00
Bettenbuk Zoltan
ecb44b6ab4 [RN] Make the header more compact 2019-03-12 12:36:15 +01:00
Jose Angel Gonzalez
039805eba3 fix(android-sdk): Recover audio device if the OS changes it 2019-03-12 09:55:51 +01:00
virtuacoplenny
22277ad799 feat(api): add notification for when filmstrip gets toggled (#3974) 2019-03-11 11:17:28 -07:00
Hristo Terezov
2af1e8da95 fix(remote-video-menu): Icon position 2019-03-11 17:36:58 +00:00
Дамян Минков
12d0aef686 Updates recording dialog. (#3953)
* Updates recording dialog.

* Update config.js doc.

* Adds comment and make a check more intuitive.

* Changes of using enum for recording types.
2019-03-11 09:17:21 -07:00
Hristo Terezov
f439ad2999 feat(popover): Make the popover menus customizable. 2019-03-11 16:04:20 +00:00
Leonard Kim
81d4f694b7 fix(chat): prevent empty messages 2019-03-09 12:09:29 +01:00
Saúl Ibarra Corretgé
c737d46d90 android: update gradle plugin version 2019-03-08 17:24:49 +01:00
Saúl Ibarra Corretgé
3f2a559d64 rn: now working on version 19.1 2019-03-08 14:04:03 +01:00
Saúl Ibarra Corretgé
bdb3099073 android: fix running on Android < M
The android.telecom.CallAudioState class was only added in API level 23 (Android
M), so make sure we don't import it in lower versioned devices.
2019-03-08 11:37:25 +01:00
damencho
b3a05db286 Update dropbox redirect uri to always use the main domain static page. 2019-03-07 15:39:16 +00:00
virtuacoplenny
08f2edf350 feat(screenshare): emit source type when starting screenshare (#3959)
* feat(screenshare): emit source type when starting screenshare

* squash: update doc
2019-03-06 21:46:17 -08:00
Bettenbuk Zoltan
98c7430b6f [RN] Replace red screen with dialog 2019-03-07 01:09:03 +01:00
Bettenbuk Zoltan
ebdcbe122a Change dialog button property keys 2019-03-07 01:09:03 +01:00
virtuacoplenny
31c1034be7 deps(chore): bump lib-jitsi-meet to e398584 (#3958)
Bring over two fixes for spot. One is for
identifying the screenshare type when using
a camera for screenshare or when using a proxy
stream. Also bring in a fix to avoid a js error
in chrome ios.
2019-03-06 15:39:01 -08:00
Hristo Terezov
a9d82a79ea fix(toolbar): Move buttons to overflow menu when the space isn't enough 2019-03-06 17:51:31 +00:00
Bettenbuk Zoltan
27e1f5a1bc [RN] Avoid adding undefined in the invite 2019-03-06 16:52:22 +01:00
virtuacoplenny
dbedee5e22 chore(deps): update to lib 320919e (#3951) 2019-03-05 17:02:22 -08:00
Дамян Минков
636c63397b Adds integrations doc. (#3929)
* Adds integrations doc.

Google, Microsoft and Dropbox for now.

* Updates doc.
2019-02-28 09:09:09 +00:00
damencho
67e7994e36 Removes unused config. 2019-02-27 16:45:26 +00:00
damencho
40f03fedc2 Replaces emoji flags with flag from a png.
Seems like windows does not have emojis for flags.
2019-02-27 16:45:26 +00:00
Hristo Terezov
55149670da fix(dropbox-auth): In Electron. 2019-02-27 16:12:54 +00:00
Gabriel-Tiberiu Imre-Lucaci
5739e1deaa feat(external_api): notify when api is disposed 2019-02-27 14:39:04 +00:00
Bettenbuk Zoltan
b6e2701991 [RN] Add invite screen 2019-02-27 13:26:21 +01:00
Bettenbuk Zoltan
38b1be1291 [RN] Extract AvatarListItem 2019-02-27 13:26:21 +01:00
damencho
555f8b3a99 Fixes toll free position. 2019-02-26 17:08:56 +00:00
Дамян Минков
ea4d49f2a0 Adds new format of phoneList service and re-design dial in numbers page. (#3903)
* Adds new format of phoneList service and re-design dial in numbers page.

Adds flags and country names (with translations) for the numbers if using the new format.

* Fixes tests and fixes get default number.

* Updates swagger with new format.

* Moves html back yo table.

Fixes displaying on mobile and also the tel: URI generation. The tel: URI is tested on Android and iOS and seems to work (Android was not interpreting 'p', but both seems to like ',').

* Fixes a wrong return statement.

* Small fixes.
2019-02-26 13:32:46 +00:00
fossterer
d7eea8abbc Added links related to NAT config in a new FAQ doc 2019-02-26 09:53:48 +00:00
paweldomas
1b8ef9a05a chore: update LJM to 5a9fc76739bcf0bed50676c7be160f688f3a19b5 2019-02-25 14:16:05 -06:00
Emil Ivov
ac7311cb52 Merge pull request #3920 from jitsi/emcho-patch-3
Update api.md
2019-02-23 17:04:13 +00:00
140 changed files with 3181 additions and 900 deletions

View File

@@ -115,16 +115,13 @@ gradle.projectsEvaluated {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processManifest.doLast {
def f = new File(manifestOutputDirectory, 'AndroidManifest.xml')
if (!f.isFile()) {
f = new File(new File(manifestOutputDirectory, output.dirName), 'AndroidManifest.xml')
}
if (f.exists()) {
def charset = 'UTF-8'
def s = f.getText(charset)
s = s.replace('</application>', "${dropboxActivity}</application>")
f.write(s, charset)
}
def outputDir = manifestOutputDirectory.get().asFile
def manifestPath = new File(outputDir, 'AndroidManifest.xml')
def charset = 'UTF-8'
def text
text = manifestPath.getText(charset)
text = text.replace('</application>', "${dropboxActivity}</application>")
manifestPath.write(text, charset)
}
}
}

View File

@@ -12,7 +12,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'io.fabric.tools:gradle:1.27.0'

View File

@@ -18,5 +18,5 @@
# org.gradle.parallel=true
buildNumber=1
appVersion=19.0.0
appVersion=19.1.0
sdkVersion=1.21.0

View File

@@ -1,6 +1,6 @@
#Wed Dec 19 12:02:47 CET 2018
#Fri Mar 08 13:36:51 CET 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

View File

@@ -107,13 +107,17 @@ android.libraryVariants.all { def variant ->
currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)
variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)
variant.mergeResources.dependsOn(currentBundleTask)
def assetsDir = variant.mergeAssets.outputDir
def mergeAssetsTask = variant.mergeAssetsProvider.get()
def mergeResourcesTask = variant.mergeResourcesProvider.get()
mergeAssetsTask.dependsOn(currentBundleTask)
mergeResourcesTask.dependsOn(currentBundleTask)
mergeAssetsTask.doLast {
def assetsDir = mergeAssetsTask.outputDir
variant.mergeAssets.doLast {
// Bundle fonts
//
copy {
@@ -139,19 +143,19 @@ android.libraryVariants.all { def variant ->
//
if (currentBundleTask.enabled) {
copy {
from(jsBundleDir)
from(jsBundleFile)
into(assetsDir)
}
}
}
variant.mergeResources.doLast {
mergeResourcesTask.doLast {
// Copy React resources
//
if (currentBundleTask.enabled) {
copy {
from(resourcesDir)
into(variant.mergeResources.outputDir)
into(mergeResourcesTask.outputDir)
}
}
}

View File

@@ -26,7 +26,6 @@ import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.telecom.CallAudioState;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
@@ -59,8 +58,7 @@ import java.util.concurrent.Executors;
* Before a call has started and after it has ended the
* {@code AudioModeModule.DEFAULT} mode should be used.
*/
class AudioModeModule
extends ReactContextBaseJavaModule
class AudioModeModule extends ReactContextBaseJavaModule
implements AudioManager.OnAudioFocusChangeListener {
/**
@@ -104,29 +102,29 @@ class AudioModeModule
/**
* Converts any of the "DEVICE_" constants into the corresponding
* {@link CallAudioState} "ROUTE_" number.
* {@link android.telecom.CallAudioState} "ROUTE_" number.
*
* @param audioDevice one of the "DEVICE_" constants.
* @return a route number {@link CallAudioState#ROUTE_EARPIECE} if no match
* is found.
* @return a route number {@link android.telecom.CallAudioState#ROUTE_EARPIECE} if
* no match is found.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static int audioDeviceToRouteInt(String audioDevice) {
if (audioDevice == null) {
return CallAudioState.ROUTE_EARPIECE;
return android.telecom.CallAudioState.ROUTE_EARPIECE;
}
switch (audioDevice) {
case DEVICE_BLUETOOTH:
return CallAudioState.ROUTE_BLUETOOTH;
return android.telecom.CallAudioState.ROUTE_BLUETOOTH;
case DEVICE_EARPIECE:
return CallAudioState.ROUTE_EARPIECE;
return android.telecom.CallAudioState.ROUTE_EARPIECE;
case DEVICE_HEADPHONES:
return CallAudioState.ROUTE_WIRED_HEADSET;
return android.telecom.CallAudioState.ROUTE_WIRED_HEADSET;
case DEVICE_SPEAKER:
return CallAudioState.ROUTE_SPEAKER;
return android.telecom.CallAudioState.ROUTE_SPEAKER;
default:
Log.e(TAG, "Unsupported device name: " + audioDevice);
return CallAudioState.ROUTE_EARPIECE;
return android.telecom.CallAudioState.ROUTE_EARPIECE;
}
}
@@ -134,25 +132,26 @@ class AudioModeModule
* Populates given route mask into the "DEVICE_" list.
*
* @param supportedRouteMask an integer coming from
* {@link CallAudioState#getSupportedRouteMask()}.
* {@link android.telecom.CallAudioState#getSupportedRouteMask()}.
* @return a list of device names.
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private static Set<String> routesToDeviceNames(int supportedRouteMask) {
Set<String> devices = new HashSet<>();
if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE)
== CallAudioState.ROUTE_EARPIECE) {
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_EARPIECE)
== android.telecom.CallAudioState.ROUTE_EARPIECE) {
devices.add(DEVICE_EARPIECE);
}
if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
== CallAudioState.ROUTE_BLUETOOTH) {
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_BLUETOOTH)
== android.telecom.CallAudioState.ROUTE_BLUETOOTH) {
devices.add(DEVICE_BLUETOOTH);
}
if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER)
== CallAudioState.ROUTE_SPEAKER) {
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_SPEAKER)
== android.telecom.CallAudioState.ROUTE_SPEAKER) {
devices.add(DEVICE_SPEAKER);
}
if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
== CallAudioState.ROUTE_WIRED_HEADSET) {
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_WIRED_HEADSET)
== android.telecom.CallAudioState.ROUTE_WIRED_HEADSET) {
devices.add(DEVICE_HEADPHONES);
}
return devices;
@@ -272,7 +271,7 @@ class AudioModeModule
/**
* Used on API >= 26 to store the most recently reported audio devices.
* Makes it easier to compare for a change, because the devices are stored
* as a mask in the {@link CallAudioState}. The mask is populated into
* as a mask in the {@link android.telecom.CallAudioState}. The mask is populated into
* the {@link #availableDevices} on each update.
*/
@RequiresApi(api = Build.VERSION_CODES.O)
@@ -433,7 +432,9 @@ class AudioModeModule
}
@RequiresApi(api = Build.VERSION_CODES.O)
void onCallAudioStateChange(final CallAudioState callAudioState) {
void onCallAudioStateChange(Object callAudioState_) {
final android.telecom.CallAudioState callAudioState
= (android.telecom.CallAudioState)callAudioState_;
runInAudioThread(new Runnable() {
@Override
public void run() {
@@ -456,6 +457,10 @@ class AudioModeModule
// Reset user selection
userSelectedDevice = null;
// If the OS changes the Audio Route or Devices we could have lost
// the selected audio device
selectedDevice = null;
if (mode != -1) {
updateAudioRoute(mode);
}

View File

@@ -1326,7 +1326,14 @@ export default {
this.isSharingScreen = newStream && newStream.videoType === 'desktop';
if (wasSharingScreen !== this.isSharingScreen) {
APP.API.notifyScreenSharingStatusChanged(this.isSharingScreen);
const details = {};
if (this.isSharingScreen) {
details.sourceType = newStream.sourceType;
}
APP.API.notifyScreenSharingStatusChanged(
this.isSharingScreen, details);
}
},

View File

@@ -174,7 +174,17 @@ var config = {
// Enable the dropbox integration.
// dropbox: {
// appKey: '<APP_KEY>' // Specify your app key here.
// // A URL to redirect the user to, after authenticating
// // by default uses:
// // 'https://jitsi-meet.example.com/static/oauth.html'
// redirectURI:
// 'https://jitsi-meet.example.com/subfolder/static/oauth.html'
// },
// When integrations like dropbox are enabled only that will be shown,
// by enabling fileRecordingsServiceEnabled, we show both the integrations
// and the generic recording service (its configuration and storage type
// depends on jibri configuration)
// fileRecordingsServiceEnabled: false
// Whether to enable live streaming or not.
// liveStreamingEnabled: false,
@@ -412,7 +422,6 @@ var config = {
externalConnectUrl
firefox_fake_device
googleApiApplicationClientID
googleApiIOSClientID
iAmRecorder
iAmSipGateway
microsoftApiApplicationClientID

View File

@@ -40,3 +40,11 @@
.videocontainer .tOoji {
background: none;
}
/**
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
* with the correct height.
*/
.toolbox-button-wth-dialog .eYJELv {
max-height: initial;
}

View File

@@ -25,6 +25,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-phone:before {
content: "\e0cd";
}
.icon-radio_button_unchecked:before {
content: "\e836";
}
.icon-radio_button_checked:before {
content: "\e837";
}
.icon-search:before {
content: "\e8b6";
}
.icon-chat-unread:before {
content: "\e0b7";
}

View File

@@ -41,3 +41,10 @@
top: -25px;
width: 100%;
}
.popover {
background-color: $popoverBg;
border-radius: 3px;
margin: -16px -24px;
padding: 16px 24px;
}

View File

@@ -5,16 +5,13 @@
.popupmenu {
min-width: 75px;
text-align: left;
padding: 0;
padding: 0px;
width: 150px;
white-space: nowrap;
&__item {
list-style-type: none;
height: 35px;
&:hover {
background-color: rgba(9, 30, 66, 0.04);
}
}
// Link Appearance
@@ -28,6 +25,12 @@
width: 100%;
cursor: pointer;
padding: 0 5px;
color: $popupMenuColor;
&:hover {
background-color: $popupMenuHoverBackground;
color: $popupMenuHoverColor;
}
&.disabled {
pointer-events: none;
@@ -62,6 +65,18 @@
top: 50%;
transform: translate(0, -50%);
width: 100%;
&::-webkit-slider-runnable-track {
background-color: $popupSliderColor;
}
&::-moz-range-track {
background-color: $popupSliderColor;
}
&::-ms-fill-lower {
background-color: $popupSliderColor;
}
}
}
}
@@ -91,7 +106,7 @@
* InlineDialogs.
*/
ul.popupmenu {
margin: -15px;
margin: -16px -24px;
}
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {

View File

@@ -11,42 +11,37 @@
flex: 0;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 32px;
.recording-title {
display: inline-flex;
align-items: center;
font-size: 16px;
font-weight: bold;
margin-left: 16px;
}
}
.recording-icon-container {
display: inline-flex;
align-items: center;
}
.recording-icon {
width: 32px;
height: 32px;
object-fit: contain;
}
.recording-switch {
margin-left: auto;
}
.authorization-panel {
display: flex;
flex-direction: column;
margin-bottom: 10px;
margin: 0 40px 10px 40px;
padding-bottom: 10px;
.dropbox-sign-in {
align-items: center;
border: 1px solid #4285f4;
background-color: white;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
padding: 10px;
font-size: 18px;
font-weight: 600;
margin: 10px 0px;
color: #4285f4;
.dropbox-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding-right: 5px;
height: 18px;
}
}
.logged-in-panel {
padding: 10px;
}

View File

@@ -190,6 +190,14 @@
cursor: initial;
color: #3b475c;
}
i.toggled {
background: inherit;
}
i.toggled:hover {
background: inherit;
}
}
.beta-tag {

View File

@@ -63,7 +63,7 @@ $audioLevelShadow: rgba(9, 36, 77, 0.9);
$videoStateIndicatorColor: $defaultColor;
$videoStateIndicatorBackground: $toolbarBackground;
$videoStateIndicatorSize: 40px;
$remoteVideoMenuIconLeft: initial;
$remoteVideoMenuIconMargin: initial;
/**
* Feedback Modal

View File

@@ -409,10 +409,10 @@
height: 13px;
color: #FFF;
font-size: 10pt;
margin-right: $remoteVideoMenuIconMargin;
>i{
cursor: hand;
margin-left: $remoteVideoMenuIconLeft;
}
}
.remote-video-menu-trigger {

View File

@@ -28,7 +28,7 @@
max-width: 40em;
padding: 35px 0 40px 0;
text-align: center;
width: 75%;
width: 90%;
a:active {
text-decoration: none;
@@ -46,7 +46,7 @@
&__text,
.deep-linking-dial-in {
font-size: 1.2em;
font-size: 1em;
line-height: em(29px, 21px);
margin-bottom: 0.65em;
@@ -59,6 +59,31 @@
font-size: em(21, 18);
}
}
table {
font-size: 1em;
}
.dial-in-conference-id {
margin: 10px 0 10px 0;
}
.dial-in-conference-description {
font-size: 0.8em;
}
.toll-free-list {
min-width: 80px;
}
.numbers-list {
min-width: 150px;
}
li.toll-free:empty:before {
content: '.';
visibility: hidden;
}
}
&__href {

View File

@@ -74,6 +74,7 @@
.remote-video-menu-trigger {
margin-bottom: 7px;
margin-left: $remoteVideoMenuIconMargin;
}
}

View File

@@ -18,6 +18,11 @@
/* Animations END */
/* Flags BEGIN */
$flagsImagePath: "/images/";
@import "../node_modules/bc-css-flags/dist/css/bc-css-flags.scss";
/* Flags END */
/* Fonts BEGIN */
@import 'font';

View File

@@ -66,7 +66,7 @@
}
.info-dialog-dial-in {
white-space: nowrap;
word-break: break-all;
.conference-id,
.phone-number {
@@ -77,6 +77,7 @@
.info-dialog-icon {
color: #6453C0;
font-size: 16px;
min-width: 30px;
}
.info-dialog-url-text,
@@ -124,29 +125,83 @@
}
}
.dial-in-numbers-list {
margin-top: 20px;
font-size: 12px;
line-height: 24px;
border-collapse: separate;
border-spacing: 0 5px;
thead {
text-align: left;
}
td,
th {
border-bottom: 1px solid #d1dbe8;
}
.flag-cell {
vertical-align: top;
width: 30px;
}
.flag {
display: block;
margin: 5px 5px 0px 5px;
}
.country {
font-weight: bold;
vertical-align: top;
padding: 0 20px 0 0;
}
ul {
padding: 0px 0px 0px 0px;
}
.numbers-list {
list-style: none;
padding: 0 20px 0 0;
}
.toll-free-list {
font-weight: bold;
list-style: none;
vertical-align: top;
}
li.toll-free:empty:before {
content: '.';
visibility: hidden;
}
}
.dial-in-page {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
font-size: 24px;
font-size: 12px;
max-height: 100%;
overflow: auto;
padding: 25px;
position: absolute;
transform: translateY(-50%);
top: 50%;
width: 100%;
.dial-in-numbers-list {
font-size: 24px;
margin-top: 20px;
}
.dial-in-conference-id {
text-align: center;
min-width: 200px;
width: 30%;
}
.dial-in-conference-name,
.dial-in-conference-pin {
font-size: 18px;
}
.dial-in-conference-description {
margin: 12px;
}
}

View File

@@ -80,9 +80,12 @@ $errorColor: #c61600;
$feedbackCancelFontColor: #333;
// Popover colors
$popoverBg: #000;
$popoverFontColor: #ffffff;
$popupMenuSelectedItemBackground: rgba(256, 256, 256, .2);
$popoverBg: initial;
$popoverFontColor: #ffffff !important;
$popupMenuColor: #ffffff !important;
$popupMenuHoverColor: #ffffff !important;
$popupMenuHoverBackground: rgba(255, 255, 255, 0.1);
$popupSliderColor: #0376da;
// Toolbar
$secondaryToolbarBg: rgba(0, 0, 0, 0.5);

12
debian/rules vendored
View File

@@ -3,12 +3,22 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
LANGUAGES := $(shell node -p "Object.keys(require('./lang/languages.json')).join(' ')")
COUNTRIES_DIR := node_modules/i18n-iso-countries/langs
%:
dh $@
# we skip making Makefile exists for updating browserify modules when developing
override_dh_auto_build:
override_dh_install:
override_dh_install: $(LANGUAGES)
dh_installdirs
dh_install -X/config.js -X/package.json
$(LANGUAGES):
if [ -f $(COUNTRIES_DIR)/$@.json ] ; \
then \
dh_install -pjitsi-meet-web $(COUNTRIES_DIR)/$@.json usr/share/jitsi-meet/lang/; \
mv debian/jitsi-meet-web/usr/share/jitsi-meet/lang/$@.json debian/jitsi-meet-web/usr/share/jitsi-meet/lang/countries-$@.json; \
fi;

View File

@@ -168,7 +168,14 @@ changes. The listener will receive an object with the following structure:
* **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure:
```javascript
{
"on": on //whether screen sharing is on
"on": on, //whether screen sharing is on
"details": {
// From where the screen sharing is capturing, if known. Values which are
// passed include "window", "screen", "proxy", "device". The value undefined
// will be passed if the source type is unknown or screen share is off.
sourceType: sourceType
}
}
```
@@ -207,6 +214,12 @@ changes. The listener will receive an object with the following structure:
"email": email // the new email
}
```
* **filmstripDisplayChanged** - event notifications about the visibility of the filmstrip being updated.
```javascript
{
"visible": visible, // Whether or not the filmstrip is displayed or hidden.
}
```
* **participantJoined** - event notifications about new participants who join the room. The listener will receive an object with the following structure:
```javascript
@@ -256,7 +269,7 @@ changes. The listener will receive an object with the following structure:
* **readyToClose** - event notification fired when Jitsi Meet is ready to be closed (hangup operations are completed).
* **subjectChange** - event notifications about subject of conference changes.
* **subjectChange** - event notifications about subject of conference changes.
The listener will receive an object with the following structure:
```javascript
{

View File

@@ -71,7 +71,7 @@ paths:
$ref: "#/definitions/ConferenceMapperDetails"
405:
description: "Invalid input"
/phoneNumberList:
get:
tags:
@@ -96,7 +96,7 @@ securityDefinitions:
name: "Authorization"
in: "header"
definitions:
ConferenceMapperRequest:
description: "Request to create or find a conference mapping"
type: "object"
@@ -114,7 +114,7 @@ definitions:
domain:
type: "string"
description: "Domain part of the conference. Used if 'conference' is not provided. Defaults to domain of the API endpoint. Used to generate a 'conference' value (search by conference)"
ConferenceMapperDetails:
description: "Conference mapping between conference JID and numeric ID"
type: "object"
@@ -126,22 +126,26 @@ definitions:
type: "string"
format: "JID"
description: "Full JID for the conference OR boolean false if no conference was found (search by ID)"
PhoneNumberList:
type: "object"
properties:
numbersEnabled:
type: "boolean"
description: "Control flag for Jitsi Meet user interface. Must be set to true for Jitsi Meet to display phone-in UI elements"
numbers:
description: "List of dial in numbers for the conference."
type: "array"
items:
type: "object"
description: "Keys are Country Names, each value is an array of phone numbers"
additionalProperties:
type: "array"
items:
properties:
countryCode:
type: "string"
format: "phone"
description: "ISO 3166-1 country code. Alpha-2 supported."
default:
type: "boolean"
description: "Whether this number is the default one to show. Optional."
formattedNumber:
type: "string"
description: "The formatted telephone number to show."
tollFree:
type: "boolean"
description: "Whether the number is toll free number."
externalDocs:
description: "Find out more about the Jitsi Cloud API"
url: "https://jitsi.org/CloudAPI"
url: "https://jitsi.org/CloudAPI"

7
doc/faq.md Normal file
View File

@@ -0,0 +1,7 @@
**1. How to tell if my server instance is behind NAT?**
A. In general, if the tool ifconfig (or ipconfig) shows the assigned IP address to be some local address (10.x.x.x or 192.x.x.x) but you know that its public IP address is different from that, the server is most probably behind NAT
**2. Clients could communicate well in room created at meet.jit.si . The same clients still could connect to my self-hosted instance but can neither hear nor see one another. What's wrong?**
A. Most probably, the server is behind NAT. See this [resolved question](https://community.jitsi.org/t/cannot-see-video-or-hear-audio-on-self-hosted-instance/). You need to follow the steps detailed [here](https://github.com/jitsi/ice4j/blob/master/doc/quick-install.md#Advanced-configuration)

38
doc/integrations.md Normal file
View File

@@ -0,0 +1,38 @@
Document describing enabling various jitsi-meet integrations.
## Creating the Google API client for Google Calendar and Youtube integration
1. Log into a Google admin account.
1. Go to Google cloud platform dashboard. https://console.cloud.google.com/apis/dashboard
1. In the Select a Project dropdown, click New Project.
1. Give the project a name.
1. Proceed to the Credentials settings of the new project.
1. In the Credentials tab of the Credentials settings, click Create Credentials and select the type OAuth client ID.
1. Proceed with creating a Web application and add the domains (origins) on which the application will be hosted. Local development environments (http://localhost:8000 for example) can be added here.
1. While still in the Google cloud platform dashboard, click the Library settings for the calendar project.
1. Search for the Google Calendar API (used for calendar accessing), click its result, and enable it.
1. Do the same for YouTube Data API v3
## Creating the Microsoft app for Microsoft Outlook integration
1. Go to https://apps.dev.microsoft.com/
1. Proceed through the "Add an app" flow. Once created, a page with several Graph Permissions fields should display.
1. Under "Platforms" add "Web"
1. Add a redirect URL for the Microsoft auth flow to visit once a user has confirmed authentication. Target domain if available is just 'yourdomain.com' (the deployment address) and the redirect URL is `https://yourdomain.com/static/msredirect.html`.
1. Add Microsoft Graph delegated permissions, if this option is available: Calendars.Read, Calendars.ReadWrite, Calendars.Read.Shared, Calendars.ReadWrite.Shared.
1. Check `Allow Implicit Flow` (and `Restrict token issuing to this app` if available).
1. Save the changes.
## Creating the Dropbox app for Dropbox recording integration
1. You need a Dropbox account (If you don't already have one, you can sign up for a free account [here](https://www.dropbox.com/register).)
1. Create new App as described in [Getting Started Guide](https://www.dropbox.com/developers/reference/getting-started?_tk=guides_lp&_ad=guides2&_camp=get_started#app%20console) in App Console section.
1. Choose
1. 'Dropbox API - For apps that need to access files in Dropbox.'
1. 'App folder Access to a single folder created specifically for your app.'
1. Fill in the name of your app
1. You need only, the newly created App key, goes in config.js in
```
dropbox: {
appKey: '__dropbox_app_key__'
}
```
1. Add your Redirect URIs in the form `https://yourdeployment.com//static/oauth.html`
1. Fill in Branding

View File

@@ -55,7 +55,7 @@ Simply run the following in your shell
```
#### Advanced configuration
If installation is on a machine behind NAT further configuration of jitsi-videobridge is needed in order for it to be accessible.
If installation is on a machine [behind NAT](https://github.com/jitsi/jitsi-meet/blob/master/doc/faq.md) further configuration of jitsi-videobridge is needed in order for it to be accessible.
Provided that all required ports are routed (forwarded) to the machine that it runs on. By default these ports are (TCP/443 or TCP/4443 and UDP 10000).
The following extra lines need to be added the file `/etc/jitsi/videobridge/sip-communicator.properties`:
```

Binary file not shown.

View File

@@ -8,6 +8,7 @@
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" d="" />
<glyph unicode="&#xe0b7;" glyph-name="chat-unread" d="M768 682v86h-512v-86h512zM598 426v86h-342v-86h342zM256 640v-86h512v86h-512zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
<glyph unicode="&#xe0cd;" glyph-name="phone" d="M282 564c62-120 162-220 282-282l94 94c12 12 30 16 44 10 48-16 100-24 152-24 24 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-44z" />
<glyph unicode="&#xe145;" glyph-name="invite" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
<glyph unicode="&#xe146;" glyph-name="add" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
<glyph unicode="&#xe1aa;" glyph-name="bluetooth" d="M550 328l-80 82v-162zM470 776v-162l80 82zM670 696l-184-184 184-184-244-242h-42v324l-196-196-60 60 238 238-238 238 60 60 196-196v324h42zM834 738c40-64 62-142 62-222 0-84-24-160-66-226l-50 50c26 52 42 110 42 172s-16 120-42 172zM608 512l98 98c12-30 20-64 20-98s-8-70-20-100z" />
@@ -21,8 +22,11 @@
<glyph unicode="&#xe616;" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
<glyph unicode="&#xe61d;" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
<glyph unicode="&#xe80b;" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe836;" glyph-name="radio_button_unchecked" d="M512 170c188 0 342 154 342 342s-154 342-342 342-342-154-342-342 154-342 342-342zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe837;" glyph-name="radio_button_checked" d="M512 170c188 0 342 154 342 342s-154 342-342 342-342-154-342-342 154-342 342-342zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426zM512 726c118 0 214-96 214-214s-96-214-214-214-214 96-214 214 96 214 214 214z" />
<glyph unicode="&#xe89e;" glyph-name="open_in_new" d="M598 896h298v-298h-86v152l-418-418-60 60 418 418h-152v86zM810 214v298h86v-298c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h298v-86h-298v-596h596z" />
<glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
<glyph unicode="&#xe8b6;" glyph-name="search" d="M406 426c106 0 192 86 192 192s-86 192-192 192-192-86-192-192 86-192 192-192zM662 426l212-212-64-64-212 212v34l-12 12c-48-42-112-66-180-66-154 0-278 122-278 276s124 278 278 278 276-124 276-278c0-68-24-132-66-180l12-12h34z" />
<glyph unicode="&#xe900;" glyph-name="AUD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
<glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
<glyph unicode="&#xe904;" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
images/dropboxLogo_square.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/flags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
images/flags@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
images/jitsiLogo_square.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>19.0.0</string>
<string>19.1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -344,10 +344,11 @@
"cancelPassword": "Cancel password",
"conferenceURL": "Link:",
"country": "Country",
"dialANumber": "To join your meeting, dial one of these numbers and then enter this PIN: __conferenceID__#",
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
"dialInNotSupported": "Sorry, dialing in is currently not supported.",
"dialInNumber": "Dial-in:",
"dialInTollFree": "Toll Free",
"genericError": "Whoops, something went wrong.",
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
@@ -361,7 +362,18 @@
"numbers": "Dial-in Numbers",
"password": "Password:",
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting"
"tooltip": "Share link and dial-in info for this meeting",
"label": "Meeting info"
},
"inviteDialog": {
"alertOk": "Ok",
"alertText": "Failed to invite some participants.",
"alertTitle": "Invite",
"header": "Invite",
"searchCallOnlyPlaceholder": "Enter phone number",
"searchPeopleOnlyPlaceholder": "Search for participants",
"searchPlaceholder": "Participant or phone number",
"send": "Send"
},
"inlineDialogFailure": {
"msg": "We stumbled a bit.",
@@ -506,9 +518,10 @@
"on": "Recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"serviceDescription": "Your recording will be saved by the recording service",
"serviceName": "Recording service",
"signIn": "sign in",
"signOut": "Sign Out",
"signIn": "Sign in",
"signOut": "Sign out",
"startRecordingBody": "Are you sure you would like to start recording?",
"unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
@@ -620,11 +633,14 @@
"callQuality": "Manage call quality",
"cameraDisabled": "Camera is not available",
"chat": "Open / Close chat",
"closeChat": "Close chat",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"etherpad": "Open / Close shared document",
"exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"filmstrip": "Show / Hide videos",
"fullscreen": "View / Exit full screen",
@@ -633,13 +649,16 @@
"lock": "Lock / Unlock room",
"login": "Login",
"logout": "Logout",
"lowerYourHand": "Lower your hand",
"micDisabled": "Microphone is not available",
"micMutedPopup": "Your microphone has been muted so that you would fully enjoy your shared video.",
"moreActions": "More actions",
"mute": "Mute / Unmute",
"openChat": "Open chat",
"pip": "Enter Picture-in-Picture mode",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",
"Settings": "Settings",
"sharedvideo": "Share a YouTube video",
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
@@ -647,6 +666,10 @@
"shortcuts": "View shortcuts",
"sip": "Call SIP number",
"speakerStats": "Speaker stats",
"startScreenSharing": "Start screen sharing",
"startSubtitles": "Start subtitles",
"stopScreenSharing": "Stop screen sharing",
"stopSubtitles": "Stop subtitles",
"stopSharedVideo": "Stop YouTube video",
"talkWhileMutedPopup": "Trying to speak? You are muted.",
"tileViewToggle": "Toggle tile view",
@@ -655,7 +678,7 @@
"videomute": "Start / Stop camera"
},
"transcribing": {
"ccButtonTooltip": "Start / Stop showing subtitles",
"ccButtonTooltip": "Start / Stop subtitles",
"error": "Transcribing failed. Please try again.",
"expandedLabel": "Transcribing is currently on",
"failedToStart": "Transcribing failed to start",

View File

@@ -551,17 +551,37 @@ class API {
this._sendEvent({ name: 'feedback-prompt-displayed' });
}
/**
* Notify external application (if API is enabled) that the display
* configuration of the filmstrip has been changed.
*
* @param {boolean} visible - Whether or not the filmstrip has been set to
* be displayed or hidden.
* @returns {void}
*/
notifyFilmstripDisplayChanged(visible: boolean) {
this._sendEvent({
name: 'filmstrip-display-changed',
visible
});
}
/**
* Notify external application (if API is enabled) that the screen sharing
* has been turned on/off.
*
* @param {boolean} on - True if screen sharing is enabled.
* @param {Object} details - Additional information about the screen
* sharing.
* @param {string} details.sourceType - Type of device or window the screen
* share is capturing.
* @returns {void}
*/
notifyScreenSharingStatusChanged(on: boolean) {
notifyScreenSharingStatusChanged(on: boolean, details: Object) {
this._sendEvent({
name: 'screen-sharing-status-changed',
on
on,
details
});
}

View File

@@ -4,5 +4,11 @@
"url": "accounts.google.com"
},
"target": "electron"
},
"dropbox-auth": {
"matchPatterns": {
"url": "dropbox.com/oauth2/authorize"
},
"target": "electron"
}
}

View File

@@ -44,6 +44,7 @@ const events = {
'email-change': 'emailChange',
'feedback-submitted': 'feedbackSubmitted',
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
'filmstrip-display-changed': 'filmstripDisplayChanged',
'incoming-message': 'incomingMessage',
'outgoing-message': 'outgoingMessage',
'participant-joined': 'participantJoined',
@@ -534,6 +535,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* @returns {void}
*/
dispose() {
this.emit('_willDispose');
this._transport.dispose();
this.removeAllListeners();
if (this._frame) {

22
package-lock.json generated
View File

@@ -3298,6 +3298,11 @@
"integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=",
"dev": true
},
"bc-css-flags": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bc-css-flags/-/bc-css-flags-3.0.0.tgz",
"integrity": "sha1-OJWiPppx+VgE6u8V8WXVG5rV4hM="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@@ -7494,6 +7499,11 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"i18n-iso-countries": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-3.7.8.tgz",
"integrity": "sha512-NkT3lRiw7D4kKtSAVjVdHCvGlc2UOe0ALKa9IfEx0LkEDf0q3YgjP/veVk0d/OZ7yqUNzV8aJP4lJc6RPj++Gw=="
},
"i18next": {
"version": "8.4.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-8.4.3.tgz",
@@ -8456,8 +8466,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#2e1436e20d4d8fb6020497a87b2714dff38a6c86",
"from": "github:jitsi/lib-jitsi-meet#2e1436e20d4d8fb6020497a87b2714dff38a6c86",
"version": "github:jitsi/lib-jitsi-meet#e39858418724c59bfef8b4e1ba16bb11f36abdc0",
"from": "github:jitsi/lib-jitsi-meet#e39858418724c59bfef8b4e1ba16bb11f36abdc0",
"requires": {
"@jitsi/sdp-interop": "0.1.13",
"@jitsi/sdp-simulcast": "0.2.1",
@@ -8467,7 +8477,7 @@
"js-utils": "github:jitsi/js-utils#446497893023aa8dec403e0e4e35a22cae6bc87d",
"lodash.isequal": "4.5.0",
"sdp-transform": "2.3.0",
"strophe.js": "1.2.15",
"strophe.js": "1.2.16",
"strophejs-plugin-disco": "0.0.2",
"webrtc-adapter": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a",
"yaeti": "1.0.1"
@@ -13929,9 +13939,9 @@
"dev": true
},
"strophe.js": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.2.15.tgz",
"integrity": "sha512-aM5SCLltSLKubPNil28ieJ03I+15jcVX02c1/7SBVIUWRfwfxwondRJSMJpB7OBss5b3jCNxpTqig8nXncJ5yg=="
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-1.2.16.tgz",
"integrity": "sha512-r/Uq7aqrusg25Y0qHwV48cFnMY6K/CuZdGt3EggRx3kY4sMv8lG+AFoMlrmTcYVMG1BaJvQfv9Cthw4Ll8z7fQ=="
},
"strophejs-plugin-disco": {
"version": "0.0.2",

View File

@@ -36,7 +36,9 @@
"@microsoft/microsoft-graph-client": "1.1.0",
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"bc-css-flags": "3.0.0",
"dropbox": "4.0.9",
"i18n-iso-countries": "3.7.8",
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",
"i18next-xhr-backend": "1.4.2",
@@ -50,7 +52,7 @@
"jsc-android": "224109.1.0",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2e1436e20d4d8fb6020497a87b2714dff38a6c86",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#e39858418724c59bfef8b4e1ba16bb11f36abdc0",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.11",
"moment": "2.19.4",

View File

@@ -338,18 +338,40 @@ export function createProfilePanelButtonEvent(buttonName, attributes = {}) {
* @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop').
* @param {string} buttonName - The name of the button (e.g. 'confirm' or
* 'cancel').
* @param {Object} attributes - Attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRecordingDialogEvent(dialogName, buttonName) {
export function createRecordingDialogEvent(
dialogName, buttonName, attributes = {}) {
return {
action: 'clicked',
actionSubject: buttonName,
attributes,
source: `${dialogName}.recording.dialog`,
type: TYPE_UI
};
}
/**
* Creates an event which indicates that a specific button on one of the
* liveStreaming-related dialogs was clicked.
*
* @param {string} dialogName - The name of the dialog (e.g. 'start' or 'stop').
* @param {string} buttonName - The name of the button (e.g. 'confirm' or
* 'cancel').
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createLiveStreamingDialogEvent(dialogName, buttonName) {
return {
action: 'clicked',
actionSubject: buttonName,
source: `${dialogName}.liveStreaming.dialog`,
type: TYPE_UI
};
}
/**
* Creates an event which indicates that an action related to recording has
* occured.

View File

@@ -14,9 +14,18 @@ export default {
'Dialog': {
background: ColorPalette.blackBlue,
border: getRGBAFormat(ColorPalette.white, 0.2),
buttonBackground: ColorPalette.blue,
buttonLabel: ColorPalette.white,
icon: ColorPalette.white,
text: ColorPalette.white
},
'Header': {
background: ColorPalette.blue,
icon: ColorPalette.white,
statusBar: ColorPalette.blueHighlight,
statusBarContent: ColorPalette.white,
text: ColorPalette.white
},
'LargeVideo': {
background: ColorPalette.black
},

View File

@@ -67,12 +67,12 @@ class BaseSubmitDialog<P: Props, S: *> extends BaseDialog<P, S> {
disabled = { this.props.okDisabled }
onPress = { this._onSubmit }
style = { [
brandedDialog.button,
_dialogStyles.button,
additionalButtons
? null : brandedDialog.buttonFarLeft,
brandedDialog.buttonFarRight
] }>
<Text style = { _dialogStyles.text }>
<Text style = { _dialogStyles.buttonLabel }>
{ t(this._getSubmitButtonKey()) }
</Text>
</TouchableOpacity>

View File

@@ -43,7 +43,7 @@ class ConfirmDialog extends BaseSubmitDialog<Props, *> {
* @returns {string}
*/
_getSubmitButtonKey() {
return 'dialog.confirmYes';
return this.props.okKey || 'dialog.confirmYes';
}
_onCancel: () => void;
@@ -57,18 +57,18 @@ class ConfirmDialog extends BaseSubmitDialog<Props, *> {
* @inheritdoc
*/
_renderAdditionalButtons() {
const { _dialogStyles, t } = this.props;
const { _dialogStyles, cancelKey, t } = this.props;
return (
<TouchableOpacity
onPress = { this._onCancel }
style = { [
brandedDialog.button,
_dialogStyles.button,
brandedDialog.buttonFarLeft,
_dialogStyles.buttonSeparator
] }>
<Text style = { _dialogStyles.text }>
{ t('dialog.confirmNo') }
<Text style = { _dialogStyles.buttonLabel }>
{ t(cancelKey || 'dialog.confirmNo') }
</Text>
</TouchableOpacity>
);
@@ -80,6 +80,10 @@ class ConfirmDialog extends BaseSubmitDialog<Props, *> {
* @inheritdoc
*/
_renderSubmittable() {
if (this.props.children) {
return this.props.children;
}
const { _dialogStyles, contentKey, t } = this.props;
const content
= typeof contentKey === 'string'

View File

@@ -95,11 +95,11 @@ class InputDialog extends BaseDialog<Props, State> {
disabled = { okDisabled }
onPress = { this._onSubmitValue }
style = { [
brandedDialog.button,
_dialogStyles.button,
brandedDialog.buttonFarLeft,
brandedDialog.buttonFarRight
] }>
<Text style = { _dialogStyles.text }>
<Text style = { _dialogStyles.buttonLabel }>
{ t('dialog.Ok') }
</Text>
</TouchableOpacity>

View File

@@ -66,12 +66,6 @@ export const brandedDialog = createStyleSheet({
fontWeight: 'bold'
},
button: {
backgroundColor: ColorPalette.blue,
flex: 1,
padding: BoxModel.padding * 1.5
},
buttonFarLeft: {
borderBottomLeftRadius: BORDER_RADIUS
},
@@ -185,6 +179,12 @@ ColorSchemeRegistry.register('BottomSheet', {
* Color schemed styles for all the component based on the abstract dialog.
*/
ColorSchemeRegistry.register('Dialog', {
button: {
backgroundColor: schemeColor('buttonBackground'),
flex: 1,
padding: BoxModel.padding * 1.5
},
/**
* Separator line for the buttons in a dialog.
*/
@@ -193,6 +193,12 @@ ColorSchemeRegistry.register('Dialog', {
borderRightWidth: 1
},
buttonLabel: {
color: schemeColor('buttonLabel'),
fontSize: MD_FONT_SIZE,
textAlign: 'center'
},
/**
* Style of the close icon on a dialog.
*/

View File

@@ -239,7 +239,7 @@ class StatelessDialog extends Component<Props> {
key = 'cancel'
onClick = { this._onCancel }
type = 'button'>
{ t(this.props.cancelTitleKey || 'dialog.Cancel') }
{ t(this.props.cancelKey || 'dialog.Cancel') }
</Button>
);
}
@@ -268,7 +268,7 @@ class StatelessDialog extends Component<Props> {
key = 'submit'
onClick = { this._onSubmit }
type = 'button'>
{ t(this.props.okTitleKey || 'dialog.Ok') }
{ t(this.props.okKey || 'dialog.Ok') }
</Button>
);
}

View File

@@ -10,7 +10,7 @@ export type DialogProps = {
/**
* Optional i18n key to change the cancel button title.
*/
cancelTitleKey: string,
cancelKey: string,
/**
* The React {@code Component} children which represents the dialog's body.
@@ -25,7 +25,7 @@ export type DialogProps = {
/**
* Optional i18n key to change the ok button title.
*/
okTitleKey: string,
okKey: string,
/**
* The handler for onCancel event.

View File

@@ -13,7 +13,7 @@ import { translate as reactI18nextTranslate } from 'react-i18next';
export function translate(component, options = { wait: true }) {
// Use the default list of namespaces.
return (
reactI18nextTranslate([ 'main', 'languages' ], options)(
reactI18nextTranslate([ 'main', 'languages', 'countries' ], options)(
component));
}

View File

@@ -3,6 +3,7 @@
import i18next from 'i18next';
import I18nextXHRBackend from 'i18next-xhr-backend';
import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json';
import LANGUAGES_RESOURCES from '../../../../lang/languages.json';
import MAIN_RESOURCES from '../../../../lang/main.json';
@@ -51,7 +52,7 @@ const options = {
load: 'unspecific',
ns: {
defaultNs: 'main',
namespaces: [ 'main', 'languages' ]
namespaces: [ 'main', 'languages', 'countries' ]
},
resGetPath: 'lang/__ns__-__lng__.json',
useDataAttrOptions: true
@@ -68,6 +69,12 @@ i18next
.init(options);
// Add default language which is preloaded from the source code.
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'countries',
COUNTRIES_RESOURCES,
/* deep */ true,
/* overwrite */ true);
i18next.addResourceBundle(
DEFAULT_LANGUAGE,
'languages',

View File

@@ -7,6 +7,11 @@ import type { ComponentType, Element } from 'react';
*/
export type Item = {
/**
* The avatar URL or icon name.
*/
avatar: ?string,
/**
* the color base of the avatar
*/

View File

@@ -1 +1,3 @@
// @flow
export * from './native';

View File

@@ -1,3 +1,5 @@
// @flow
export * from './_';
export { default as AbstractPage } from './AbstractPage';
export { default as NavigateSectionList } from './NavigateSectionList';

View File

@@ -0,0 +1,203 @@
// @flow
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Icon } from '../../../font-icons';
import { Avatar } from '../../../participants';
import { StyleType } from '../../../styles';
import { type Item } from '../../Types';
import Container from './Container';
import styles, { AVATAR_SIZE, UNDERLAY_COLOR } from './styles';
type Props = {
/**
* Preferred size of the avatar.
*/
avatarSize?: number,
/**
* External style to be applied to the avatar (icon).
*/
avatarStyle?: StyleType,
/**
* External style to be applied to the avatar (text).
*/
avatarTextStyle?: StyleType,
/**
* Children of the component.
*/
children?: ?React$Element<*>,
/**
* item containing data to be rendered
*/
item: Item,
/**
* External style prop to be applied to the extra lines.
*/
linesStyle?: StyleType,
/**
* Function to invoke on press.
*/
onPress: ?Function,
/**
* External style prop to be applied to the title.
*/
titleStyle?: StyleType
};
/**
* Implements a list item with an avatar rendered for it.
*/
export default class AvatarListItem extends Component<Props> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._renderItemLine = this._renderItemLine.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const {
avatarSize = AVATAR_SIZE,
avatarStyle,
avatarTextStyle
} = this.props;
const { avatar, colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase),
...avatarStyle,
borderRadius: avatarSize / 2,
height: avatarSize,
width: avatarSize
};
const isAvatarURL = Boolean(avatar && avatar.match(/^http[s]*:\/\//i));
return (
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
{
isAvatarURL && <Avatar
size = { avatarSize }
uri = { avatar } />
}
{
Boolean(avatar && !isAvatarURL) && <Icon
name = { avatar } />
}
{
!avatar && <Text
style = { [
styles.avatarContent,
avatarTextStyle
] }>
{ title.substr(0, 1).toUpperCase() }
</Text>
}
</Container>
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle,
this.props.titleStyle
] }>
{ title }
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.children }
</Container>
);
}
/**
* 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}`];
}
_renderItemLine: (string, number) => React$Node;
/**
* Renders a single line from the additional lines.
*
* @param {string} line - The line text.
* @param {number} index - The index of the line.
* @private
* @returns {React$Node}
*/
_renderItemLine(line, index) {
if (!line) {
return null;
}
return (
<Text
key = { index }
numberOfLines = { 1 }
style = { [
styles.listItemText,
this.props.linesStyle
] }>
{line}
</Text>
);
}
_renderItemLines: Array<string> => Array<React$Node>;
/**
* Renders the additional item lines, if any.
*
* @param {Array<string>} lines - The lines to render.
* @private
* @returns {Array<React$Node>}
*/
_renderItemLines(lines) {
return lines && lines.length ? lines.map(this._renderItemLine) : null;
}
}

View File

@@ -2,11 +2,11 @@
import React, { Component } from 'react';
import { TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { Icon } from '../../../font-icons';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link BackButton}
*/
@@ -20,13 +20,18 @@ type Props = {
/**
* An external style object passed to the component.
*/
style?: Object
style?: Object,
/**
* The color schemed style of the Header component.
*/
_headerStyles: Object
};
/**
* A component rendering a back button.
*/
export default class BackButton extends Component<Props> {
class BackButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
@@ -41,10 +46,26 @@ export default class BackButton extends Component<Props> {
<Icon
name = { 'arrow_back' }
style = { [
styles.headerButton,
this.props._headerStyles.headerButtonIcon,
this.props.style
] } />
</TouchableOpacity>
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default connect(_mapStateToProps)(BackButton);

View File

@@ -0,0 +1,44 @@
/* @flow */
import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
type Props = {
/**
* React Elements to display within the component.
*/
children: React$Node | Object,
/**
* Handler called when the user presses the button.
*/
onValueChange: Function,
/**
* The component's external style
*/
style: Object
};
/**
* Renders a button.
*/
export default class ButtonImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<TouchableOpacity
onPress = { this.props.onValueChange } >
<Text style = { this.props.style }>
{ this.props.children }
</Text>
</TouchableOpacity>
);
}
}

View File

@@ -0,0 +1,91 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { translate } from '../../../i18n';
/**
* The type of the React {@code Component} props of {@link ForwardButton}
*/
type Props = {
/**
* True if the nutton should be disabled.
*/
disabled: boolean;
/**
* The i18n label key of the button.
*/
labelKey: string,
/**
* The action to be performed when the button is pressed.
*/
onPress: Function,
/**
* An external style object passed to the component.
*/
style?: Object,
/**
* The function to be used to translate i18n labels.
*/
t: Function,
/**
* The color schemed style of the Header component.
*/
_headerStyles: Object
};
/**
* A component rendering a forward/next/action button.
*/
class ForwardButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _headerStyles } = this.props;
return (
<TouchableOpacity
accessibilityLabel = { 'Forward' }
disabled = { this.props.disabled }
onPress = { this.props.onPress } >
<Text
style = { [
_headerStyles.headerButtonText,
this.props.disabled && _headerStyles.disabledButtonText,
this.props.style
] }>
{ this.props.t(this.props.labelKey) }
</Text>
</TouchableOpacity>
);
}
}
/**
* Maps part of the Redux state to the props of the component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default translate(connect(_mapStateToProps)(ForwardButton));

View File

@@ -2,14 +2,24 @@
import React, { Component, type Node } from 'react';
import { Platform, SafeAreaView, StatusBar, View } from 'react-native';
import { connect } from 'react-redux';
import styles, { HEADER_PADDING, STATUSBAR_COLOR } from './styles';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { isDarkColor } from '../../../styles';
import { HEADER_PADDING } from './headerstyles';
/**
* Compatibility header padding size for iOS 10 (and older) devices.
*/
const IOS10_PADDING = 20;
/**
* Constanst for the (currently) supported statusbar colors.
*/
const STATUSBAR_DARK = 'dark-content';
const STATUSBAR_LIGHT = 'light-content';
/**
* The type of the React {@code Component} props of {@link Header}
*/
@@ -23,43 +33,18 @@ type Props = {
/**
* The component's external style
*/
style: Object
style: Object,
/**
* The color schemed style of the component.
*/
_styles: Object
}
/**
* A generic screen header component.
*/
export default class Header extends Component<Props> {
/**
* The style of button-like React {@code Component}s rendered in
* {@code Header}.
*
* @returns {Object}
*/
static get buttonStyle(): Object {
return styles.headerButton;
}
/**
* The style of a React {@code Component} rendering a {@code Header} as its
* child.
*
* @returns {Object}
*/
static get pageStyle(): Object {
return styles.page;
}
/**
* The style of text rendered in {@code Header}.
*
* @returns {Object}
*/
static get textStyle(): Object {
return styles.headerText;
}
class Header extends Component<Props> {
/**
* Initializes a new {@code Header} instance.
*
@@ -78,20 +63,22 @@ export default class Header extends Component<Props> {
* @inheritdoc
*/
render() {
const { _styles } = this.props;
return (
<View
style = { [
styles.headerOverlay,
_styles.headerOverlay,
this._getIOS10CompatiblePadding()
] } >
<StatusBar
backgroundColor = { STATUSBAR_COLOR }
barStyle = 'light-content'
backgroundColor = { _styles.statusBar }
barStyle = { this._getStatusBarContentColor() }
translucent = { false } />
<SafeAreaView>
<View
style = { [
styles.screenHeader,
_styles.screenHeader,
this.props.style
] }>
{
@@ -128,4 +115,54 @@ export default class Header extends Component<Props> {
return null;
}
/**
* Calculates the color of the statusbar content (light or dark) based on
* certain criterias.
*
* @returns {string}
*/
_getStatusBarContentColor() {
const { _styles } = this.props;
const { statusBarContent } = _styles;
if (statusBarContent) {
// We have the possibility to define the statusbar color in the
// color scheme feature, but since mobile devices (at the moment)
// only support two colors (light and dark) we need to normalize
// the value.
if (isDarkColor(statusBarContent)) {
return STATUSBAR_DARK;
}
return STATUSBAR_LIGHT;
}
// The statusbar color is not defined, so we need to base our choice
// on the header colors
const { statusBar, screenHeader } = _styles;
if (isDarkColor(statusBar || screenHeader.backgroundColor)) {
return STATUSBAR_LIGHT;
}
return STATUSBAR_DARK;
}
}
/**
* Maps part of the Redux state to the props of the component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _styles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default connect(_mapStateToProps)(Header);

View File

@@ -1,12 +1,12 @@
// @flow
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { translate } from '../../../i18n';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link HeaderLabel}
*/
@@ -20,7 +20,12 @@ type Props = {
/**
* The i18n translate function.
*/
t: Function
t: Function,
/**
* The color schemed style of the Header component.
*/
_headerStyles: Object
};
/**
@@ -34,15 +39,35 @@ class HeaderLabel extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _headerStyles } = this.props;
return (
<Text
style = { [
styles.headerText
] }>
{ this.props.t(this.props.labelKey) }
</Text>
<View
pointerEvents = 'box-none'
style = { _headerStyles.headerTextWrapper }>
<Text
style = { [
_headerStyles.headerText
] }>
{ this.props.t(this.props.labelKey) }
</Text>
</View>
);
}
}
export default translate(HeaderLabel);
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default translate(connect(_mapStateToProps)(HeaderLabel));

View File

@@ -0,0 +1,41 @@
// @flow
import React, { Component } from 'react';
import { Image } from 'react-native';
/**
* The type of the React {@code Component} props of {@link Image}.
*/
type Props = {
/**
* The URL to be rendered as image.
*/
src: string,
/**
* The component's external style
*/
style: Object
};
/**
* A component rendering aN IMAGE.
*
* @extends Component
*/
export default class ImageImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Image
source = { this.props.src }
style = { this.props.style } />
);
}
}

View File

@@ -7,9 +7,11 @@ import { ColorPalette } from '../../../styles';
import Container from './Container';
import Text from './Text';
import styles, { UNDERLAY_COLOR } from './styles';
import styles from './styles';
import type { Item } from '../../Types';
import AvatarListItem from './AvatarListItem';
type Props = {
/**
@@ -48,34 +50,11 @@ export default class NavigateSectionListItem extends Component<Props> {
*/
constructor(props: Props) {
super(props);
this._getAvatarColor = this._getAvatarColor.bind(this);
this._renderItemLine = this._renderItemLine.bind(this);
this._renderItemLines = this._renderItemLines.bind(this);
}
_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}`];
}
_renderItemLine: (string, number) => React$Node;
/**
@@ -138,12 +117,8 @@ export default class NavigateSectionListItem extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { slideActions } = this.props;
const { id, colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase)
};
const { item, slideActions } = this.props;
const { id } = item;
let right;
// NOTE: The {@code Swipeout} component has an onPress prop encapsulated
@@ -165,31 +140,12 @@ export default class NavigateSectionListItem extends Component<Props> {
<Swipeout
backgroundColor = { ColorPalette.transparent }
right = { right }>
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
</Container>
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
<AvatarListItem
item = { item }
onPress = { this.props.onPress }>
{ this.props.secondaryAction
&& this._renderSecondaryAction() }
</Container>
</AvatarListItem>
</Swipeout>
);
}

View File

@@ -1,7 +1,7 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView, Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../font-icons';
@@ -215,13 +215,13 @@ class PagedList extends Component<Props, State> {
{
this._renderPage(pages[pageIndex], disabled)
}
<View style = { styles.pageIndicatorContainer }>
<SafeAreaView style = { styles.pageIndicatorContainer }>
{
pages.map((page, index) => this._renderPageIndicator(
page, index, disabled
))
}
</View>
</SafeAreaView>
</View>
);
}
@@ -247,7 +247,7 @@ class PagedList extends Component<Props, State> {
key = { index }
onPress = { this._onSelectPage(index) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<View style = { styles.pageIndicatorContent }>
<Icon
name = { page.icon }
style = { [

View File

@@ -0,0 +1,88 @@
// @flex
import { StyleSheet } from 'react-native';
import { ColorSchemeRegistry, schemeColor } from '../../../color-scheme';
import { BoxModel } from '../../../styles';
const HEADER_FONT_SIZE = 18;
const HEADER_HEIGHT = 48;
export const HEADER_PADDING = BoxModel.padding / 2;
ColorSchemeRegistry.register('Header', {
/**
* Style of a disabled button in the header (e.g. Next).
*/
disabledButtonText: {
opacity: 0.6
},
/**
* Platform specific header button (e.g. back, menu, etc).
*/
headerButtonIcon: {
alignSelf: 'center',
color: schemeColor('icon'),
fontSize: 22,
marginRight: 12,
padding: 8
},
headerButtonText: {
color: schemeColor('text'),
fontSize: HEADER_FONT_SIZE
},
/**
* Style of the header overlay to cover the unsafe areas.
*/
headerOverlay: {
backgroundColor: schemeColor('background')
},
/**
* Generic style for a label placed in the header.
*/
headerText: {
color: schemeColor('text'),
fontSize: HEADER_FONT_SIZE
},
headerTextWrapper: {
alignItems: 'center',
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0
},
/**
* The top-level element of a page.
*/
page: {
...StyleSheet.absoluteFillObject,
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
overflow: 'hidden'
},
/**
* Base style of Header.
*/
screenHeader: {
alignItems: 'center',
backgroundColor: schemeColor('background'),
flexDirection: 'row',
height: HEADER_HEIGHT,
justifyContent: 'space-between',
paddingHorizontal: BoxModel.padding,
paddingVertical: HEADER_PADDING
},
statusBar: schemeColor('statusBar'),
statusBarContent: schemeColor('statusBarContent')
});

View File

@@ -1,7 +1,13 @@
// @flow
export { default as AvatarListItem } from './AvatarListItem';
export { default as BackButton } from './BackButton';
export { default as Button } from './Button';
export { default as Container } from './Container';
export { default as ForwardButton } from './ForwardButton';
export { default as Header } from './Header';
export { default as HeaderLabel } from './HeaderLabel';
export { default as Image } from './Image';
export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as Modal } from './Modal';

View File

@@ -5,69 +5,13 @@ import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
const AVATAR_OPACITY = 0.4;
const AVATAR_SIZE = 65;
const HEADER_COLOR = ColorPalette.blue;
// Header height is from Android guidelines. Also, this looks good.
const HEADER_HEIGHT = 56;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
const SECONDARY_ACTION_BUTTON_SIZE = 30;
export const HEADER_PADDING = BoxModel.padding;
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
export const AVATAR_SIZE = 65;
export const SIDEBAR_WIDTH = 250;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
const HEADER_STYLES = {
/**
* Platform specific header button (e.g. back, menu, etc).
*/
headerButton: {
alignSelf: 'center',
color: ColorPalette.white,
fontSize: 26,
paddingRight: 22
},
/**
* Style of the header overlay to cover the unsafe areas.
*/
headerOverlay: {
backgroundColor: HEADER_COLOR
},
/**
* Generic style for a label placed in the header.
*/
headerText: {
color: ColorPalette.white,
fontSize: 20
},
/**
* The top-level element of a page.
*/
page: {
...StyleSheet.absoluteFillObject,
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
overflow: 'hidden'
},
/**
* Base style of Header.
*/
screenHeader: {
alignItems: 'center',
backgroundColor: HEADER_COLOR,
flexDirection: 'row',
height: HEADER_HEIGHT,
justifyContent: 'flex-start',
padding: HEADER_PADDING
}
};
/**
* Style classes of the PagedList-based components.
*/
@@ -85,9 +29,9 @@ const PAGED_LIST_STYLES = {
*/
pageIndicator: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
justifyContent: 'center',
padding: BoxModel.padding / 2
},
/**
@@ -101,10 +45,15 @@ const PAGED_LIST_STYLES = {
* Container for the page indicators (Android).
*/
pageIndicatorContainer: {
alignItems: 'stretch',
alignItems: 'center',
backgroundColor: ColorPalette.blue,
flexDirection: 'row',
height: 56,
justifyContent: 'space-around'
},
pageIndicatorContent: {
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center'
},
@@ -153,10 +102,7 @@ const SECTION_LIST_STYLES = {
avatar: {
alignItems: 'center',
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
borderRadius: AVATAR_SIZE,
height: AVATAR_SIZE,
justifyContent: 'center',
width: AVATAR_SIZE
justifyContent: 'center'
},
/**
@@ -200,7 +146,7 @@ const SECTION_LIST_STYLES = {
avatarContent: {
backgroundColor: 'rgba(0, 0, 0, 0)',
color: OVERLAY_FONT_COLOR,
fontSize: 32,
fontSize: Math.floor(AVATAR_SIZE / 2),
fontWeight: '100',
textAlign: 'center'
},
@@ -334,7 +280,6 @@ export const TINTED_VIEW_DEFAULT = {
* base/react.
*/
export default createStyleSheet({
...HEADER_STYLES,
...PAGED_LIST_STYLES,
...SECTION_LIST_STYLES,
...SIDEBAR_STYLES

View File

@@ -0,0 +1,41 @@
/* @flow */
import Button from '@atlaskit/button';
import React, { Component } from 'react';
type Props = {
/**
* React Elements to display within the component.
*/
children: React$Node | Object,
/**
* Handler called when the user presses the button.
*/
onValueChange: Function
};
/**
* Renders a button.
*/
export default class ButtonImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { onValueChange } = this.props;
return (
<Button
appearance = 'primary'
onClick = { onValueChange }
type = 'button'>
{ this.props.children }
</Button>
);
}
}

View File

@@ -0,0 +1,19 @@
import React, { Component } from 'react';
/**
* Implements a React/Web {@link Component} for displaying image
* in order to facilitate cross-platform source code.
*
* @extends Component
*/
export default class Image extends Component {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return React.createElement('img', this.props);
}
}

View File

@@ -1,4 +1,6 @@
export { default as Button } from './Button';
export { default as Container } from './Container';
export { default as Image } from './Image';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as MeetingsList } from './MeetingsList';
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';

View File

@@ -23,6 +23,12 @@ const HEX_SHORT_COLOR_FORMAT
*/
const RGB_COLOR_FORMAT = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i;
/**
* RegExp pattern for RGBA color format.
*/
const RGBA_COLOR_FORMAT
= /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*([0-9.]+)\)$/i;
/**
* The list of the well-known style properties which may not be numbers on Web
* but must be numbers on React Native.
@@ -136,6 +142,23 @@ export function getRGBAFormat(color: string, alpha: number): string {
return color;
}
/**
* Decides if a color is light or dark based on the ITU-R BT.709 and W3C
* recommendations.
*
* NOTE: Please see https://www.w3.org/TR/WCAG20/#relativeluminancedef.
*
* @param {string} color - The color in rgb, rgba or hex format.
* @returns {boolean}
*/
export function isDarkColor(color: string): boolean {
const rgb = _getRGBObjectFormat(color);
return ((_getColorLuminance(rgb.r) * 0.2126)
+ (_getColorLuminance(rgb.g) * 0.7152)
+ (_getColorLuminance(rgb.b) * 0.0722)) <= 0.179;
}
/**
* Converts an [0..1] alpha value into HEX.
*
@@ -147,6 +170,67 @@ function _getAlphaInHex(alpha: number): string {
.padStart(2, '0');
}
/**
* Calculated the color luminance component for an individual color channel.
*
* NOTE: Please see https://www.w3.org/TR/WCAG20/#relativeluminancedef.
*
* @param {number} c - The color which we need the individual luminance
* for.
* @returns {number}
*/
function _getColorLuminance(c: number): number {
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
/**
* Parses a color string into an object containing the RGB values as numbers.
*
* NOTE: Object properties are not alpha-sorted for sanity.
*
* @param {string} color - The color to convert.
* @returns {{
* r: number,
* g: number,
* b: number
* }}
*/
function _getRGBObjectFormat(color: string): {r: number, g: number, b: number} {
let match = color.match(HEX_LONG_COLOR_FORMAT);
if (match) {
return {
r: parseInt(match[1], 16) / 255.0,
g: parseInt(match[2], 16) / 255.0,
b: parseInt(match[3], 16) / 255.0
};
}
match = color.match(HEX_SHORT_COLOR_FORMAT);
if (match) {
return {
r: parseInt(`${match[1]}${match[1]}`, 16) / 255.0,
g: parseInt(`${match[2]}${match[2]}`, 16) / 255.0,
b: parseInt(`${match[3]}${match[3]}`, 16) / 255.0
};
}
match = color.match(RGB_COLOR_FORMAT) || color.match(RGBA_COLOR_FORMAT);
if (match) {
return {
r: parseInt(match[1], 10) / 255.0,
g: parseInt(match[2], 10) / 255.0,
b: parseInt(match[3], 10) / 255.0
};
}
return {
r: 0,
g: 0,
b: 0
};
}
/**
* Shims style properties to work correctly on native. Allows us to minimize the
* number of style declarations that need to be set or overridden for specific

View File

@@ -160,8 +160,9 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
* @returns {string}
*/
_getIconName() {
return (this._isToggled() ? this.toggledIconName : this.iconName)
|| this.iconName;
return (
this._isToggled() ? this.toggledIconName : this.iconName
) || this.iconName;
}
/**

View File

@@ -5,3 +5,4 @@ export { default as AbstractButton } from './AbstractButton';
export type { Props as AbstractButtonProps } from './AbstractButton';
export { default as AbstractHangupButton } from './AbstractHangupButton';
export { default as AbstractVideoMuteButton } from './AbstractVideoMuteButton';
export { default as OverflowMenuItem } from './OverflowMenuItem';

View File

@@ -26,7 +26,10 @@ export function addLinkToCalendarEntry(
return new Promise((resolve, reject) => {
getShareInfoText(state, link, true).then(shareInfoText => {
RNCalendarEvents.findEventById(id).then(event => {
const updateText = `${event.description}\n\n${shareInfoText}`;
const updateText
= event.description
? `${event.description}\n\n${shareInfoText}`
: shareInfoText;
const updateObject = {
id: event.id,
...Platform.select({

View File

@@ -157,9 +157,13 @@ class ChatInput extends Component<Props, State> {
&& event.shiftKey === false) {
event.preventDefault();
this.props.dispatch(sendMessage(this.state.message));
const trimmed = this.state.message.trim();
this.setState({ message: '' });
if (trimmed) {
this.props.dispatch(sendMessage(trimmed));
this.setState({ message: '' });
}
}
}

View File

@@ -22,7 +22,7 @@ import {
TileView
} from '../../../filmstrip';
import { LargeVideo } from '../../../large-video';
import { CalleeInfoContainer } from '../../../invite';
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { Captions } from '../../../subtitles';
import { setToolboxVisible, Toolbox } from '../../../toolbox';
import { shouldDisplayTileView } from '../../../video-layout';
@@ -255,6 +255,7 @@ class Conference extends Component<Props> {
translucent = { true } />
<Chat />
<AddPeopleDialog />
{/*
* The LargeVideo is the lowermost stacking layer.

View File

@@ -189,7 +189,7 @@ class DesktopPicker extends PureComponent<Props, State> {
<Dialog
isModal = { false }
okDisabled = { Boolean(!this.state.selectedSource.id) }
okTitleKey = 'dialog.Share'
okKey = 'dialog.Share'
onCancel = { this._onCloseModal }
onSubmit = { this._onSubmit }
titleKey = 'dialog.shareYourScreen'

View File

@@ -1,7 +1,5 @@
// @flow
import { getLocationContextRoot } from '../base/util';
import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
import { _authorizeDropbox } from './functions';
@@ -15,8 +13,14 @@ export function authorizeDropbox() {
const state = getState();
const { locationURL } = state['features/base/connection'];
const { dropbox = {} } = state['features/base/config'];
const redirectURI = `${locationURL.origin
+ getLocationContextRoot(locationURL)}static/oauth.html`;
// By default we use the static page on the main domain for redirection.
// So we need to setup only one redirect URI in dropbox app
// configuration (not multiple for all the tenants).
// In case deployment is running in subfolder dropbox.redirectURI
// can be configured.
const redirectURI
= dropbox.redirectURI || `${locationURL.origin}/static/oauth.html`;
_authorizeDropbox(dropbox.appKey, redirectURI)
.then(

View File

@@ -44,8 +44,11 @@ export function getSpaceUsage(token: string) {
* Returns <tt>true</tt> if the dropbox features is enabled and <tt>false</tt>
* otherwise.
*
* @param {Object} state - The redux state.
* @returns {boolean}
*/
export function isEnabled() {
return Dropbox.ENABLED;
export function isEnabled(state: Object) {
const { dropbox = {} } = state['features/base/config'];
return Dropbox.ENABLED && typeof dropbox.appKey === 'string';
}

View File

@@ -23,12 +23,10 @@ function authorize(authUrl: string): Promise<string> {
return new Promise(resolve => {
const popup = window.open(authUrl, windowName);
gloabalNS.oauthCallbacks[windowName] = () => {
const returnURL = popup.location.href;
gloabalNS.oauthCallbacks[windowName] = url => {
popup.close();
delete gloabalNS.oauthCallbacks.windowName;
resolve(returnURL);
resolve(url);
};
});
}

View File

@@ -228,7 +228,7 @@ class FeedbackDialog extends Component<Props, State> {
return (
<Dialog
okTitleKey = 'dialog.Submit'
okKey = 'dialog.Submit'
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'feedback.rateExperience'>

View File

@@ -41,6 +41,16 @@ export const REMOVE_PENDING_INVITE_REQUESTS
*/
export const SET_CALLEE_INFO_VISIBLE = Symbol('SET_CALLEE_INFO_VISIBLE');
/**
* The type of redux action which sets the invite dialog visible or invisible.
*
* {
* type: SET_INVITE_DIALOG_VISIBLE,
* visible: boolean
* }
*/
export const SET_INVITE_DIALOG_VISIBLE = Symbol('SET_INVITE_DIALOG_VISIBLE');
/**
* The type of the action which signals an error occurred while requesting dial-
* in numbers.

View File

@@ -9,6 +9,7 @@ import {
BEGIN_ADD_PEOPLE,
REMOVE_PENDING_INVITE_REQUESTS,
SET_CALLEE_INFO_VISIBLE,
SET_INVITE_DIALOG_VISIBLE,
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
@@ -197,6 +198,22 @@ export function updateDialInNumbers() {
};
}
/**
* Sets the visibility of the invite dialog.
*
* @param {boolean} visible - The visibility to set.
* @returns {{
* type: SET_INVITE_DIALOG_VISIBLE,
* visible: boolean
* }}
*/
export function setAddPeopleDialogVisible(visible: boolean) {
return {
type: SET_INVITE_DIALOG_VISIBLE,
visible
};
}
/**
* Sets the visibility of {@code CalleeInfo}.
*

View File

@@ -5,12 +5,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { getParticipantCount } from '../../base/participants';
import { OverflowMenuItem } from '../../base/toolbox';
import { getActiveSession } from '../../recording';
import { ToolbarButton } from '../../toolbox';
import { updateDialInNumbers } from '../actions';
import { InfoDialog } from './info-dialog';
@@ -59,6 +60,11 @@ type Props = {
*/
dispatch: Dispatch<*>,
/**
* Whether to show the label or not.
*/
showLabel: boolean,
/**
* Invoked to obtain translated strings.
*/
@@ -122,6 +128,8 @@ class InfoDialogButton extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance.
this._onDialogClose = this._onDialogClose.bind(this);
this._onDialogToggle = this._onDialogToggle.bind(this);
this._onClickOverflowMenuButton
= this._onClickOverflowMenuButton.bind(this);
}
/**
@@ -142,16 +150,28 @@ class InfoDialogButton extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _dialIn, _liveStreamViewURL, t } = this.props;
const { _dialIn, _liveStreamViewURL, showLabel, t } = this.props;
const { showDialog } = this.state;
const iconClass = `icon-info ${showDialog ? 'toggled' : ''}`;
if (showLabel) {
return (
<OverflowMenuItem
accessibilityLabel = { t('info.accessibilityLabel') }
icon = 'icon-info'
key = 'info-button'
onClick = { this._onClickOverflowMenuButton }
text = { t('info.label') } />
);
}
return (
<div className = 'toolbox-button-wth-dialog'>
<InlineDialog
content = {
<InfoDialog
dialIn = { _dialIn }
isInlineDialog = { true }
liveStreamViewURL = { _liveStreamViewURL }
onClose = { this._onDialogClose } /> }
isOpen = { showDialog }
@@ -179,6 +199,23 @@ class InfoDialogButton extends Component<Props, State> {
this.setState({ showDialog: false });
}
_onClickOverflowMenuButton: () => void;
/**
* Opens the Info dialog.
*
* @returns {void}
*/
_onClickOverflowMenuButton() {
const { _dialIn, _liveStreamViewURL } = this.props;
this.props.dispatch(openDialog(InfoDialog, {
dialIn: _dialIn,
liveStreamViewURL: _liveStreamViewURL,
isInlineDialog: false
}));
}
_onDialogToggle: () => void;
/**

View File

@@ -8,29 +8,21 @@ import { AbstractButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { beginShareRoom } from '../../share-room';
import { beginAddPeople } from '../actions';
import { setAddPeopleDialogVisible } from '../actions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../functions';
type Props = AbstractButtonProps & {
/**
* Whether or not the feature to directly invite people into the
* Whether or not the feature to invite people to join the
* conference is available.
*/
_addPeopleEnabled: boolean,
/**
* Whether or not the feature to dial out to number to join the
* conference is available.
* Opens the add people dialog.
*/
_dialOutEnabled: boolean,
/**
* Launches native invite dialog.
*
* @protected
*/
_onAddPeople: Function,
_onOpenAddPeopleDialog: Function,
/**
* Begins the UI procedure to share the conference/room URL.
@@ -54,12 +46,17 @@ class InviteButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
// FIXME: dispatch _onAddPeople here, when we have a dialog for it.
const {
_addPeopleEnabled,
_onOpenAddPeopleDialog,
_onShareRoom
} = this.props;
_onShareRoom();
if (_addPeopleEnabled) {
_onOpenAddPeopleDialog();
} else {
_onShareRoom();
}
}
}
@@ -69,22 +66,23 @@ class InviteButton extends AbstractButton<Props, *> {
*
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @returns {{
* _onAddPeople,
* _onOpenAddPeopleDialog,
* _onShareRoom
* }}
* @private
*/
function _mapDispatchToProps(dispatch: Dispatch<*>) {
return {
/**
* Launches the add people dialog.
* Opens the add people dialog.
*
* @private
* @returns {void}
* @type {Function}
*/
_onAddPeople() {
dispatch(beginAddPeople());
_onOpenAddPeopleDialog() {
dispatch(setAddPeopleDialogVisible(true));
},
/**
@@ -107,25 +105,12 @@ function _mapDispatchToProps(dispatch: Dispatch<*>) {
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* _addPeopleEnabled: boolean
* }}
*/
function _mapStateToProps(state) {
return {
/**
* Whether or not the feature to directly invite people into the
* conference is available.
*
* @type {boolean}
*/
_addPeopleEnabled: isAddPeopleEnabled(state),
/**
* Whether or not the feature to dial out to number to join the
* conference is available.
*
* @type {boolean}
*/
_dialOutEnabled: isDialOutEnabled(state)
_addPeopleEnabled: isAddPeopleEnabled(state) || isDialOutEnabled(state)
};
}

View File

@@ -0,0 +1,222 @@
// @flow
import { Component } from 'react';
import { createInviteDialogEvent, sendAnalytics } from '../../../analytics';
import { invite } from '../../actions';
import {
getInviteResultsForQuery,
getInviteTypeCounts,
isAddPeopleEnabled,
isDialOutEnabled
} from '../../functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
export type Props = {
/**
* Whether or not to show Add People functionality.
*/
_addPeopleEnabled: boolean,
/**
* The URL for validating if a phone number can be called.
*/
_dialOutAuthUrl: string,
/**
* Whether or not to show Dial Out functionality.
*/
_dialOutEnabled: boolean,
/**
* The JWT token.
*/
_jwt: string,
/**
* The query types used when searching people.
*/
_peopleSearchQueryTypes: Array<string>,
/**
* The URL pointing to the service allowing for people search.
*/
_peopleSearchUrl: string,
/**
* The Redux dispatch function.
*/
dispatch: Function
};
export type State = {
/**
* Indicating that an error occurred when adding people to the call.
*/
addToCallError: boolean,
/**
* Indicating that we're currently adding the new people to the
* call.
*/
addToCallInProgress: boolean,
/**
* The list of invite items.
*/
inviteItems: Array<Object>,
};
/**
* Implements an abstract dialog to invite people to the conference.
*/
export default class AbstractAddPeopleDialog<P: Props, S: State>
extends Component<P, S> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this._query = this._query.bind(this);
}
/**
* 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.
*
* @param {Array<Object>} invitees - The items to be invited.
* @returns {Promise<Array<Object>>}
*/
_invite(invitees) {
const inviteTypeCounts = getInviteTypeCounts(invitees);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return Promise.resolve([]);
}
this.setState({
addToCallInProgress: true
});
const { dispatch } = this.props;
return dispatch(invite(invitees))
.then(invitesLeftToSend => {
this.setState({
addToCallInProgress: false
});
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
const erroredInviteTypeCounts
= getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallError: true
});
}
return invitesLeftToSend;
});
}
/**
* Indicates if the Add button should be disabled.
*
* @private
* @returns {boolean} - True to indicate that the Add button should
* be disabled, false otherwise.
*/
_isAddDisabled() {
return !this.state.inviteItems.length
|| this.state.addToCallInProgress;
}
_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 {
_addPeopleEnabled: addPeopleEnabled,
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutEnabled: dialOutEnabled,
_jwt: jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
} = this.props;
const options = {
addPeopleEnabled,
dialOutAuthUrl,
dialOutEnabled,
jwt,
peopleSearchQueryTypes,
peopleSearchUrl
};
return getInviteResultsForQuery(query, options);
}
}
/**
* Maps (parts of) the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _addPeopleEnabled: boolean,
* _dialOutAuthUrl: string,
* _dialOutEnabled: boolean,
* _jwt: string,
* _peopleSearchQueryTypes: Array<string>,
* _peopleSearchUrl: string
* }}
*/
export function _mapStateToProps(state: Object) {
const {
dialOutAuthUrl,
peopleSearchQueryTypes,
peopleSearchUrl
} = state['features/base/config'];
return {
_addPeopleEnabled: isAddPeopleEnabled(state),
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutEnabled: isDialOutEnabled(state),
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
};
}

View File

@@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@@ -0,0 +1,469 @@
// @flow
import _ from 'lodash';
import React from 'react';
import {
ActivityIndicator,
Alert,
FlatList,
SafeAreaView,
TextInput,
TouchableOpacity,
View
} from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../../base/font-icons';
import { translate } from '../../../../base/i18n';
import {
AvatarListItem,
BackButton,
ForwardButton,
Header,
HeaderLabel,
Modal,
type Item
} from '../../../../base/react';
import { setAddPeopleDialogVisible } from '../../../actions';
import AbstractAddPeopleDialog, {
type Props as AbstractProps,
type State as AbstractState,
_mapStateToProps as _abstractMapStateToProps
} from '../AbstractAddPeopleDialog';
import styles, {
AVATAR_SIZE,
DARK_GREY
} from './styles';
type Props = AbstractProps & {
/**
* True if the invite dialog should be open, false otherwise.
*/
_isVisible: boolean,
/**
* Function used to translate i18n labels.
*/
t: Function
};
type State = AbstractState & {
/**
* True if a search is in progress, false otherwise.
*/
searchInprogress: boolean,
/**
* An array of items that are selectable on this dialog. This is usually
* populated by an async search.
*/
selectableItems: Array<Object>
};
/**
* Implements a special dialog to invite people from a directory service.
*/
class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
/**
* Default state object to reset the state to when needed.
*/
defaultState = {
addToCallError: false,
addToCallInProgress: false,
inviteItems: [],
searchInprogress: false,
selectableItems: []
};
/**
* Ref of the search field.
*/
inputFieldRef: ?TextInput;
/**
* TimeoutID to delay the search for the time the user is probably typing.
*/
searchTimeout: TimeoutID;
/**
* Contrustor of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = this.defaultState;
this._keyExtractor = this._keyExtractor.bind(this);
this._renderItem = this._renderItem.bind(this);
this._renderSeparator = this._renderSeparator.bind(this);
this._onCloseAddPeopleDialog = this._onCloseAddPeopleDialog.bind(this);
this._onInvite = this._onInvite.bind(this);
this._onPressItem = this._onPressItem.bind(this);
this._onTypeQuery = this._onTypeQuery.bind(this);
this._setFieldRef = this._setFieldRef.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (prevProps._isVisible !== this.props._isVisible) {
// Clear state
this._clearState();
}
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const {
_addPeopleEnabled,
_dialOutEnabled
} = this.props;
const { inviteItems } = this.state;
let placeholderKey = 'searchPlaceholder';
if (!_addPeopleEnabled) {
placeholderKey = 'searchCallOnlyPlaceholder';
} else if (!_dialOutEnabled) {
placeholderKey = 'searchPeopleOnlyPlaceholder';
}
return (
<Modal
onRequestClose = { this._onCloseAddPeopleDialog }
visible = { this.props._isVisible }>
<Header>
<BackButton onPress = { this._onCloseAddPeopleDialog } />
<HeaderLabel labelKey = 'inviteDialog.header' />
<ForwardButton
disabled = { this._isAddDisabled() }
labelKey = 'inviteDialog.send'
onPress = { this._onInvite } />
</Header>
<SafeAreaView style = { styles.dialogWrapper }>
<View
style = { styles.searchFieldWrapper }>
<View style = { styles.searchIconWrapper }>
{ this.state.searchInprogress
? <ActivityIndicator
color = { DARK_GREY }
size = 'small' />
: <Icon
name = { 'search' }
style = { styles.searchIcon } />}
</View>
<TextInput
autoCorrect = { false }
editable = { !this.state.searchInprogress }
onChangeText = { this._onTypeQuery }
placeholder = {
this.props.t(`inviteDialog.${placeholderKey}`)
}
ref = { this._setFieldRef }
style = { styles.searchField } />
</View>
<FlatList
ItemSeparatorComponent = { this._renderSeparator }
data = { this.state.selectableItems }
extraData = { inviteItems }
keyExtractor = { this._keyExtractor }
renderItem = { this._renderItem }
style = { styles.resultList } />
</SafeAreaView>
</Modal>
);
}
/**
* Clears the dialog content.
*
* @returns {void}
*/
_clearState() {
this.setState(this.defaultState);
}
_invite: Array<Object> => Promise<Array<Object>>
_isAddDisabled: () => boolean;
_keyExtractor: Object => string
/**
* Key extractor for the flatlist.
*
* @param {Object} item - The flatlist item that we need the key to be
* generated for.
* @returns {string}
*/
_keyExtractor(item) {
return item.type === 'user' ? item.user_id : item.number;
}
_onCloseAddPeopleDialog: () => void
/**
* Closes the dialog.
*
* @returns {void}
*/
_onCloseAddPeopleDialog() {
this.props.dispatch(setAddPeopleDialogVisible(false));
}
_onInvite: () => void
/**
* Invites the selected entries.
*
* @returns {void}
*/
_onInvite() {
this._invite(this.state.inviteItems)
.then(invitesLeftToSend => {
if (invitesLeftToSend.length) {
this.setState({
inviteItems: invitesLeftToSend
});
this._showFailedInviteAlert();
} else {
this._onCloseAddPeopleDialog();
}
});
}
_onPressItem: Item => Function
/**
* Function to preapre a callback for the onPress event of the touchable.
*
* @param {Item} item - The item on which onPress was invoked.
* @returns {Function}
*/
_onPressItem(item) {
return () => {
const { inviteItems } = this.state;
const finderKey = item.type === 'phone' ? 'number' : 'user_id';
if (inviteItems.find(
_.matchesProperty(finderKey, item[finderKey]))) {
// Item is already selected, need to unselect it.
this.setState({
inviteItems: inviteItems.filter(
element => item[finderKey] !== element[finderKey])
});
} else {
// Item is not selected yet, need to add to the list.
this.setState({
inviteItems: _.orderBy(
inviteItems.concat(item), [ 'name' ], [ 'asc' ])
});
}
};
}
_onTypeQuery: string => void
/**
* Handles the typing event of the text field on the dialog and performs the
* search.
*
* @param {string} query - The query that is typed in the field.
* @returns {void}
*/
_onTypeQuery(query) {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.setState({
searchInprogress: true
}, () => {
this._performSearch(query);
});
}, 500);
}
/**
* Performs the actual search.
*
* @param {string} query - The query to search for.
* @returns {void}
*/
_performSearch(query) {
this._query(query).then(results => {
const { inviteItems } = this.state;
let selectableItems = results.filter(result => {
switch (result.type) {
case 'phone':
return result.allowed && result.number
&& !inviteItems.find(
_.matchesProperty('number', result.number));
case 'user':
return result.user_id && !inviteItems.find(
_.matchesProperty('user_id', result.user_id));
default:
return false;
}
});
selectableItems
= _.orderBy(
this.state.inviteItems.concat(selectableItems),
[ 'name' ], [ 'asc' ]);
this.setState({
selectableItems
});
})
.finally(() => {
this.setState({
searchInprogress: false
}, () => {
this.inputFieldRef && this.inputFieldRef.focus();
});
});
}
_query: (string) => Promise<Array<Object>>;
_renderItem: Object => ?React$Element<*>
/**
* Renders a single item in the {@code FlatList}.
*
* @param {Object} flatListItem - An item of the data array of the
* {@code FlatList}.
* @param {number} index - The index of the currently rendered item.
* @returns {?React$Element<*>}
*/
_renderItem(flatListItem, index) {
const { item } = flatListItem;
const { inviteItems } = this.state;
let selected = false;
let renderableItem;
switch (item.type) {
case 'phone':
selected
= inviteItems.find(_.matchesProperty('number', item.number));
renderableItem = {
avatar: 'phone',
key: item.number,
title: item.number
};
break;
case 'user':
selected
= inviteItems.find(_.matchesProperty('user_id', item.user_id));
renderableItem = {
avatar: item.avatar,
key: item.user_id,
title: item.name
};
break;
default:
return null;
}
return (
<TouchableOpacity onPress = { this._onPressItem(item) } >
<View
pointerEvents = 'box-only'
style = { styles.itemWrapper }>
<Icon
name = { selected
? 'radio_button_checked'
: 'radio_button_unchecked' }
style = { styles.radioButton } />
<AvatarListItem
avatarSize = { AVATAR_SIZE }
avatarStyle = { styles.avatar }
avatarTextStyle = { styles.avatarText }
item = { renderableItem }
key = { index }
linesStyle = { styles.itemLinesStyle }
titleStyle = { styles.itemText } />
</View>
</TouchableOpacity>
);
}
_renderSeparator: () => ?React$Element<*>
/**
* Renders the item separator.
*
* @returns {?React$Element<*>}
*/
_renderSeparator() {
return (
<View style = { styles.separator } />
);
}
_setFieldRef: ?TextInput => void
/**
* Sets a reference to the input field for later use.
*
* @param {?TextInput} input - The reference to the input field.
* @returns {void}
*/
_setFieldRef(input) {
this.inputFieldRef = input;
}
/**
* Shows an alert telling the user that some invitees were failed to be
* invited.
*
* NOTE: We're using an Alert here because we're on a modal and it makes
* using our dialogs a tad more difficult.
*
* @returns {void}
*/
_showFailedInviteAlert() {
const { t } = this.props;
Alert.alert(
t('inviteDialog.alertTitle'),
t('inviteDialog.alertText'),
[
{
text: t('inviteDialog.alertOk')
}
]
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _isVisible: boolean
* }}
*/
function _mapStateToProps(state: Object) {
return {
..._abstractMapStateToProps(state),
_isVisible: state['features/invite'].inviteDialogVisible
};
}
export default translate(connect(_mapStateToProps)(AddPeopleDialog));

View File

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

View File

@@ -0,0 +1,91 @@
// @flow
import { ColorPalette } from '../../../../base/styles';
export const AVATAR_SIZE = 40;
export const DARK_GREY = 'rgb(28, 32, 37)';
export const LIGHT_GREY = 'rgb(209, 219, 232)';
export const ICON_SIZE = 15;
export default {
avatar: {
backgroundColor: LIGHT_GREY
},
avatarText: {
color: 'rgb(28, 32, 37)',
fontSize: 12
},
dialogWrapper: {
alignItems: 'stretch',
backgroundColor: ColorPalette.white,
flex: 1,
flexDirection: 'column'
},
itemLinesStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 13
},
itemText: {
color: DARK_GREY,
fontSize: 14,
fontWeight: 'normal'
},
itemWrapper: {
alignItems: 'center',
flexDirection: 'row',
paddingLeft: 5
},
radioButton: {
color: DARK_GREY,
fontSize: 16,
padding: 2
},
resultList: {
padding: 5
},
searchField: {
backgroundColor: 'rgb(240, 243, 247)',
borderBottomRightRadius: 10,
borderTopRightRadius: 10,
flex: 1,
fontSize: 17,
paddingVertical: 7
},
separator: {
borderBottomColor: LIGHT_GREY,
borderBottomWidth: 1,
marginLeft: 85
},
searchFieldWrapper: {
alignItems: 'stretch',
flexDirection: 'row',
height: 52,
paddingHorizontal: 15,
paddingVertical: 8
},
searchIcon: {
color: DARK_GREY,
fontSize: ICON_SIZE
},
searchIconWrapper: {
alignItems: 'center',
backgroundColor: 'rgb(240, 243, 247)',
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
flexDirection: 'row',
justifyContent: 'center',
width: ICON_SIZE + 16
}
};

View File

@@ -2,26 +2,27 @@
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message';
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate, translateToHTML } from '../../base/i18n';
import { getLocalParticipant } from '../../base/participants';
import { MultiSelectAutocomplete } from '../../base/react';
import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
import { Dialog, hideDialog } from '../../../../base/dialog';
import { translate, translateToHTML } from '../../../../base/i18n';
import { getLocalParticipant } from '../../../../base/participants';
import { MultiSelectAutocomplete } from '../../../../base/react';
import { invite } from '../actions';
import { getInviteResultsForQuery, getInviteTypeCounts } from '../functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
import AbstractAddPeopleDialog, {
type Props as AbstractProps,
type State,
_mapStateToProps as _abstractMapStateToProps
} from '../AbstractAddPeopleDialog';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link AddPeopleDialog}.
*/
type Props = {
type Props = AbstractProps & {
/**
* The {@link JitsiMeetConference} which will be used to invite "room"
@@ -29,41 +30,11 @@ type Props = {
*/
_conference: Object,
/**
* The URL for validating if a phone number can be called.
*/
_dialOutAuthUrl: string,
/**
* Whether to show a footer text after the search results as a last element.
*/
_footerTextEnabled: boolean,
/**
* The JWT token.
*/
_jwt: string,
/**
* The query types used when searching people.
*/
_peopleSearchQueryTypes: Array<string>,
/**
* The URL pointing to the service allowing for people search.
*/
_peopleSearchUrl: string,
/**
* Whether or not to show Add People functionality.
*/
addPeopleEnabled: boolean,
/**
* Whether or not to show Dial Out functionality.
*/
dialOutEnabled: boolean,
/**
* The redux {@code dispatch} function.
*/
@@ -75,32 +46,10 @@ type Props = {
t: Function,
};
/**
* The type of the React {@code Component} state of {@link AddPeopleDialog}.
*/
type State = {
/**
* Indicating that an error occurred when adding people to the call.
*/
addToCallError: boolean,
/**
* Indicating that we're currently adding the new people to the
* call.
*/
addToCallInProgress: boolean,
/**
* The list of invite items.
*/
inviteItems: Array<Object>
};
/**
* The dialog that allows to invite people to the call.
*/
class AddPeopleDialog extends Component<Props, State> {
class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
_multiselect = null;
_resourceClient: Object;
@@ -121,12 +70,10 @@ class AddPeopleDialog extends Component<Props, State> {
super(props);
// 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 = {
@@ -183,25 +130,27 @@ class AddPeopleDialog extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { _footerTextEnabled,
addPeopleEnabled,
dialOutEnabled,
t } = this.props;
const {
_addPeopleEnabled,
_dialOutEnabled,
_footerTextEnabled,
t
} = this.props;
let isMultiSelectDisabled = this.state.addToCallInProgress || false;
let placeholder;
let loadingMessage;
let noMatches;
let footerText;
if (addPeopleEnabled && dialOutEnabled) {
if (_addPeopleEnabled && _dialOutEnabled) {
loadingMessage = 'addPeople.loading';
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.searchPeopleAndNumbers';
} else if (addPeopleEnabled) {
} else if (_addPeopleEnabled) {
loadingMessage = 'addPeople.loadingPeople';
noMatches = 'addPeople.noResults';
placeholder = 'addPeople.searchPeople';
} else if (dialOutEnabled) {
} else if (_dialOutEnabled) {
loadingMessage = 'addPeople.loadingNumber';
noMatches = 'addPeople.noValidNumbers';
placeholder = 'addPeople.searchNumbers';
@@ -227,7 +176,7 @@ class AddPeopleDialog extends Component<Props, State> {
return (
<Dialog
okDisabled = { this._isAddDisabled() }
okTitleKey = 'addPeople.add'
okKey = 'addPeople.add'
onSubmit = { this._onSubmit }
titleKey = 'addPeople.title'
width = 'medium'>
@@ -250,19 +199,9 @@ class AddPeopleDialog extends Component<Props, State> {
);
}
_isAddDisabled: () => boolean;
_invite: Array<Object> => Promise<*>
/**
* Indicates if the Add button should be disabled.
*
* @private
* @returns {boolean} - True to indicate that the Add button should
* be disabled, false otherwise.
*/
_isAddDisabled() {
return !this.state.inviteItems.length
|| this.state.addToCallInProgress;
}
_isAddDisabled: () => boolean;
_onItemSelected: (Object) => Object;
@@ -300,12 +239,7 @@ class AddPeopleDialog extends Component<Props, State> {
_onSubmit: () => void;
/**
* 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.
* Submits the selection for inviting.
*
* @private
* @returns {void}
@@ -313,45 +247,10 @@ class AddPeopleDialog extends Component<Props, State> {
_onSubmit() {
const { inviteItems } = this.state;
const invitees = inviteItems.map(({ item }) => item);
const inviteTypeCounts = getInviteTypeCounts(invitees);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return;
}
this.setState({
addToCallInProgress: true
});
const { dispatch } = this.props;
dispatch(invite(invitees))
this._invite(invitees)
.then(invitesLeftToSend => {
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
const erroredInviteTypeCounts
= getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallInProgress: false,
addToCallError: true
});
const unsentInviteIDs
= invitesLeftToSend.map(invitee =>
invitee.id || invitee.number);
@@ -362,15 +261,9 @@ class AddPeopleDialog extends Component<Props, State> {
if (this._multiselect) {
this._multiselect.setSelectedItems(itemsToSelect);
}
return;
} else {
this.props.dispatch(hideDialog());
}
this.setState({
addToCallInProgress: false
});
dispatch(hideDialog());
});
}
@@ -442,34 +335,6 @@ class AddPeopleDialog extends Component<Props, State> {
_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 {
addPeopleEnabled,
dialOutEnabled,
_dialOutAuthUrl,
_jwt,
_peopleSearchQueryTypes,
_peopleSearchUrl
} = this.props;
const options = {
dialOutAuthUrl: _dialOutAuthUrl,
addPeopleEnabled,
dialOutEnabled,
jwt: _jwt,
peopleSearchQueryTypes: _peopleSearchQueryTypes,
peopleSearchUrl: _peopleSearchUrl
};
return getInviteResultsForQuery(query, options);
}
/**
* Renders the error message if the add doesn't succeed.
*
@@ -557,10 +422,7 @@ class AddPeopleDialog extends Component<Props, State> {
*/
function _mapStateToProps(state) {
const {
dialOutAuthUrl,
enableFeaturesBasedOnToken,
peopleSearchQueryTypes,
peopleSearchUrl
enableFeaturesBasedOnToken
} = state['features/base/config'];
let footerTextEnabled = false;
@@ -573,11 +435,8 @@ function _mapStateToProps(state) {
}
return {
_dialOutAuthUrl: dialOutAuthUrl,
_footerTextEnabled: footerTextEnabled,
_jwt: state['features/base/jwt'].jwt,
_peopleSearchQueryTypes: peopleSearchQueryTypes,
_peopleSearchUrl: peopleSearchUrl
..._abstractMapStateToProps(state),
_footerTextEnabled: footerTextEnabled
};
}

View File

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

View File

@@ -14,6 +14,11 @@ type Props = {
*/
conferenceID: number,
/**
* The name of the conference.
*/
conferenceName: ?string,
/**
* Invoked to obtain translated strings.
*/
@@ -33,11 +38,19 @@ class ConferenceID extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { conferenceID, t } = this.props;
const { conferenceID, conferenceName, t } = this.props;
return (
<div className = 'dial-in-conference-id'>
{ t('info.dialANumber', { conferenceID }) }
<div className = 'dial-in-conference-name'>
{ conferenceName }
</div>
<div className = 'dial-in-conference-description'>
{ t('info.dialANumber') }
</div>
<div className = 'dial-in-conference-pin'>
{ `${t('info.dialInConferenceID')} ${conferenceID}` }
</div>
</div>
);
}

View File

@@ -56,9 +56,9 @@ type State = {
loading: boolean,
/**
* The dial-in numbers. entered by the local participant.
* The dial-in numbers to be displayed.
*/
numbers: ?Array<Object>,
numbers: ?Array<Object> | ?Object,
/**
* Whether or not dial-in is allowed.
@@ -143,6 +143,7 @@ class DialInSummary extends Component<Props, State> {
conferenceID
? <ConferenceID
conferenceID = { conferenceID }
conferenceName = { this.props.room }
key = 'conferenceID' />
: null,
<NumbersList
@@ -238,17 +239,22 @@ class DialInSummary extends Component<Props, State> {
* Callback invoked when fetching dial-in numbers succeeds. Sets the
* internal to show the numbers.
*
* @param {Object} response - The response from fetching dial-in numbers.
* @param {Array|Object} response - The response from fetching
* dial-in numbers.
* @param {Array|Object} response.numbers - The dial-in numbers.
* @param {boolean} reponse.numbersEnabled - Whether or not dial-in is
* enabled.
* @param {boolean} response.numbersEnabled - Whether or not dial-in is
* enabled, old syntax that is deprecated.
* @private
* @returns {void}
*/
_onGetNumbersSuccess({ numbers, numbersEnabled }) {
_onGetNumbersSuccess(
response: Array<Object> | { numbersEnabled?: boolean }) {
this.setState({
numbersEnabled,
numbers
numbersEnabled:
Array.isArray(response)
? response.length > 0 : response.numbersEnabled,
numbers: response
});
}

View File

@@ -17,10 +17,10 @@ type Props = {
conferenceID: number,
/**
* The phone numbers to display. Can be an array of numbers or an object
* with countries as keys and an array of numbers as values.
* The phone numbers to display. Can be an array of number Objects or an
* object with countries as keys and an array of numbers as values.
*/
numbers: { [string]: Array<string> } | Array<string>,
numbers: { [string]: Array<string> } | Array<Object>,
/**
* Invoked to obtain translated strings.
@@ -41,92 +41,157 @@ class NumbersList extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { numbers, t } = this.props;
const { numbers } = this.props;
return (
<table className = 'dial-in-numbers-list'>
<thead>
<tr>
{ Array.isArray(numbers)
? null
: <th>{ t('info.country') }</th> }
<th>{ t('info.numbers') }</th>
</tr>
</thead>
<tbody className = 'dial-in-numbers-body'>
{ Array.isArray(numbers)
? numbers.map(this._renderNumberRow)
: this._renderWithCountries(numbers) }
</tbody>
</table>);
return this._renderWithCountries(numbers);
}
/**
* Renders rows of countries and associated phone numbers.
*
* @param {Object} numbersMapping - An object with country names as keys
* and values as arrays of phone numbers.
* @param {Object|Array<Object>} numbersMapping - An object with country
* names as keys and values as arrays of phone numbers.
* @private
* @returns {ReactElement[]}
*/
_renderWithCountries(numbersMapping: Object) {
const rows = [];
_renderWithCountries(
numbersMapping: { numbers: Array<string> } | Array<Object>) {
const { t } = this.props;
let hasFlags = false, numbers;
for (const [ country, numbers ] of Object.entries(numbersMapping)) {
if (!Array.isArray(numbers)) {
return;
}
if (Array.isArray(numbersMapping)) {
hasFlags = true;
numbers = numbersMapping.reduce(
(resultNumbers, number) => {
const countryName
= t(`countries:countries.${number.countryCode}`);
const formattedNumbers = numbers.map(number => {
if (typeof number === 'string') {
return this._renderNumberDiv(number);
if (resultNumbers[countryName]) {
resultNumbers[countryName].push(number);
} else {
resultNumbers[countryName] = [ number ];
}
return resultNumbers;
}, {});
} else {
numbers = {};
for (const [ country, numbersArray ]
of Object.entries(numbersMapping.numbers)) {
if (Array.isArray(numbersArray)) {
/* eslint-disable arrow-body-style */
const formattedNumbers = numbersArray.map(number => ({
formattedNumber: number
}));
/* eslint-enable arrow-body-style */
numbers[country] = formattedNumbers;
}
return null;
});
rows.push(
<tr key = { country }>
<td>{ country }</td>
<td className = 'dial-in-numbers'>{ formattedNumbers }</td>
</tr>
);
}
}
return rows;
const rows = [];
Object.keys(numbers).forEach((countryName: string) => {
const numbersArray = numbers[countryName];
rows.push(
<tr
className = 'number-group'
key = { countryName }>
{ this._renderFlag(numbersArray[0].countryCode) }
<td className = 'country' >{ countryName }</td>
<td className = 'numbers-list-column'>
{ this._renderNumbersList(numbersArray) }
</td>
<td className = 'toll-free-list-column' >
{ this._renderNumbersTollFreeList(numbersArray) }
</td>
</tr>
);
});
return (
<table className = 'dial-in-numbers-list'>
<thead>
<tr>
{ hasFlags ? <th /> : null}
<th>{ t('info.country') }</th>
<th>{ t('info.numbers') }</th>
<th />
</tr>
</thead>
<tbody className = 'dial-in-numbers-body'>
{ rows }
</tbody>
</table>
);
}
/**
* Renders a table row for a phone number.
* Renders a div container for a flag for the country of the phone number.
*
* @param {string} number - The phone number to display.
* @param {string} countryCode - The country code flag to display.
* @private
* @returns {ReactElement[]}
* @returns {ReactElement}
*/
_renderNumberRow(number) {
return (
<tr key = { number }>
<td className = 'dial-in-number'>
{ this._renderNumberLink(number) }
</td>
</tr>
);
_renderFlag(countryCode) {
if (countryCode) {
return (
<td className = 'flag-cell'>
<i className = { `flag iti-flag ${countryCode}` } />
</td>);
}
return null;
}
/**
* Renders a div container for a phone number.
*
* @param {string} number - The phone number to display.
* @param {Array} numbers - The phone number to display.
* @private
* @returns {ReactElement[]}
*/
_renderNumberDiv(number) {
return (
<div
_renderNumbersList(numbers) {
const numbersListItems = numbers.map(number =>
(<li
className = 'dial-in-number'
key = { number }>
{ this._renderNumberLink(number) }
</div>
key = { number.formattedNumber }>
{ this._renderNumberLink(number.formattedNumber) }
</li>));
return (
<ul className = 'numbers-list'>
{ numbersListItems }
</ul>
);
}
/**
* Renders list with a toll free text on the position where there is a
* number marked as toll free.
*
* @param {Array} numbers - The phone number that are displayed.
* @private
* @returns {ReactElement[]}
*/
_renderNumbersTollFreeList(numbers) {
const { t } = this.props;
const tollNumbersListItems = numbers.map(number =>
(<li
className = 'toll-free'
key = { number.formattedNumber }>
{ number.tollFree ? t('info.dialInTollFree') : '' }
</li>));
return (
<ul className = 'toll-free-list'>
{ tollNumbersListItems }
</ul>
);
}
@@ -141,9 +206,12 @@ class NumbersList extends Component<Props> {
*/
_renderNumberLink(number) {
if (this.props.clickableNumbers) {
// Url encode # to %23, Android phone was cutting the # after
// clicking it.
// Seems that using ',' and '%23' works on iOS and Android.
return (
<a
href = { `tel:${number}p${this.props.conferenceID}#` }
href = { `tel:${number},${this.props.conferenceID}%23` }
key = { number } >
{ number }
</a>

View File

@@ -1,4 +1,6 @@
export { default as AddPeopleDialog } from './AddPeopleDialog';
// @flow
export * from './add-people-dialog';
export { DialInSummary } from './dial-in-summary';
export { default as InfoDialogButton } from './InfoDialogButton';
export { default as InviteButton } from './InviteButton';

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