mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-06 06:42:28 +00:00
Compare commits
34 Commits
emcho-patc
...
3261
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8400d01d75 | ||
|
|
d04068344a | ||
|
|
55a971c0fd | ||
|
|
20c1b1cfae | ||
|
|
ecb44b6ab4 | ||
|
|
039805eba3 | ||
|
|
22277ad799 | ||
|
|
2af1e8da95 | ||
|
|
12d0aef686 | ||
|
|
f439ad2999 | ||
|
|
81d4f694b7 | ||
|
|
c737d46d90 | ||
|
|
3f2a559d64 | ||
|
|
bdb3099073 | ||
|
|
b3a05db286 | ||
|
|
08f2edf350 | ||
|
|
98c7430b6f | ||
|
|
ebdcbe122a | ||
|
|
31c1034be7 | ||
|
|
a9d82a79ea | ||
|
|
27e1f5a1bc | ||
|
|
dbedee5e22 | ||
|
|
636c63397b | ||
|
|
67e7994e36 | ||
|
|
40f03fedc2 | ||
|
|
55149670da | ||
|
|
5739e1deaa | ||
|
|
b6e2701991 | ||
|
|
38b1be1291 | ||
|
|
555f8b3a99 | ||
|
|
ea4d49f2a0 | ||
|
|
d7eea8abbc | ||
|
|
1b8ef9a05a | ||
|
|
ac7311cb52 |
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
# org.gradle.parallel=true
|
||||
|
||||
buildNumber=1
|
||||
appVersion=19.0.0
|
||||
appVersion=19.1.0
|
||||
sdkVersion=1.21.0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
11
config.js
11
config.js
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -41,3 +41,10 @@
|
||||
top: -25px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popover {
|
||||
background-color: $popoverBg;
|
||||
border-radius: 3px;
|
||||
margin: -16px -24px;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,14 @@
|
||||
cursor: initial;
|
||||
color: #3b475c;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-tag {
|
||||
|
||||
@@ -63,7 +63,7 @@ $audioLevelShadow: rgba(9, 36, 77, 0.9);
|
||||
$videoStateIndicatorColor: $defaultColor;
|
||||
$videoStateIndicatorBackground: $toolbarBackground;
|
||||
$videoStateIndicatorSize: 40px;
|
||||
$remoteVideoMenuIconLeft: initial;
|
||||
$remoteVideoMenuIconMargin: initial;
|
||||
|
||||
/**
|
||||
* Feedback Modal
|
||||
|
||||
@@ -409,10 +409,10 @@
|
||||
height: 13px;
|
||||
color: #FFF;
|
||||
font-size: 10pt;
|
||||
margin-right: $remoteVideoMenuIconMargin;
|
||||
|
||||
>i{
|
||||
cursor: hand;
|
||||
margin-left: $remoteVideoMenuIconLeft;
|
||||
}
|
||||
}
|
||||
.remote-video-menu-trigger {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
.remote-video-menu-trigger {
|
||||
margin-bottom: 7px;
|
||||
margin-left: $remoteVideoMenuIconMargin;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
12
debian/rules
vendored
@@ -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;
|
||||
|
||||
17
doc/api.md
17
doc/api.md
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
7
doc/faq.md
Normal 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
38
doc/integrations.md
Normal 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
|
||||
@@ -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`:
|
||||
```
|
||||
|
||||
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
@@ -8,6 +8,7 @@
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " d="" />
|
||||
<glyph unicode="" 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="" 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="" glyph-name="invite" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
|
||||
<glyph unicode="" glyph-name="add" d="M810 470h-256v-256h-84v256h-256v84h256v256h84v-256h256v-84z" />
|
||||
<glyph unicode="" glyph-name="bluetooth" d="M550 328l-80 82v-162zM470 776v-162l80 82zM670 696l-184-184 184-184-244-242h-42v324l-196-196-60 60 238 238-238 238 60 60 196-196v324h42zM834 738c40-64 62-142 62-222 0-84-24-160-66-226l-50 50c26 52 42 110 42 172s-16 120-42 172zM608 512l98 98c12-30 20-64 20-98s-8-70-20-100z" />
|
||||
@@ -21,8 +22,11 @@
|
||||
<glyph unicode="" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
|
||||
<glyph unicode="" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
|
||||
<glyph unicode="" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="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="" 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="" 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="" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
|
||||
<glyph unicode="" glyph-name="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="" 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="" 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="" glyph-name="kick" d="M512 810l284-426h-568zM214 298h596v-84h-596v84z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
images/dropboxLogo_square.png
Executable file
BIN
images/dropboxLogo_square.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/flags.png
Normal file
BIN
images/flags.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
BIN
images/flags@2x.png
Normal file
BIN
images/flags@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 KiB |
BIN
images/jitsiLogo_square.png
Normal file
BIN
images/jitsiLogo_square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,11 @@
|
||||
"url": "accounts.google.com"
|
||||
},
|
||||
"target": "electron"
|
||||
},
|
||||
"dropbox-auth": {
|
||||
"matchPatterns": {
|
||||
"url": "dropbox.com/oauth2/authorize"
|
||||
},
|
||||
"target": "electron"
|
||||
}
|
||||
}
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -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
22
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @flow
|
||||
|
||||
export * from './_';
|
||||
export { default as AbstractPage } from './AbstractPage';
|
||||
export { default as NavigateSectionList } from './NavigateSectionList';
|
||||
|
||||
203
react/features/base/react/components/native/AvatarListItem.js
Normal file
203
react/features/base/react/components/native/AvatarListItem.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
44
react/features/base/react/components/native/Button.js
Normal file
44
react/features/base/react/components/native/Button.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
91
react/features/base/react/components/native/ForwardButton.js
Normal file
91
react/features/base/react/components/native/ForwardButton.js
Normal 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));
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
41
react/features/base/react/components/native/Image.js
Normal file
41
react/features/base/react/components/native/Image.js
Normal 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 } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = { [
|
||||
|
||||
88
react/features/base/react/components/native/headerstyles.js
Normal file
88
react/features/base/react/components/native/headerstyles.js
Normal 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')
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
41
react/features/base/react/components/web/Button.js
Normal file
41
react/features/base/react/components/web/Button.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
19
react/features/base/react/components/web/Image.js
Normal file
19
react/features/base/react/components/web/Image.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
@@ -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));
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as AddPeopleDialog } from './AddPeopleDialog';
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as AddPeopleDialog } from './AddPeopleDialog';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user