mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-19 18:59:16 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fa1b210f5 |
@@ -33,8 +33,6 @@ dependencies {
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-webrtc')
|
||||
compile project(':react-native-calendar-events')
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
// Build process helpers
|
||||
@@ -71,22 +69,6 @@ gradle.projectsEvaluated {
|
||||
runBefore("processUniversal${buildNameCapitalized}Resources", currentFontTask)
|
||||
runBefore("process${buildNameCapitalized}Resources", currentFontTask)
|
||||
|
||||
def currentSoundsTask = tasks.create(
|
||||
name: "copy${buildNameCapitalized}Sounds",
|
||||
type: Copy) {
|
||||
from("${projectDir}/../../sounds/joined.wav")
|
||||
from("${projectDir}/../../sounds/left.wav")
|
||||
into("${bundlePath}/assets/sounds")
|
||||
}
|
||||
|
||||
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Resources")
|
||||
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Assets")
|
||||
|
||||
runBefore("processArmeabi-v7a${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
runBefore("processX86${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
runBefore("processUniversal${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
runBefore("process${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
|
||||
// Bundle JavaScript and React resources.
|
||||
// (adapted from react-native/react.gradle)
|
||||
//
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -77,8 +78,7 @@ public class JitsiMeetView extends FrameLayout {
|
||||
new ExternalAPIModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new WiFiStatsModule(reactContext),
|
||||
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
|
||||
new WiFiStatsModule(reactContext)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Constructs IPv6 addresses for IPv4 addresses in the NAT64 environment.
|
||||
*
|
||||
* NAT64 translates IPv4 to IPv6 addresses by adding "well known" prefix and
|
||||
* suffix configured by the administrator. Those are figured out by discovering
|
||||
* both IPv6 and IPv4 addresses of a host and then trying to find a place where
|
||||
* the IPv4 address fits into the format described here:
|
||||
* https://tools.ietf.org/html/rfc6052#section-2.2
|
||||
*/
|
||||
public class NAT64AddrInfo {
|
||||
/**
|
||||
* Coverts bytes array to upper case HEX string.
|
||||
*
|
||||
* @param bytes an array of bytes to be converted
|
||||
* @return ex. "010AFF" for an array of {1, 10, 255}.
|
||||
*/
|
||||
static String bytesToHexString(byte[] bytes) {
|
||||
StringBuilder hexStr = new StringBuilder();
|
||||
|
||||
for (byte b : bytes) {
|
||||
hexStr.append(String.format("%02X", b));
|
||||
}
|
||||
|
||||
return hexStr.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to discover the NAT64 prefix/suffix based on the IPv4 and IPv6
|
||||
* addresses resolved for given {@code host}.
|
||||
*
|
||||
* @param host the host for which the code will try to discover IPv4 and
|
||||
* IPv6 addresses which then will be used to figure out the NAT64 prefix.
|
||||
* @return {@link NAT64AddrInfo} instance if the NAT64 prefix/suffix was
|
||||
* successfully discovered or {@code null} if it failed for any reason.
|
||||
* @throws UnknownHostException thrown by {@link InetAddress#getAllByName}.
|
||||
*/
|
||||
public static NAT64AddrInfo discover(String host)
|
||||
throws UnknownHostException {
|
||||
InetAddress ipv4 = null;
|
||||
InetAddress ipv6 = null;
|
||||
|
||||
for(InetAddress addr : InetAddress.getAllByName(host)) {
|
||||
byte[] bytes = addr.getAddress();
|
||||
|
||||
if (bytes.length == 4) {
|
||||
ipv4 = addr;
|
||||
} else if (bytes.length == 16) {
|
||||
ipv6 = addr;
|
||||
}
|
||||
}
|
||||
|
||||
if (ipv4 != null && ipv6 != null) {
|
||||
return figureOutNAT64AddrInfo(ipv4.getAddress(), ipv6.getAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on IPv4 and IPv6 addresses of the same host, the method will make
|
||||
* an attempt to figure out what are the NAT64 prefix and suffix.
|
||||
*
|
||||
* @param ipv4AddrBytes the IPv4 address of the same host in NAT64 network,
|
||||
* as returned by {@link InetAddress#getAddress()}.
|
||||
* @param ipv6AddrBytes the IPv6 address of the same host in NAT64 network,
|
||||
* as returned by {@link InetAddress#getAddress()}.
|
||||
* @return {@link NAT64AddrInfo} instance which contains the prefix/suffix
|
||||
* of the current NAT64 network or {@code null} if the prefix could not be
|
||||
* found.
|
||||
*/
|
||||
static NAT64AddrInfo figureOutNAT64AddrInfo(
|
||||
byte[] ipv4AddrBytes,
|
||||
byte[] ipv6AddrBytes) {
|
||||
String ipv6Str = bytesToHexString(ipv6AddrBytes);
|
||||
String ipv4Str = bytesToHexString(ipv4AddrBytes);
|
||||
|
||||
// NAT64 address format:
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |32| prefix |v4(32) | u | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |40| prefix |v4(24) | u |(8)| suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |48| prefix |v4(16) | u | (16) | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |56| prefix |(8)| u | v4(24) | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |64| prefix | u | v4(32) | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |96| prefix | v4(32) |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
int prefixLength = 96;
|
||||
int suffixLength = 0;
|
||||
String prefix = null;
|
||||
String suffix = null;
|
||||
|
||||
if (ipv4Str.equalsIgnoreCase(ipv6Str.substring(prefixLength / 4))) {
|
||||
prefix = ipv6Str.substring(0, prefixLength / 4);
|
||||
} else {
|
||||
// Cut out the 'u' octet
|
||||
ipv6Str = ipv6Str.substring(0, 16) + ipv6Str.substring(18);
|
||||
|
||||
for (prefixLength = 64, suffixLength = 6; prefixLength >= 32; ) {
|
||||
if (ipv4Str.equalsIgnoreCase(
|
||||
ipv6Str.substring(
|
||||
prefixLength / 4, prefixLength / 4 + 8))) {
|
||||
prefix = ipv6Str.substring(0, prefixLength / 4);
|
||||
suffix = ipv6Str.substring(ipv6Str.length() - suffixLength);
|
||||
break;
|
||||
}
|
||||
|
||||
prefixLength -= 8;
|
||||
suffixLength += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return prefix != null ? new NAT64AddrInfo(prefix, suffix) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overload for {@link #hexStringToIPv6String(StringBuilder)}.
|
||||
*
|
||||
* @param hexStr a hex representation of IPv6 address bytes.
|
||||
* @return an IPv6 address string.
|
||||
*/
|
||||
static String hexStringToIPv6String(String hexStr) {
|
||||
return hexStringToIPv6String(new StringBuilder(hexStr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from HEX representation of IPv6 address bytes into IPv6 address
|
||||
* string which includes the ':' signs.
|
||||
*
|
||||
* @param str a hex representation of IPv6 address bytes.
|
||||
* @return eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
|
||||
*/
|
||||
static String hexStringToIPv6String(StringBuilder str) {
|
||||
for (int i = 32 - 4; i > 0; i -= 4) {
|
||||
str.insert(i, ":");
|
||||
}
|
||||
|
||||
return str.toString().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an IPv4 address string and returns it's byte array representation.
|
||||
*
|
||||
* @param ipv4Address eg. '192.168.3.23'
|
||||
* @return byte representation of given IPv4 address string.
|
||||
* @throws IllegalArgumentException if the address is not in valid format.
|
||||
*/
|
||||
static byte[] ipv4AddressStringToBytes(String ipv4Address) {
|
||||
InetAddress address;
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName(ipv4Address);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid IP address: " + ipv4Address, e);
|
||||
}
|
||||
|
||||
byte[] bytes = address.getAddress();
|
||||
|
||||
if (bytes.length != 4) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not an IPv4 address: " + ipv4Address);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The NAT64 prefix added to construct IPv6 from an IPv4 address.
|
||||
*/
|
||||
private final String prefix;
|
||||
|
||||
/**
|
||||
* The NAT64 suffix (if any) used to construct IPv6 from an IPv4 address.
|
||||
*/
|
||||
private final String suffix;
|
||||
|
||||
/**
|
||||
* Creates new instance of {@link NAT64AddrInfo}.
|
||||
*
|
||||
* @param prefix the NAT64 prefix.
|
||||
* @param suffix the NAT64 suffix.
|
||||
*/
|
||||
private NAT64AddrInfo(String prefix, String suffix) {
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the NAT64 prefix and suffix will create an IPv6 representation
|
||||
* of the given IPv4 address.
|
||||
*
|
||||
* @param ipv4Address eg. '192.34.2.3'
|
||||
* @return IPv6 address string eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
|
||||
* @throws IllegalArgumentException if given string is not a valid IPv4
|
||||
* address.
|
||||
*/
|
||||
public String getIPv6Address(String ipv4Address) {
|
||||
byte[] ipv4AddressBytes = ipv4AddressStringToBytes(ipv4Address);
|
||||
StringBuilder newIPv6Str = new StringBuilder();
|
||||
|
||||
newIPv6Str.append(prefix);
|
||||
newIPv6Str.append(bytesToHexString(ipv4AddressBytes));
|
||||
|
||||
if (suffix != null) {
|
||||
// Insert the 'u' octet.
|
||||
newIPv6Str.insert(16, "00");
|
||||
newIPv6Str.append(suffix);
|
||||
}
|
||||
|
||||
return hexStringToIPv6String(newIPv6Str);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* This module exposes the functionality of creating an IPv6 representation
|
||||
* of IPv4 addresses in NAT64 environment.
|
||||
*
|
||||
* See[1] and [2] for more info on what NAT64 is.
|
||||
* [1]: https://tools.ietf.org/html/rfc6146
|
||||
* [2]: https://tools.ietf.org/html/rfc6052
|
||||
*/
|
||||
public class NAT64AddrInfoModule extends ReactContextBaseJavaModule {
|
||||
/**
|
||||
* The host for which the module wil try to resolve both IPv4 and IPv6
|
||||
* addresses in order to figure out the NAT64 prefix.
|
||||
*/
|
||||
private final static String HOST = "nat64.jitsi.net";
|
||||
|
||||
/**
|
||||
* How long is the {@link NAT64AddrInfo} instance valid.
|
||||
*/
|
||||
private final static long INFO_LIFETIME = 60 * 1000;
|
||||
|
||||
/**
|
||||
* The name of this module.
|
||||
*/
|
||||
private final static String MODULE_NAME = "NAT64AddrInfo";
|
||||
|
||||
/**
|
||||
* The {@code Log} tag {@code NAT64AddrInfoModule} is to log messages with.
|
||||
*/
|
||||
private final static String TAG = MODULE_NAME;
|
||||
|
||||
/**
|
||||
* The {@link NAT64AddrInfo} instance which holds NAT64 prefix/suffix.
|
||||
*/
|
||||
private NAT64AddrInfo info;
|
||||
|
||||
/**
|
||||
* When {@link #info} was created.
|
||||
*/
|
||||
private long infoTimestamp;
|
||||
|
||||
/**
|
||||
* Creates new {@link NAT64AddrInfoModule}.
|
||||
*
|
||||
* @param reactContext the react context to be used by the new module
|
||||
* instance.
|
||||
*/
|
||||
public NAT64AddrInfoModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to obtain IPv6 address for given IPv4 address in NAT64 environment.
|
||||
*
|
||||
* @param ipv4Address IPv4 address string.
|
||||
* @param promise a {@link Promise} which will be resolved either with IPv6
|
||||
* address for given IPv4 address or with {@code null} if no
|
||||
* {@link NAT64AddrInfo} was resolved for the current network. Will be
|
||||
* rejected if given {@code ipv4Address} is not a valid IPv4 address.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void getIPv6Address(String ipv4Address, final Promise promise) {
|
||||
// Reset if cached for too long.
|
||||
if (System.currentTimeMillis() - infoTimestamp > INFO_LIFETIME) {
|
||||
info = null;
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
String host = HOST;
|
||||
|
||||
try {
|
||||
info = NAT64AddrInfo.discover(host);
|
||||
} catch (UnknownHostException e) {
|
||||
Log.e(TAG, "NAT64AddrInfo.discover: " + host, e);
|
||||
}
|
||||
infoTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
String result;
|
||||
|
||||
try {
|
||||
result = info == null ? null : info.getIPv6Address(ipv4Address);
|
||||
} catch (IllegalArgumentException exc) {
|
||||
Log.e(TAG, "Failed to get IPv6 address for: " + ipv4Address, exc);
|
||||
|
||||
// We don't want to reject. It's not a big deal if there's no IPv6
|
||||
// address resolved.
|
||||
result = null;
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MODULE_NAME;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link NAT64AddrInfo} class.
|
||||
*/
|
||||
public class NAT64AddrInfoTest {
|
||||
/**
|
||||
* Test case for the 96 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test96Prefix() {
|
||||
testPrefixSuffix(
|
||||
"260777000000000400000000", "", "203.0.113.1", "23.17.23.3");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 64 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test64Prefix() {
|
||||
String prefix = "1FF2A227B3AAF3D2";
|
||||
String suffix = "BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "48.46.87.34", "23.87.145.4");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 56 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test56Prefix() {
|
||||
String prefix = "1FF2A227B3AAF3";
|
||||
String suffix = "A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "34.72.234.255", "1.235.3.65");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 48 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test48Prefix() {
|
||||
String prefix = "1FF2A227B3AA";
|
||||
String suffix = "72A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "97.54.3.23", "77.49.0.33");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 40 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test40Prefix() {
|
||||
String prefix = "1FF2A227B3";
|
||||
String suffix = "D972A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "10.23.56.121", "97.65.32.21");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 32 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test32Prefix()
|
||||
throws UnknownHostException {
|
||||
String prefix = "1FF2A227";
|
||||
String suffix = "20D972A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "162.63.65.189", "135.222.84.206");
|
||||
}
|
||||
|
||||
private static String buildIPv6Addr(
|
||||
String prefix, String suffix, String ipv4Hex) {
|
||||
String ipv6Str = prefix + ipv4Hex + suffix;
|
||||
|
||||
if (suffix.length() > 0) {
|
||||
ipv6Str = new StringBuilder(ipv6Str).insert(16, "00").toString();
|
||||
}
|
||||
|
||||
return ipv6Str;
|
||||
}
|
||||
|
||||
private void testPrefixSuffix(
|
||||
String prefix, String suffix, String ipv4, String otherIPv4) {
|
||||
byte[] ipv4Bytes = NAT64AddrInfo.ipv4AddressStringToBytes(ipv4);
|
||||
String ipv4String = NAT64AddrInfo.bytesToHexString(ipv4Bytes);
|
||||
String ipv6Str = buildIPv6Addr(prefix, suffix, ipv4String);
|
||||
|
||||
BigInteger ipv6Address = new BigInteger(ipv6Str, 16);
|
||||
|
||||
NAT64AddrInfo nat64AddrInfo
|
||||
= NAT64AddrInfo.figureOutNAT64AddrInfo(
|
||||
ipv4Bytes, ipv6Address.toByteArray());
|
||||
|
||||
assertNotNull("Failed to figure out NAT64 info", nat64AddrInfo);
|
||||
|
||||
String newIPv6 = nat64AddrInfo.getIPv6Address(ipv4);
|
||||
|
||||
assertEquals(
|
||||
NAT64AddrInfo.hexStringToIPv6String(ipv6Address.toString(16)),
|
||||
newIPv6);
|
||||
|
||||
byte[] ipv4Addr2 = NAT64AddrInfo.ipv4AddressStringToBytes(otherIPv4);
|
||||
String ipv4Addr2Hex = NAT64AddrInfo.bytesToHexString(ipv4Addr2);
|
||||
|
||||
newIPv6 = nat64AddrInfo.getIPv6Address(otherIPv4);
|
||||
|
||||
assertEquals(
|
||||
NAT64AddrInfo.hexStringToIPv6String(
|
||||
buildIPv6Addr(prefix, suffix, ipv4Addr2Hex)),
|
||||
newIPv6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidIPv4Format() {
|
||||
testInvalidIPv4Format("256.1.2.3");
|
||||
testInvalidIPv4Format("FE80:CD00:0000:0CDA:1357:0000:212F:749C");
|
||||
}
|
||||
|
||||
private void testInvalidIPv4Format(String ipv4Str) {
|
||||
try {
|
||||
NAT64AddrInfo.ipv4AddressStringToBytes(ipv4Str);
|
||||
fail("Did not throw IllegalArgumentException");
|
||||
} catch (IllegalArgumentException exc) {
|
||||
/* OK */
|
||||
}
|
||||
}
|
||||
}
|
||||
128
conference.js
128
conference.js
@@ -43,8 +43,7 @@ import {
|
||||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
p2pStatusChanged,
|
||||
sendLocalParticipant,
|
||||
setDesktopSharingEnabled
|
||||
sendLocalParticipant
|
||||
} from './react/features/base/conference';
|
||||
import { updateDeviceList } from './react/features/base/devices';
|
||||
import {
|
||||
@@ -94,6 +93,7 @@ import {
|
||||
getLocationContextRoot,
|
||||
getJitsiMeetGlobalNS
|
||||
} from './react/features/base/util';
|
||||
import { statsEmitter } from './react/features/connection-indicator';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
import {
|
||||
@@ -104,8 +104,10 @@ import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
} from './react/features/overlay';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { isButtonEnabled } from './react/features/toolbox';
|
||||
import {
|
||||
isButtonEnabled,
|
||||
showDesktopSharingButton
|
||||
} from './react/features/toolbox';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -232,14 +234,13 @@ function maybeRedirectToWelcomePage(options) {
|
||||
}));
|
||||
}
|
||||
|
||||
// if Welcome page is enabled redirect to welcome page after 3 sec, if
|
||||
// there is a thank you message to be shown, 0.5s otherwise.
|
||||
// if Welcome page is enabled redirect to welcome page after 3 sec.
|
||||
if (config.enableWelcomePage) {
|
||||
setTimeout(
|
||||
() => {
|
||||
APP.store.dispatch(redirectWithStoredParams('/'));
|
||||
},
|
||||
options.showThankYou ? 3000 : 500);
|
||||
3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +505,16 @@ export default {
|
||||
*/
|
||||
desktopSharingDisabledTooltip: null,
|
||||
|
||||
/*
|
||||
* Whether the local "raisedHand" flag is on.
|
||||
*/
|
||||
isHandRaised: false,
|
||||
|
||||
/*
|
||||
* Whether the local participant is the dominant speaker in the conference.
|
||||
*/
|
||||
isDominantSpeaker: false,
|
||||
|
||||
/**
|
||||
* The local audio track (if any).
|
||||
* FIXME tracks from redux store should be the single source of truth
|
||||
@@ -762,8 +773,7 @@ export default {
|
||||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
this.isDesktopSharingEnabled);
|
||||
|
||||
APP.store.dispatch(
|
||||
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
|
||||
APP.store.dispatch(showDesktopSharingButton());
|
||||
|
||||
this._createRoom(tracks);
|
||||
APP.remoteControl.init();
|
||||
@@ -1354,6 +1364,7 @@ export default {
|
||||
this.isSharingScreen = newStream && newStream.videoType === 'desktop';
|
||||
|
||||
if (wasSharingScreen !== this.isSharingScreen) {
|
||||
APP.UI.updateDesktopSharingButtons();
|
||||
APP.API.notifyScreenSharingStatusChanged(this.isSharingScreen);
|
||||
}
|
||||
},
|
||||
@@ -1376,6 +1387,37 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a tooltip to display when a feature was attempted to be used
|
||||
* while in audio only mode.
|
||||
*
|
||||
* @param {string} featureName - The name of the feature that attempted to
|
||||
* toggle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayAudioOnlyTooltip(featureName) {
|
||||
let buttonName = null;
|
||||
let tooltipElementId = null;
|
||||
|
||||
switch (featureName) {
|
||||
case 'screenShare':
|
||||
buttonName = 'desktop';
|
||||
tooltipElementId = 'screenshareWhileAudioOnly';
|
||||
break;
|
||||
case 'videoMute':
|
||||
buttonName = 'camera';
|
||||
tooltipElementId = 'unmuteWhileAudioOnly';
|
||||
break;
|
||||
}
|
||||
|
||||
if (tooltipElementId) {
|
||||
APP.UI.showToolbar(6000);
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
buttonName, tooltipElementId, true, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether or not the conference is currently in audio only mode.
|
||||
*
|
||||
@@ -1486,6 +1528,8 @@ export default {
|
||||
}
|
||||
|
||||
if (this.isAudioOnly()) {
|
||||
this._displayAudioOnlyTooltip('screenShare');
|
||||
|
||||
return Promise.reject('No screensharing in audio only mode');
|
||||
}
|
||||
|
||||
@@ -1827,6 +1871,9 @@ export default {
|
||||
|
||||
room.on(JitsiConferenceEvents.TALK_WHILE_MUTED, () => {
|
||||
APP.UI.showToolbar(6000);
|
||||
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'microphone', 'talkWhileMutedPopup', true, 5000);
|
||||
});
|
||||
|
||||
room.on(
|
||||
@@ -1849,6 +1896,19 @@ export default {
|
||||
});
|
||||
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => {
|
||||
APP.store.dispatch(dominantSpeakerChanged(id));
|
||||
|
||||
if (this.isLocalId(id)) {
|
||||
this.isDominantSpeaker = true;
|
||||
this.setRaisedHand(false);
|
||||
} else {
|
||||
this.isDominantSpeaker = false;
|
||||
const participant = room.getParticipantById(id);
|
||||
|
||||
if (participant) {
|
||||
APP.UI.setRaisedHandStatus(participant, false);
|
||||
}
|
||||
}
|
||||
APP.UI.markDominantSpeaker(id);
|
||||
});
|
||||
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
@@ -1903,6 +1963,10 @@ export default {
|
||||
reportError(e);
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
|
||||
() => this._displayAudioOnlyTooltip('videoMute'));
|
||||
}
|
||||
|
||||
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||
@@ -1958,10 +2022,7 @@ export default {
|
||||
(participant, name, oldValue, newValue) => {
|
||||
switch (name) {
|
||||
case 'raisedHand':
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: participant.getId(),
|
||||
raisedHand: newValue === 'true'
|
||||
}));
|
||||
APP.UI.setRaisedHandStatus(participant, newValue);
|
||||
break;
|
||||
case 'remoteControlSessionStatus':
|
||||
APP.UI.setRemoteControlActiveStatus(
|
||||
@@ -2020,13 +2081,26 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.DTMF_SUPPORT_CHANGED,
|
||||
isDTMFSupported => {
|
||||
APP.UI.updateDTMFSupport(isDTMFSupported);
|
||||
}
|
||||
);
|
||||
|
||||
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
|
||||
this.muteAudio(muted);
|
||||
});
|
||||
APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
|
||||
this.muteVideo(muted);
|
||||
if (this.isAudioOnly() && !muted) {
|
||||
this._displayAudioOnlyTooltip('videoMute');
|
||||
} else {
|
||||
this.muteVideo(muted);
|
||||
}
|
||||
});
|
||||
|
||||
statsEmitter.startListeningForStats(room);
|
||||
|
||||
room.addCommandListener(this.commands.defaults.ETHERPAD,
|
||||
({ value }) => {
|
||||
APP.UI.initEtherpad(value);
|
||||
@@ -2287,8 +2361,6 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
APP.store.dispatch(setSharedVideoStatus(state));
|
||||
});
|
||||
room.addCommandListener(
|
||||
this.commands.defaults.SHARED_VIDEO,
|
||||
@@ -2551,6 +2623,30 @@ export default {
|
||||
APP.API.notifyVideoAvailabilityChanged(available);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the local "raised hand" status.
|
||||
*/
|
||||
maybeToggleRaisedHand() {
|
||||
this.setRaisedHand(!this.isHandRaised);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the local "raised hand" status to a particular value.
|
||||
*/
|
||||
setRaisedHand(raisedHand) {
|
||||
if (raisedHand !== this.isHandRaised) {
|
||||
APP.UI.onLocalRaiseHandChanged(raisedHand);
|
||||
|
||||
this.isHandRaised = raisedHand;
|
||||
|
||||
// Advertise the updated status
|
||||
room.setLocalParticipantProperty('raisedHand', raisedHand);
|
||||
|
||||
// Update the view
|
||||
APP.UI.setLocalRaisedHandStatus(raisedHand);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disconnect from the conference and optionally request user feedback.
|
||||
* @param {boolean} [requestFeedback=false] if user feedback should be
|
||||
|
||||
21
config.js
21
config.js
@@ -57,9 +57,6 @@ var config = {
|
||||
// P2P test mode disables automatic switching to P2P when there are 2
|
||||
// participants in the conference.
|
||||
p2pTestMode: false
|
||||
|
||||
// Enables the test specific features consumed by jitsi-meet-torture
|
||||
// testMode: false
|
||||
},
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
@@ -181,23 +178,6 @@ var config = {
|
||||
// Disables or enables RTX (RFC 4588) (defaults to false).
|
||||
// disableRtx: false,
|
||||
|
||||
// Disables or enables TCC (the default is in Jicofo and set to true)
|
||||
// (draft-holmer-rmcat-transport-wide-cc-extensions-01). This setting
|
||||
// affects congestion control, it practically enables send-side bandwidth
|
||||
// estimations.
|
||||
// enableTcc: true,
|
||||
|
||||
// Disables or enables REMB (the default is in Jicofo and set to false)
|
||||
// (draft-alvestrand-rmcat-remb-03). This setting affects congestion
|
||||
// control, it practically enables recv-side bandwidth estimations. When
|
||||
// both TCC and REMB are enabled, TCC takes precedence. When both are
|
||||
// disabled, then bandwidth estimations are disabled.
|
||||
// enableRemb: false,
|
||||
|
||||
// Defines the minimum number of participants to start a call (the default
|
||||
// is set in Jicofo and set to 2).
|
||||
// minParticipants: 2,
|
||||
|
||||
// Use XEP-0215 to fetch STUN and TURN servers.
|
||||
// useStunTurn: true,
|
||||
|
||||
@@ -342,6 +322,7 @@ var config = {
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
alwaysVisibleToolbar
|
||||
autoEnableDesktopSharing
|
||||
autoRecord
|
||||
autoRecordToken
|
||||
debug
|
||||
|
||||
@@ -3,17 +3,11 @@
|
||||
* none is applied. Other browsers already support selecting within inputs while
|
||||
* user-select is none. As such, disallow user-select except on inputs.
|
||||
*/
|
||||
* {
|
||||
*:not(input) {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
@@ -28,16 +22,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
|
||||
* preventing transparency in filmstrip-only mode. The selector chosen to
|
||||
* override this behavior is specific to where the AtlasKitThemeProvider might
|
||||
* be placed within the app hierarchy.
|
||||
*/
|
||||
.filmstrip-only #react > .ckAJgx {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -62,36 +62,6 @@
|
||||
.localuser {
|
||||
color: #087dba;
|
||||
}
|
||||
.use-new-toolbox {
|
||||
.chatmessage {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.localuser {
|
||||
color: #4C9AFF;
|
||||
}
|
||||
|
||||
.remoteuser {
|
||||
color: #B8C7E0;
|
||||
}
|
||||
|
||||
#usermsg {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chatmessage,
|
||||
#smileysarea,
|
||||
#smileysContainer,
|
||||
#usermsg {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
}
|
||||
|
||||
.smileyContainer:hover {
|
||||
background-color: $newToolbarButtonToggleColor;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: red;
|
||||
|
||||
59
css/_contact_list.scss
Normal file
59
css/_contact_list.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
#contacts_container {
|
||||
cursor: default;
|
||||
|
||||
#contacts {
|
||||
font-size: 12px;
|
||||
bottom: 0px;
|
||||
margin: 0;
|
||||
margin-top: 12px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#contacts {
|
||||
.contact-list-item {
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
color: $baseLight;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
list-style-type: none;
|
||||
padding: 0 10%;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: $toolbarSelectBackground;
|
||||
}
|
||||
|
||||
.contact-list-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
margin: 0 8px 0 4px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.avatar {
|
||||
padding: 0px;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
font-size: 22pt;
|
||||
border-radius: 20px;
|
||||
max-height: 30px;
|
||||
max-width: 30px;
|
||||
}
|
||||
@@ -5,20 +5,6 @@
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.use-new-toolbox {
|
||||
.filmstrip.reduce-height {
|
||||
bottom: $newToolbarSizeWithPadding;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
transition: bottom .3s;
|
||||
}
|
||||
|
||||
.filmstrip__videos.hidden {
|
||||
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -80,6 +66,37 @@
|
||||
&#filmstripLocalVideo {
|
||||
align-self: flex-end;
|
||||
display: block;
|
||||
|
||||
/**
|
||||
* The invite button style.
|
||||
*/
|
||||
.filmstrip__invite {
|
||||
padding-bottom: 5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
* The invite button group style.
|
||||
* TOFIX: use AtlasKit.ButtonGroup if it starts supporting different
|
||||
* flex grow options for the buttons.
|
||||
*/
|
||||
.invite-button-group {
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
& button {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
|
||||
@@ -180,18 +180,3 @@
|
||||
.icon-gsm-bars:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-open_in_new:before {
|
||||
content: "\e89e";
|
||||
}
|
||||
.icon-AUD:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-HD:before {
|
||||
content: "\e927";
|
||||
}
|
||||
.icon-LD:before {
|
||||
content: "\e928";
|
||||
}
|
||||
.icon-SD:before {
|
||||
content: "\e929";
|
||||
}
|
||||
|
||||
@@ -1,37 +1,6 @@
|
||||
/**
|
||||
* Toolbar side panel main container element.
|
||||
*/
|
||||
.use-new-toolbox #sideToolbarContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar and subtract from 100%.
|
||||
*/
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
left: 0;
|
||||
|
||||
.side-toolbar-close {
|
||||
background: gray;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 100%;
|
||||
color: white;
|
||||
cursor:pointer;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#chatconversation {
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
#sideToolbarContainer {
|
||||
background-color: $sideToolbarContainerBg;
|
||||
height: 100%;
|
||||
@@ -75,7 +44,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner container, for example settings or profile.
|
||||
* Inner container, for example contact list, settings or profile.
|
||||
*/
|
||||
.sideToolbarContainer__inner {
|
||||
display: none;
|
||||
|
||||
@@ -19,17 +19,6 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.use-new-toolbox {
|
||||
.cxGWJB{
|
||||
bottom: calc(#{$newToolbarSizeWithPadding});
|
||||
}
|
||||
.gXSEsl:nth-child(n+2) {
|
||||
transform: translateX(0) translateY(100%) translateY(16px);
|
||||
-ms-transform: translateX(0) translateY(100%) translateY(16px);
|
||||
-webkit-transform: translateX(0) translateY(100%) translateY(16px);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
display: block;
|
||||
left:0;
|
||||
@@ -272,261 +261,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: when the old filmstrip has been removed, remove the "new-" prefix.
|
||||
*/
|
||||
.new-toolbox {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
bottom: calc((#{$newToolbarSize} * 2) * -1);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px;
|
||||
position: absolute;
|
||||
transition: bottom .3s ease-in;
|
||||
width: 100%;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
&.visible {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&.no-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-group-center,
|
||||
.button-group-left,
|
||||
.button-group-right {
|
||||
display: flex;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.button-group-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite font-awesome styling to match jitsi-icon styling.
|
||||
*/
|
||||
.fa {
|
||||
font-size: 1.22em;
|
||||
}
|
||||
|
||||
i {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: $newToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.disabled {
|
||||
cursor: initial
|
||||
}
|
||||
|
||||
i.disabled:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.overflow-menu {
|
||||
font-size: 1.2em;
|
||||
list-style-type: none;
|
||||
/**
|
||||
* Undo atlaskit padding by reducing margins.
|
||||
*/
|
||||
margin: -15px -24px;
|
||||
padding: 4px 0;
|
||||
|
||||
.overflow-menu-item {
|
||||
align-items: center;
|
||||
color: #B8C7E0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 22px;
|
||||
padding: 5px 12px;
|
||||
|
||||
&:hover {
|
||||
background: #313D52;
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
line-height: $newToolbarSize;
|
||||
margin: 0 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toolbar-button-with-badge {
|
||||
position: relative;
|
||||
|
||||
.badge-round {
|
||||
bottom: 9px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
min-width: 20px;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button-wth-dialog {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
height: $newToolbarSize;
|
||||
font-size: 24px;
|
||||
width: $newToolbarSize;
|
||||
}
|
||||
}
|
||||
|
||||
.always-on-top-toolbox,
|
||||
.filmstrip-toolbox {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: $newToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
i.toggled:hover:not(.disabled) {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.always-on-top-toolbox {
|
||||
flex-direction: row;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translateX(-50%);
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
font-size: $alwaysOnTopToolbarFontSize;
|
||||
height: $alwaysOnTopToolbarSize;
|
||||
line-height: $alwaysOnTopToolbarSize;
|
||||
width: $alwaysOnTopToolbarSize;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-toolbox {
|
||||
i {
|
||||
font-size: 1.9em;
|
||||
height: 37px;
|
||||
line-height: 37px;
|
||||
width: 37px;
|
||||
}
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-only {
|
||||
.toolbox,
|
||||
.toolbox-toolbars {
|
||||
|
||||
@@ -36,12 +36,6 @@ $alwaysOnTopToolbarFontSize: 1em;
|
||||
$alwaysOnTopToolbarSize: 30px;
|
||||
$defaultToolbarSize: 50px;
|
||||
$defaultFilmStripOnlyToolbarSize: 37px;
|
||||
$newToolbarBackgroundColor: rgba(22, 38, 55, 0.8);
|
||||
$newToolbarButtonHoverColor: rgba(14, 20, 35, 0.6);
|
||||
$newToolbarButtonToggleColor: rgba(14, 20, 35, 1);
|
||||
$newToolbarFontSize: 1.9em;
|
||||
$newToolbarSize: 32px;
|
||||
$newToolbarSizeWithPadding: calc(32px + 32px);
|
||||
$secToolbarFontSize: 1.9em;
|
||||
$secToolbarLineHeight: 45px;
|
||||
$toolbarAvatarPadding: 10px;
|
||||
@@ -80,6 +74,10 @@ $feedbackInputBg: #fff;
|
||||
$feedbackTextColor: #000;
|
||||
$feedbackInputTextColor: #333;
|
||||
$feedbackInputPlaceholderColor: #777;
|
||||
$rateStarLabelColor: #333;
|
||||
$rateStarDefault: #ccc;
|
||||
$rateStarActivity: #165ecc;
|
||||
$rateStarSize: 34px;
|
||||
|
||||
/**
|
||||
* Modals
|
||||
|
||||
@@ -19,20 +19,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.use-new-toolbox {
|
||||
/**
|
||||
* Adjust the height of the filmstrip as the toolbar is displayed.
|
||||
*/
|
||||
.filmstrip {
|
||||
top: 0;
|
||||
transition: height .3s ease-in;
|
||||
|
||||
&.reduce-height {
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
@@ -46,7 +32,14 @@
|
||||
* any parent is also fixed.
|
||||
*/
|
||||
position: fixed;
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
/**
|
||||
* z-index adjusting is needed because the video state indicator has to
|
||||
* display over the filmstrip when no videos are displayed but still be
|
||||
* clickable but its inline dialogs must display over the video state
|
||||
* indicator when videos are displayed.
|
||||
*/
|
||||
z-index: #{$tooltipsZ + 1};
|
||||
|
||||
/**
|
||||
* Hide videos by making them slight to the right.
|
||||
@@ -75,7 +68,8 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-styles the local Video to better fit vertical filmstrip layout.
|
||||
* Re-styles the local Video and invite button to better fit the
|
||||
* vertical filmstrip layout.
|
||||
*/
|
||||
#filmstripLocalVideo {
|
||||
align-self: initial;
|
||||
@@ -84,6 +78,12 @@
|
||||
flex-direction: column-reverse;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
|
||||
.filmstrip__invite {
|
||||
padding-bottom: 0px;
|
||||
padding-top: 5px;
|
||||
z-index: $dropdownZ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
body.welcome-page {
|
||||
background: inherit;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
font-family: $welcomePageFontFamily;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
@@ -23,7 +19,6 @@ body.welcome-page {
|
||||
justify-content: space-around;
|
||||
margin-top: 120px;
|
||||
margin-bottom: 20px;
|
||||
max-width: calc(100% - 40px);
|
||||
min-height: 286px;
|
||||
width: 645px;
|
||||
}
|
||||
@@ -55,7 +50,6 @@ body.welcome-page {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
max-width: calc(100% - 40px);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.deep-linking-desktop {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 55px;
|
||||
background-color: #f1f2f5;
|
||||
padding-top: 15px;
|
||||
padding-left: 50px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 0 0 55px;
|
||||
.logo {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-flow: row;
|
||||
.leftColumn {
|
||||
left: 0px;
|
||||
width: 50%;
|
||||
min-height: 156px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.leftColumnContent{
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
.image {
|
||||
background-image: url('../images/deep-linking-image.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.rightColumn {
|
||||
top: 0px;
|
||||
width: 50%;
|
||||
min-height: 156px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
.rightColumnContent {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 20px 20px 20px 60px;
|
||||
.title {
|
||||
color: #1c2946;
|
||||
}
|
||||
.description {
|
||||
color: #606a80;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@import 'desktop';
|
||||
@import 'mobile';
|
||||
@import 'no-mobile-app';
|
||||
@@ -48,6 +48,7 @@
|
||||
@import 'popup_menu';
|
||||
@import 'recording';
|
||||
@import 'login_menu';
|
||||
@import 'contact_list';
|
||||
@import 'chat';
|
||||
@import 'ringing/ringing';
|
||||
@import 'welcome_page';
|
||||
@@ -71,6 +72,5 @@
|
||||
@import 'unsupported-browser/main';
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'vertical_filmstrip_overrides';
|
||||
@import 'deep-linking/main';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
|
||||
.feedback-dialog {
|
||||
.details {
|
||||
margin-top: 20px;
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
@@ -73,15 +77,16 @@
|
||||
text-align: center;
|
||||
|
||||
.star-label {
|
||||
color: $rateStarLabelColor;
|
||||
font-size: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.star-btn {
|
||||
color: inherit;
|
||||
color: $rateStarDefault;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 34px;
|
||||
font-size: $rateStarSize;
|
||||
outline: none;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
@@ -90,7 +95,7 @@
|
||||
&.active,
|
||||
&:hover,
|
||||
&.starHover {
|
||||
color: #36B37E;
|
||||
color: $rateStarActivity;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -13,15 +13,7 @@
|
||||
}
|
||||
|
||||
.add-telephone-icon {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
transform: scaleX(-1);
|
||||
width: 28px;
|
||||
|
||||
i {
|
||||
line-height: 28px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.use-new-toolbox {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-dialog {
|
||||
cursor: default;
|
||||
display: flex;
|
||||
@@ -42,7 +38,7 @@
|
||||
.info-dialog-copy-element {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
@@ -60,11 +56,11 @@
|
||||
}
|
||||
|
||||
.info-dialog-conference-url {
|
||||
width: max-content;
|
||||
width: -moz-max-content;
|
||||
width: -webkit-max-content;
|
||||
word-break: break-all;
|
||||
max-width: 400px;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: text;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-dialog-dial-in {
|
||||
@@ -81,29 +77,19 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-dialog-invite-link,
|
||||
.info-dialog-invite-link:hover {
|
||||
color: inherit;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.info-dialog-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-dialog-password,
|
||||
.info-password,
|
||||
.info-dialog-password,
|
||||
.info-password-form {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-password-field {
|
||||
margin-left: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -124,6 +110,10 @@
|
||||
.info-password-local {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.conference-id {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.dial-in-page {
|
||||
@@ -135,6 +125,10 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.dial-in-numbers-list {
|
||||
font-size: 24px;
|
||||
margin-top: 20px;
|
||||
@@ -145,12 +139,3 @@
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog,
|
||||
.dial-in-page {
|
||||
* {
|
||||
user-select: text;
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,12 +135,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog-form {
|
||||
.video-quality-dialog-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.video-state-indicator {
|
||||
background: $videoStateIndicatorBackground;
|
||||
cursor: default;
|
||||
@@ -168,11 +162,11 @@
|
||||
}
|
||||
|
||||
.centeredVideoLabel.moveToCorner {
|
||||
z-index: $zindex3;
|
||||
z-index: $tooltipsZ;
|
||||
}
|
||||
|
||||
#videoResolutionLabel {
|
||||
z-index: $zindex3 + 1;
|
||||
z-index: #{$tooltipsZ + 1};
|
||||
}
|
||||
|
||||
.centeredVideoLabel {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
@import 'no-mobile-app';
|
||||
@import 'unsupported-desktop-browser';
|
||||
@import 'unsupported-mobile-browser';
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
.deep-linking-mobile {
|
||||
.unsupported-mobile-browser {
|
||||
background-color: #fff;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background-color: #f1f2f5;
|
||||
text-align: center;
|
||||
.logo {
|
||||
margin-top: 15px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none
|
||||
}
|
||||
@@ -33,19 +20,10 @@
|
||||
a:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-weight: bolder;
|
||||
padding: 10px 10px 0px 10px;
|
||||
}
|
||||
|
||||
&__text,
|
||||
.deep-linking-dial-in {
|
||||
.unsupported-dial-in {
|
||||
font-size: 1.2em;
|
||||
line-height: em(29px, 21px);
|
||||
margin-bottom: 0.65em;
|
||||
@@ -61,27 +39,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__href {
|
||||
height: 2.2857142857142856em;
|
||||
line-height: 2.2857142857142856em;
|
||||
margin: 18px auto 20px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
font-weight: bolder;
|
||||
&__logo {
|
||||
height: 108px;
|
||||
width: 77px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border: 0;
|
||||
height: 2.2857142857142856em;
|
||||
line-height: 2.2857142857142856em;
|
||||
margin: 18px auto 10px;
|
||||
padding: 0px 10px 0px 10px;
|
||||
margin: 18px auto 20px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
@include border-radius(3px);
|
||||
background-color: $unsupportedBrowserButtonBgColor;
|
||||
color: #505F79;
|
||||
font-weight: bold;
|
||||
|
||||
&:active {
|
||||
background-color: $unsupportedBrowserButtonBgColor;
|
||||
@@ -97,7 +69,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.deep-linking-dial-in {
|
||||
.unsupported-dial-in {
|
||||
display: none;
|
||||
|
||||
&.has-numbers {
|
||||
@@ -97,6 +97,11 @@ api.executeCommand('toggleFilmStrip')
|
||||
api.executeCommand('toggleChat')
|
||||
```
|
||||
|
||||
* **toggleContactList** - Hides / shows the contact list. No arguments are required.
|
||||
```javascript
|
||||
api.executeCommand('toggleContactList')
|
||||
```
|
||||
|
||||
* **toggleShareScreen** - Starts / stops screen sharing. No arguments are required.
|
||||
```javascript
|
||||
api.executeCommand('toggleShareScreen')
|
||||
|
||||
@@ -20,14 +20,6 @@ wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | apt-key add -
|
||||
apt-get update
|
||||
```
|
||||
|
||||
(If you get an error:
|
||||
E: The method driver /usr/lib/apt/methods/https could not be found.
|
||||
run:
|
||||
```sh
|
||||
apt-get install apt-transport-https
|
||||
```
|
||||
)
|
||||
|
||||
### Install Jitsi Meet
|
||||
|
||||
Note : Something to consider before installation is how you're planning to serve Jitsi Meet. The installer will check if Nginx or Apache is present (with this order) and configure a virtualhost within the web server it finds to serve Jitsi Meet. If none of the above is found it then configures itself to be served via jetty. So if for example you are planning on deploying Jitsi Meet with a web server, you have to make sure to install the server **before** installing jitsi-meet.
|
||||
|
||||
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
@@ -23,9 +23,7 @@
|
||||
<glyph unicode="" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
|
||||
<glyph unicode="" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
|
||||
<glyph unicode="" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="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="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="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
|
||||
<glyph unicode="" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
|
||||
@@ -64,7 +62,4 @@
|
||||
<glyph unicode="" glyph-name="visibility-off" d="M506 640h6c70 0 128-58 128-128v-8zM322 606c-14-28-24-60-24-94 0-118 96-214 214-214 34 0 66 10 94 24l-66 66c-8-2-18-4-28-4-70 0-128 58-128 128 0 10 2 20 4 28zM86 842l54 54 756-756-54-54c-47.968 47.365-96.266 94.401-144 142-58-24-120-36-186-36-214 0-396 132-470 320 34 84 90 156 160 212-39.017 38.983-77.307 78.693-116 118zM512 726c-28 0-54-6-78-16l-92 92c52 20 110 30 170 30 214 0 394-132 468-320-32-80-82-148-146-202l-124 124c10 24 16 50 16 78 0 118-96 214-214 214z" />
|
||||
<glyph unicode="" glyph-name="dialpad" d="M512 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 810c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86zM256 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 214c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86z" />
|
||||
<glyph unicode="" glyph-name="gsm-bars-black" d="M896 1024c70.692 0 128-57.308 128-128v-768c0-70.692-57.308-128-128-128s-128 57.308-128 128v768c0 70.692 57.308 128 128 128zM512 768c70.692 0 128-57.308 128-128v-512c0-70.692-57.308-128-128-128s-128 57.308-128 128v512c0 70.692 57.308 128 128 128zM128 384v0c70.692 0 128-57.308 128-128v-128c0-70.692-57.308-128-128-128s-128 57.308-128 128v128c0 70.692 57.308 128 128 128v0z" />
|
||||
<glyph unicode="" glyph-name="HD" 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-512zM481.359 384v255.823h-54.273v-103.18h-116.813v103.18h-54.273v-255.823h54.273v106.903h116.813v-106.903h54.273zM544.258 640v-256h102.077c77.636 0 121.665 46.626 121.665 129.773 0 80.133-44.569 126.227-121.665 126.227h-102.077zM598.531 594.26v-164.521h39.177c47.983 0 74.94 29.075 74.94 83.147 0 51.767-27.855 81.374-74.94 81.374h-39.177z" />
|
||||
<glyph unicode="" glyph-name="LD" 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-512zM472.4 433.325h-112.35v206.5h-52.85v-252.525h165.2v46.025zM520.35 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.4zM573.2 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.15z" />
|
||||
<glyph unicode="" glyph-name="SD" 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-512zM281.6 451.175c1.925-47.075 40.95-76.65 101.15-76.65 63.35 0 102.375 31.15 102.375 82.075 0 39.2-21.875 61.075-72.625 71.75l-30.45 6.475c-29.575 6.3-41.65 15.225-41.65 30.8 0 19.25 17.5 31.5 43.925 31.5 25.55 0 44.1-13.3 46.55-33.25h49.7c-1.575 44.975-40.95 76.125-96.6 76.125-58.275 0-96.6-31.325-96.6-78.925 0-38.5 22.575-62.475 68.6-72.1l32.9-7c30.975-6.65 43.575-15.925 43.575-32.025 0-19.075-19.425-32.375-46.9-32.375-29.75 0-50.4 13.125-52.85 33.6h-51.1zM535 633.7v-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.4zM587.85 588.55v-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.15z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 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.
2393
fonts/selection.json
2393
fonts/selection.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -23,11 +23,9 @@ var interfaceConfig = {
|
||||
SHOW_BRAND_WATERMARK: false,
|
||||
BRAND_WATERMARK_LINK: '',
|
||||
SHOW_POWERED_BY: false,
|
||||
SHOW_DEEP_LINKING_IMAGE: false,
|
||||
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
|
||||
DISPLAY_WELCOME_PAGE_CONTENT: true,
|
||||
APP_NAME: 'Jitsi Meet',
|
||||
NATIVE_APP_NAME: 'Jitsi Meet',
|
||||
LANG_DETECTION: false, // Allow i18n to detect the system language
|
||||
INVITATION_POWERED_BY: true,
|
||||
|
||||
@@ -46,18 +44,22 @@ var interfaceConfig = {
|
||||
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
|
||||
|
||||
// extended toolbar
|
||||
'profile', 'info', 'chat', 'recording', 'etherpad',
|
||||
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
|
||||
'invite', 'feedback', 'stats', 'shortcuts'
|
||||
],
|
||||
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ],
|
||||
|
||||
/**
|
||||
* Main Toolbar Buttons
|
||||
* All of them should be in TOOLBAR_BUTTONS
|
||||
*/
|
||||
MAIN_TOOLBAR_BUTTONS: [ 'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup' ],
|
||||
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
|
||||
INVITE_OPTIONS: [ 'invite', 'dialout', 'addtocall' ],
|
||||
|
||||
// Determines how the video would fit the screen. 'both' would fit the whole
|
||||
// screen, 'height' would fit the original video height to the height of the
|
||||
// screen, 'width' would fit the original video width to the width of the
|
||||
// screen respecting ratio.
|
||||
VIDEO_LAYOUT_FIT: 'both',
|
||||
SHOW_CONTACTLIST_AVATARS: true,
|
||||
|
||||
/**
|
||||
* Whether to only show the filmstrip (and hide the toolbar).
|
||||
@@ -163,7 +165,7 @@ var interfaceConfig = {
|
||||
/**
|
||||
* Specify mobile app scheme for opening the app from the mobile browser.
|
||||
*/
|
||||
// APP_SCHEME: 'org.jitsi.meet'
|
||||
// MOBILE_APP_SCHEME: 'org.jitsi.meet'
|
||||
};
|
||||
|
||||
/* eslint-enable no-unused-vars, no-var, max-len */
|
||||
|
||||
@@ -43,10 +43,10 @@ PODS:
|
||||
- React/Core
|
||||
- React/fishhook
|
||||
- React/RCTBlob
|
||||
- RNSound (0.10.9):
|
||||
- RNSound (0.10.4):
|
||||
- React/Core
|
||||
- RNSound/Core (= 0.10.9)
|
||||
- RNSound/Core (0.10.9):
|
||||
- RNSound/Core (= 0.10.4)
|
||||
- RNSound/Core (0.10.4):
|
||||
- React/Core
|
||||
- RNVectorIcons (4.4.2):
|
||||
- React
|
||||
@@ -103,7 +103,7 @@ SPEC CHECKSUMS:
|
||||
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
|
||||
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
|
||||
react-native-webrtc: bc044ca9530fc802e7533f247aa08fe1b6bf8dc5
|
||||
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
|
||||
RNSound: d0818fe2435254fe30540fae48a429c5ffb72e09
|
||||
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
|
||||
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -287,7 +286,6 @@
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>See your scheduled conferences in the app.</string>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>See your scheduled conferences in the app.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Participate in conferences with video.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
|
||||
@@ -51,8 +51,6 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
||||
@@ -21,73 +21,19 @@ class ViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var videoButton: UIButton?
|
||||
|
||||
fileprivate var pipViewCoordinator: PiPViewCoordinator?
|
||||
fileprivate var jitsiMeetView: JitsiMeetView?
|
||||
private var jitsiMeetCoordinator: JitsiMeetPresentationCoordinator?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize,
|
||||
with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
let rect = CGRect(origin: CGPoint.zero, size: size)
|
||||
pipViewCoordinator?.resetBounds(bounds: rect)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func openJitsiMeet(sender: Any?) {
|
||||
cleanUp()
|
||||
|
||||
// create and configure jitsimeet view
|
||||
let jitsiMeetView = JitsiMeetView()
|
||||
jitsiMeetView.welcomePageEnabled = true
|
||||
jitsiMeetView.pictureInPictureEnabled = true
|
||||
jitsiMeetView.load(nil)
|
||||
jitsiMeetView.delegate = self
|
||||
self.jitsiMeetView = jitsiMeetView
|
||||
|
||||
// Enable jitsimeet view to be a view that can be displayed
|
||||
// on top of all the things, and let the coordinator to manage
|
||||
// the view state and interactions
|
||||
pipViewCoordinator = PiPViewCoordinator(withView: jitsiMeetView)
|
||||
pipViewCoordinator?.configureAsStickyView(withParentView: view)
|
||||
|
||||
// animate in
|
||||
jitsiMeetView.alpha = 0
|
||||
pipViewCoordinator?.show()
|
||||
}
|
||||
|
||||
fileprivate func cleanUp() {
|
||||
jitsiMeetView?.removeFromSuperview()
|
||||
jitsiMeetView = nil
|
||||
pipViewCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController: JitsiMeetViewDelegate {
|
||||
|
||||
func conferenceFailed(_ data: [AnyHashable : Any]!) {
|
||||
hideJitsiMeetViewAndCleanUp()
|
||||
}
|
||||
|
||||
func conferenceLeft(_ data: [AnyHashable : Any]!) {
|
||||
hideJitsiMeetViewAndCleanUp()
|
||||
}
|
||||
|
||||
func enterPicture(inPicture data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.pipViewCoordinator?.enterPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
private func hideJitsiMeetViewAndCleanUp() {
|
||||
DispatchQueue.main.async {
|
||||
self.pipViewCoordinator?.hide() { _ in
|
||||
self.cleanUp()
|
||||
}
|
||||
}
|
||||
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
|
||||
self.jitsiMeetCoordinator = jitsiMeetCoordinator
|
||||
jitsiMeetCoordinator.jitsiMeetView().welcomePageEnabled = true
|
||||
jitsiMeetCoordinator.jitsiMeetView().load(nil)
|
||||
jitsiMeetCoordinator.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -27,12 +27,12 @@
|
||||
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0BCA496B1EC4BBF900B793EE /* jitsi.ttf */; };
|
||||
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; };
|
||||
75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; };
|
||||
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
|
||||
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
|
||||
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
|
||||
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* PiPWindow.swift */; };
|
||||
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */; };
|
||||
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
|
||||
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; };
|
||||
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -60,14 +60,14 @@
|
||||
0BD906E51EC0C00300C8C18E /* JitsiMeet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JitsiMeet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeet.h; sourceTree = "<group>"; };
|
||||
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
|
||||
75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
|
||||
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
|
||||
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
|
||||
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
|
||||
C6A3425C204EF76800E062DD /* PiPWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiPWindow.swift; sourceTree = "<group>"; };
|
||||
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetPresentationCoordinator.swift; sourceTree = "<group>"; };
|
||||
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
|
||||
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
|
||||
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = "<group>"; };
|
||||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -89,8 +89,6 @@
|
||||
0BCA49681EC4BBE500B793EE /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
75635B0820751D6D00F29C9F /* joined.wav */,
|
||||
75635B0920751D6D00F29C9F /* left.wav */,
|
||||
0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */,
|
||||
C6245F5B2053091D0040BE68 /* image-resize@2x.png */,
|
||||
C6245F5C2053091D0040BE68 /* image-resize@3x.png */,
|
||||
@@ -167,7 +165,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C6A3425E204EF76800E062DD /* DragGestureController.swift */,
|
||||
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */,
|
||||
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */,
|
||||
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */,
|
||||
C6A3425C204EF76800E062DD /* PiPWindow.swift */,
|
||||
);
|
||||
path = "picture-in-picture";
|
||||
sourceTree = "<group>";
|
||||
@@ -252,8 +252,6 @@
|
||||
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */,
|
||||
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */,
|
||||
0BC4B8691F8C03A700CE8B21 /* CallKitIcon.png in Resources */,
|
||||
75635B0B20751D6D00F29C9F /* left.wav in Resources */,
|
||||
75635B0A20751D6D00F29C9F /* joined.wav in Resources */,
|
||||
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -339,12 +337,14 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
|
||||
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */,
|
||||
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
|
||||
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
|
||||
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
|
||||
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
|
||||
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
|
||||
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */,
|
||||
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */,
|
||||
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
|
||||
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
|
||||
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
|
||||
@@ -405,7 +405,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -458,7 +458,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
@@ -29,13 +29,8 @@ RCT_EXPORT_MODULE();
|
||||
- (NSDictionary *)constantsToExport {
|
||||
NSDictionary<NSString *, id> *infoDictionary
|
||||
= [[NSBundle mainBundle] infoDictionary];
|
||||
|
||||
// calendarEnabled
|
||||
BOOL calendarEnabled
|
||||
= infoDictionary[@"NSCalendarsUsageDescription"] != nil;
|
||||
|
||||
// name
|
||||
NSString *name = infoDictionary[@"CFBundleDisplayName"];
|
||||
NSString *version = infoDictionary[@"CFBundleShortVersionString"];
|
||||
|
||||
if (name == nil) {
|
||||
name = infoDictionary[@"CFBundleName"];
|
||||
@@ -43,13 +38,6 @@ RCT_EXPORT_MODULE();
|
||||
name = @"";
|
||||
}
|
||||
}
|
||||
|
||||
// sdkBundlePath
|
||||
NSString *sdkBundlePath = [[NSBundle bundleForClass:self.class] bundlePath];
|
||||
|
||||
// version
|
||||
NSString *version = infoDictionary[@"CFBundleShortVersionString"];
|
||||
|
||||
if (version == nil) {
|
||||
version = infoDictionary[@"CFBundleVersion"];
|
||||
if (version == nil) {
|
||||
@@ -58,9 +46,7 @@ RCT_EXPORT_MODULE();
|
||||
}
|
||||
|
||||
return @{
|
||||
@"calendarEnabled": [NSNumber numberWithBool:calendarEnabled],
|
||||
@"name": name,
|
||||
@"sdkBundlePath": sdkBundlePath,
|
||||
@"version": version
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Coordinates the presentation of JitsiMeetViewController inside of
|
||||
/// an external window that can be resized and dragged with custom PiP mode
|
||||
open class JitsiMeetPresentationCoordinator: NSObject {
|
||||
|
||||
fileprivate let meetViewController: JitsiMeetViewController
|
||||
fileprivate let meetWindow: PiPWindow
|
||||
|
||||
public init(meetViewController: JitsiMeetViewController? = nil,
|
||||
meetWindow: PiPWindow? = nil) {
|
||||
self.meetViewController = meetViewController ?? JitsiMeetViewController()
|
||||
self.meetWindow = meetWindow ?? PiPWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
super.init()
|
||||
|
||||
configureMeetWindow()
|
||||
configureMeetViewController()
|
||||
}
|
||||
|
||||
public func jitsiMeetView() -> JitsiMeetView {
|
||||
return meetViewController.jitsiMeetView
|
||||
}
|
||||
|
||||
open func show(completion: CompletionAction? = nil) {
|
||||
meetWindow.show(completion: completion)
|
||||
}
|
||||
|
||||
open func hide(completion: CompletionAction? = nil) {
|
||||
meetWindow.hide(completion: completion)
|
||||
}
|
||||
|
||||
open func cleanUp() {
|
||||
// TODO: more clean up work on this
|
||||
|
||||
meetWindow.isHidden = true
|
||||
meetWindow.stopDragGesture()
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
// MARK: - helpers
|
||||
|
||||
private func configureMeetViewController() {
|
||||
meetViewController.jitsiMeetView.pictureInPictureEnabled = true
|
||||
meetViewController.delegate = self
|
||||
}
|
||||
|
||||
private func configureMeetWindow() {
|
||||
meetWindow.backgroundColor = .clear
|
||||
meetWindow.windowLevel = UIWindowLevelStatusBar + 100
|
||||
meetWindow.rootViewController = self.meetViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
|
||||
|
||||
open func performPresentationUpdate(to: JitsiMeetPresentationUpdate) {
|
||||
switch to {
|
||||
case .enterPictureInPicture:
|
||||
meetWindow.enterPictureInPicture()
|
||||
case .traitChange:
|
||||
// resize to full screen if rotation happens
|
||||
if meetWindow.isInPiP {
|
||||
meetWindow.exitPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceStarted() {
|
||||
if meetWindow.isHidden {
|
||||
meetWindow.show()
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceEnded(didFail: Bool) {
|
||||
cleanUp()
|
||||
}
|
||||
}
|
||||
107
ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift
Normal file
107
ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public enum JitsiMeetPresentationUpdate {
|
||||
|
||||
/// The conference wants to enter Picture-in-Picture
|
||||
case enterPictureInPicture
|
||||
|
||||
/// A system traitCollectionChange (usually screen rotation)
|
||||
case traitChange
|
||||
}
|
||||
|
||||
public protocol JitsiMeetViewControllerDelegate: class {
|
||||
|
||||
/// Notifies a change of the conference presentation style.
|
||||
///
|
||||
/// - Parameter to: The presentation state that will be changed to
|
||||
func performPresentationUpdate(to: JitsiMeetPresentationUpdate)
|
||||
|
||||
/// The conference started
|
||||
func conferenceStarted()
|
||||
|
||||
/// The conference ended
|
||||
///
|
||||
/// - Parameter didFail: The reason of ending the conference
|
||||
func conferenceEnded(didFail: Bool)
|
||||
}
|
||||
|
||||
/// Wrapper ViewController of a JitsiMeetView
|
||||
///
|
||||
/// Is suggested to override this class and implement some customization
|
||||
/// on how to handle the JitsiMeetView delegate events
|
||||
open class JitsiMeetViewController: UIViewController {
|
||||
|
||||
open weak var delegate: JitsiMeetViewControllerDelegate?
|
||||
|
||||
private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView()
|
||||
|
||||
override open func loadView() {
|
||||
super.loadView()
|
||||
self.view = jitsiMeetView
|
||||
}
|
||||
|
||||
open override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
jitsiMeetView.delegate = self
|
||||
}
|
||||
|
||||
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
delegate?.performPresentationUpdate(to: .traitChange)
|
||||
}
|
||||
}
|
||||
|
||||
extension JitsiMeetViewController: JitsiMeetViewDelegate {
|
||||
|
||||
open func conferenceWillJoin(_ data: [AnyHashable : Any]!) {
|
||||
// do something
|
||||
}
|
||||
|
||||
open func conferenceJoined(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceStarted()
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceWillLeave(_ data: [AnyHashable : Any]!) {
|
||||
// do something
|
||||
}
|
||||
|
||||
open func conferenceLeft(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceEnded(didFail: false)
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceFailed(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceEnded(didFail: true)
|
||||
}
|
||||
}
|
||||
|
||||
open func loadConfigError(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceEnded(didFail: true)
|
||||
}
|
||||
}
|
||||
|
||||
open func enterPicture(inPicture data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.performPresentationUpdate(to: .enterPictureInPicture)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public typealias AnimationCompletion = (Bool) -> Void
|
||||
/// Alias defining a completion closure that returns a Bool
|
||||
public typealias CompletionAction = (Bool) -> Void
|
||||
|
||||
/// Coordinates the view state of a specified view to allow
|
||||
/// to be presented in full screen or in a custom Picture in Picture mode.
|
||||
/// This object will also provide the drag and tap interactions of the view
|
||||
/// when is presented in Picure in Picture mode.
|
||||
public class PiPViewCoordinator {
|
||||
/// A window that allows its root view controller to be presented
|
||||
/// in full screen or in a custom Picture in Picture mode
|
||||
open class PiPWindow: UIWindow {
|
||||
|
||||
/// Limits the boundries of view position on screen when minimized
|
||||
/// Limits the boundries of root view position on screen when minimized
|
||||
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
|
||||
left: 5,
|
||||
bottom: 5,
|
||||
@@ -32,75 +31,53 @@ public class PiPViewCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// The size ratio of the view when in PiP mode
|
||||
public var pipSizeRatio: CGFloat = {
|
||||
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
|
||||
switch deviceIdiom {
|
||||
case .pad:
|
||||
return 0.25
|
||||
case .phone:
|
||||
return 0.33
|
||||
default:
|
||||
return 0.25
|
||||
}
|
||||
}()
|
||||
/// The size ratio for root view controller view when in PiP mode
|
||||
public var pipSizeRatio: CGFloat = 0.333
|
||||
|
||||
private(set) var isInPiP: Bool = false // true if view is in PiP mode
|
||||
|
||||
private(set) var view: UIView
|
||||
private var currentBounds: CGRect = CGRect.zero
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
private var exitPiPButton: UIButton?
|
||||
/// The PiP state of this contents of the window
|
||||
private(set) var isInPiP: Bool = false
|
||||
|
||||
private let dragController: DragGestureController = DragGestureController()
|
||||
|
||||
public init(withView view: UIView) {
|
||||
self.view = view
|
||||
/// Used when in PiP mode to enable/disable exit PiP UI
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
private var exitPiPButton: UIButton?
|
||||
|
||||
/// Help out to bubble up the gesture detection outside of the rootVC frame
|
||||
open override func point(inside point: CGPoint,
|
||||
with event: UIEvent?) -> Bool {
|
||||
guard let vc = rootViewController else {
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
return vc.view.frame.contains(point)
|
||||
}
|
||||
|
||||
/// Configure the view to be always on top of all the contents
|
||||
/// of the provided parent view.
|
||||
/// If a parentView is not provided it will try to use the main window
|
||||
public func configureAsStickyView(withParentView parentView: UIView? = nil) {
|
||||
guard
|
||||
let parentView = parentView ?? UIApplication.shared.keyWindow
|
||||
else { return }
|
||||
|
||||
parentView.addSubview(view)
|
||||
currentBounds = parentView.bounds
|
||||
view.frame = currentBounds
|
||||
view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
/// Show view with fade in animation
|
||||
public func show(completion: AnimationCompletion? = nil) {
|
||||
if view.isHidden || view.alpha < 1 {
|
||||
view.isHidden = false
|
||||
view.alpha = 0
|
||||
/// animate in the window
|
||||
open func show(completion: CompletionAction? = nil) {
|
||||
if self.isHidden || self.alpha < 1 {
|
||||
self.isHidden = false
|
||||
self.alpha = 0
|
||||
|
||||
animateTransition(animations: { [weak self] in
|
||||
self?.view.alpha = 1
|
||||
animateTransition(animations: {
|
||||
self.alpha = 1
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide view with fade out animation
|
||||
public func hide(completion: AnimationCompletion? = nil) {
|
||||
if view.isHidden || view.alpha > 0 {
|
||||
animateTransition(animations: { [weak self] in
|
||||
self?.view.alpha = 0
|
||||
self?.view.isHidden = true
|
||||
/// animate out the window
|
||||
open func hide(completion: CompletionAction? = nil) {
|
||||
if !self.isHidden || self.alpha > 0 {
|
||||
animateTransition(animations: {
|
||||
self.alpha = 1
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize view to and change state to custom PictureInPicture mode
|
||||
/// This will resize view, add a gesture to enable user to "drag" view
|
||||
/// around screen, and add a button of top of the view to be able to exit mode
|
||||
public func enterPictureInPicture() {
|
||||
/// Resize the root view to PiP mode
|
||||
open func enterPictureInPicture() {
|
||||
guard let view = rootViewController?.view else { return }
|
||||
isInPiP = true
|
||||
animateViewChange()
|
||||
animateRootViewChange()
|
||||
dragController.startDragListener(inView: view)
|
||||
dragController.insets = dragBoundInsets
|
||||
|
||||
@@ -112,11 +89,10 @@ public class PiPViewCoordinator {
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
/// Exit Picture in picture mode, this will resize view, remove
|
||||
/// exit pip button, and disable the drag gesture
|
||||
@objc public func exitPictureInPicture() {
|
||||
/// Resize the root view to full screen
|
||||
open func exitPictureInPicture() {
|
||||
isInPiP = false
|
||||
animateViewChange()
|
||||
animateRootViewChange()
|
||||
dragController.stopDragListener()
|
||||
|
||||
// hide PiP UI
|
||||
@@ -129,13 +105,6 @@ public class PiPViewCoordinator {
|
||||
tapGestureRecognizer = nil
|
||||
}
|
||||
|
||||
/// Reset view to provide bounds, use this method on rotation or
|
||||
/// screen size changes
|
||||
public func resetBounds(bounds: CGRect) {
|
||||
currentBounds = bounds
|
||||
exitPictureInPicture()
|
||||
}
|
||||
|
||||
/// Stop the dragging gesture of the root view
|
||||
public func stopDragGesture() {
|
||||
dragController.stopDragListener()
|
||||
@@ -153,14 +122,41 @@ public class PiPViewCoordinator {
|
||||
button.backgroundColor = .gray
|
||||
button.layer.cornerRadius = size.width / 2
|
||||
button.frame = CGRect(origin: CGPoint.zero, size: size)
|
||||
button.center = view.convert(view.center, from: view.superview)
|
||||
if let view = rootViewController?.view {
|
||||
button.center = view.convert(view.center, from:view.superview)
|
||||
}
|
||||
button.addTarget(target, action: action, for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
// MARK: - Interactions
|
||||
// MARK: - Manage presentation switching
|
||||
|
||||
private func animateRootViewChange() {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.rootViewController?.view.frame = self.changeRootViewRect()
|
||||
self.rootViewController?.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private func changeRootViewRect() -> CGRect {
|
||||
guard isInPiP else {
|
||||
return self.bounds
|
||||
}
|
||||
|
||||
// resize to suggested ratio and position to the bottom right
|
||||
let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
|
||||
let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
||||
height: bounds.size.height * pipSizeRatio)
|
||||
let x: CGFloat = adjustedBounds.maxX - size.width
|
||||
let y: CGFloat = adjustedBounds.maxY - size.height
|
||||
return CGRect(x: x, y: y, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Exit PiP
|
||||
|
||||
@objc private func toggleExitPiP() {
|
||||
guard let view = rootViewController?.view else { return }
|
||||
|
||||
if exitPiPButton == nil {
|
||||
// show button
|
||||
let exitSelector = #selector(exitPictureInPicture)
|
||||
@@ -176,40 +172,18 @@ public class PiPViewCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Size calculation
|
||||
|
||||
private func animateViewChange() {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.frame = self.changeViewRect()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
@objc private func exitPiP() {
|
||||
exitPictureInPicture()
|
||||
}
|
||||
|
||||
private func changeViewRect() -> CGRect {
|
||||
let bounds = currentBounds
|
||||
|
||||
guard isInPiP else {
|
||||
return bounds
|
||||
}
|
||||
|
||||
// resize to suggested ratio and position to the bottom right
|
||||
let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets)
|
||||
let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
||||
height: bounds.size.height * pipSizeRatio)
|
||||
let x: CGFloat = adjustedBounds.maxX - size.width
|
||||
let y: CGFloat = adjustedBounds.maxY - size.height
|
||||
return CGRect(x: x, y: y, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Animation helpers
|
||||
// MARK: - Animation transition
|
||||
|
||||
private func animateTransition(animations: @escaping () -> Void,
|
||||
completion: AnimationCompletion?) {
|
||||
completion: CompletionAction?) {
|
||||
UIView.animate(withDuration: 0.1,
|
||||
delay: 0,
|
||||
options: .beginFromCurrentState,
|
||||
animations: animations,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": "",
|
||||
"zhCN": "",
|
||||
"nb": "",
|
||||
"eo": ""
|
||||
}
|
||||
@@ -1,512 +0,0 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"contactlist_plural_undefined": "",
|
||||
"passwordSetRemotely": "",
|
||||
"poweredby": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"audioDevices": {
|
||||
"bluetooth": "",
|
||||
"headphones": "",
|
||||
"phone": "",
|
||||
"speaker": ""
|
||||
},
|
||||
"audioOnly": {
|
||||
"audioOnly": "",
|
||||
"featureToggleDisabled": ""
|
||||
},
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": "",
|
||||
"edgeGrantPermissions": ""
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": "",
|
||||
"showSpeakerStats": ""
|
||||
},
|
||||
"welcomepage": {
|
||||
"appDescription": "",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "",
|
||||
"video": ""
|
||||
},
|
||||
"calendar": "",
|
||||
"go": "",
|
||||
"join": "",
|
||||
"privacy": "",
|
||||
"roomname": "",
|
||||
"roomnameHint": "",
|
||||
"sendFeedback": "",
|
||||
"terms": "",
|
||||
"title": ""
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"text": "",
|
||||
"rejoinKeyTitle": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"addPeople": "",
|
||||
"audioonly": "",
|
||||
"mute": "",
|
||||
"videomute": "",
|
||||
"authenticate": "",
|
||||
"lock": "",
|
||||
"chat": "",
|
||||
"etherpad": "",
|
||||
"sharedvideo": "",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sip": "",
|
||||
"Settings": "",
|
||||
"hangup": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"dialpad": "",
|
||||
"sharedVideoMutedPopup": "",
|
||||
"micMutedPopup": "",
|
||||
"talkWhileMutedPopup": "",
|
||||
"unableToUnmutePopup": "",
|
||||
"cameraDisabled": "",
|
||||
"micDisabled": "",
|
||||
"filmstrip": "",
|
||||
"profile": "",
|
||||
"raiseHand": ""
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appNotInstalled": "",
|
||||
"downloadApp": "",
|
||||
"openApp": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
},
|
||||
"messagebox": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": "",
|
||||
"update": "",
|
||||
"name": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"noDevice": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": ""
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": "",
|
||||
"remoteControl": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"framerate": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural_undefined": "",
|
||||
"localport": "",
|
||||
"localport_plural_undefined": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural_undefined": "",
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural_undefined": "",
|
||||
"transport": "",
|
||||
"transport_plural_undefined": "",
|
||||
"bandwidth": "",
|
||||
"na": "",
|
||||
"turn": "",
|
||||
"quality": {
|
||||
"good": "",
|
||||
"inactive": "",
|
||||
"lost": "",
|
||||
"nonoptimal": "",
|
||||
"poor": ""
|
||||
},
|
||||
"status": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connectedOneMember": "",
|
||||
"connectedTwoMembers": "",
|
||||
"connectedThreePlusMembers": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": "",
|
||||
"suboptimalExperienceTitle": "",
|
||||
"suboptimalExperienceDescription": ""
|
||||
},
|
||||
"dialog": {
|
||||
"allow": "",
|
||||
"kickMessage": "",
|
||||
"popupErrorTitle": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"contactSupport": "",
|
||||
"error": "",
|
||||
"detectext": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"dismiss": "",
|
||||
"rejoinNow": "",
|
||||
"maxUsersLimitReachedTitle": "",
|
||||
"maxUsersLimitReached": "",
|
||||
"lockTitle": "",
|
||||
"lockMessage": "",
|
||||
"warning": "",
|
||||
"passwordNotSupportedTitle": "",
|
||||
"passwordNotSupported": "",
|
||||
"internalErrorTitle": "",
|
||||
"internalError": "",
|
||||
"unableToSwitch": "",
|
||||
"SLDFailure": "",
|
||||
"SRDFailure": "",
|
||||
"oops": "",
|
||||
"currentPassword": "",
|
||||
"passwordLabel": "",
|
||||
"defaultError": "",
|
||||
"passwordRequired": "",
|
||||
"Ok": "",
|
||||
"done": "",
|
||||
"Remove": "",
|
||||
"removePassword": "",
|
||||
"shareVideoTitle": "",
|
||||
"shareVideoLinkError": "",
|
||||
"removeSharedVideoTitle": "",
|
||||
"removeSharedVideoMsg": "",
|
||||
"alreadySharedVideoMsg": "",
|
||||
"alreadySharedVideoTitle": "",
|
||||
"WaitingForHost": "",
|
||||
"WaitForHostMsg": "",
|
||||
"IamHost": "",
|
||||
"Cancel": "",
|
||||
"Submit": "",
|
||||
"retry": "",
|
||||
"logoutTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"sessTerminated": "",
|
||||
"hungUp": "",
|
||||
"joinAgain": "",
|
||||
"Share": "",
|
||||
"Save": "",
|
||||
"recording": "",
|
||||
"recordingToken": "",
|
||||
"Back": "",
|
||||
"serviceUnavailable": "",
|
||||
"gracefulShutdown": "",
|
||||
"Yes": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"password": "",
|
||||
"userPassword": "",
|
||||
"token": "",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"tokenAuthFailed": "",
|
||||
"displayNameRequired": "",
|
||||
"enterDisplayName": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"thankYou": "",
|
||||
"sorryFeedback": "",
|
||||
"liveStreaming": "",
|
||||
"streamKey": "",
|
||||
"startLiveStreaming": "",
|
||||
"stopStreamingWarning": "",
|
||||
"stopRecordingWarning": "",
|
||||
"stopLiveStreaming": "",
|
||||
"stopRecording": "",
|
||||
"doNotShowMessageAgain": "",
|
||||
"permissionDenied": "",
|
||||
"screenSharingFailedToInstall": "",
|
||||
"screenSharingFailedToInstallTitle": "",
|
||||
"screenSharingFirefoxPermissionDeniedError": "",
|
||||
"screenSharingFirefoxPermissionDeniedTitle": "",
|
||||
"screenSharingPermissionDeniedError": "",
|
||||
"cameraUnsupportedResolutionError": "",
|
||||
"cameraUnknownError": "",
|
||||
"cameraPermissionDeniedError": "",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraConstraintFailedError": "",
|
||||
"micUnknownError": "",
|
||||
"micPermissionDeniedError": "",
|
||||
"micNotFoundError": "",
|
||||
"micConstraintFailedError": "",
|
||||
"micNotSendingDataTitle": "",
|
||||
"micNotSendingData": "",
|
||||
"cameraNotSendingDataTitle": "",
|
||||
"cameraNotSendingData": "",
|
||||
"goToStore": "",
|
||||
"externalInstallationTitle": "",
|
||||
"externalInstallationMsg": "",
|
||||
"inlineInstallationMsg": "",
|
||||
"inlineInstallExtension": "",
|
||||
"muteParticipantTitle": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": "",
|
||||
"remoteControlTitle": "",
|
||||
"remoteControlRequestMessage": "",
|
||||
"remoteControlShareScreenWarning": "",
|
||||
"remoteControlDeniedMessage": "",
|
||||
"remoteControlAllowedMessage": "",
|
||||
"remoteControlErrorMessage": "",
|
||||
"startRemoteControlErrorMessage": "",
|
||||
"remoteControlStopMessage": "",
|
||||
"close": "",
|
||||
"shareYourScreen": "",
|
||||
"yourEntireScreen": "",
|
||||
"applicationWindow": ""
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": "",
|
||||
"subject": "",
|
||||
"body": "",
|
||||
"and": ""
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "",
|
||||
"CONNECTING": "",
|
||||
"RECONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ATTACHED": ""
|
||||
},
|
||||
"recording": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"failedToStart": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"unavailable": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"buttonTooltip": "",
|
||||
"changeSignIn": "",
|
||||
"choose": "",
|
||||
"chooseCTA": "",
|
||||
"enterStreamKey": "",
|
||||
"error": "",
|
||||
"errorAPI": "",
|
||||
"failedToStart": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"signIn": "",
|
||||
"signInCTA": "",
|
||||
"start": "",
|
||||
"streamIdHelp": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"videoSIPGW": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"errorInvite": "",
|
||||
"errorInviteTitle": "",
|
||||
"errorAlreadyInvited": "",
|
||||
"errorInviteFailedTitle": "",
|
||||
"errorInviteFailed": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"speakerStats": {
|
||||
"hours": "",
|
||||
"minutes": "",
|
||||
"name": "",
|
||||
"seconds": "",
|
||||
"speakerStats": "",
|
||||
"speakerTime": ""
|
||||
},
|
||||
"deviceSelection": {
|
||||
"deviceSettings": "",
|
||||
"noPermission": "",
|
||||
"previewUnavailable": "",
|
||||
"selectADevice": "",
|
||||
"testAudio": ""
|
||||
},
|
||||
"videoStatus": {
|
||||
"callQuality": "",
|
||||
"hd": "",
|
||||
"highDefinition": "",
|
||||
"labelTooltipVideo": "",
|
||||
"labelTooltipAudioOnly": "",
|
||||
"ld": "",
|
||||
"lowDefinition": "",
|
||||
"onlyAudioAvailable": "",
|
||||
"onlyAudioSupported": "",
|
||||
"p2pEnabled": "",
|
||||
"p2pVideoQualityDescription": "",
|
||||
"recHighDefinitionOnly": "",
|
||||
"sd": "",
|
||||
"standardDefinition": "",
|
||||
"qualityButtonTip": ""
|
||||
},
|
||||
"dialOut": {
|
||||
"statusMessage": ""
|
||||
},
|
||||
"addPeople": {
|
||||
"add": "",
|
||||
"countryNotSupported": "",
|
||||
"countryReminder": "",
|
||||
"disabled": "",
|
||||
"invite": "",
|
||||
"loading": "",
|
||||
"loadingNumber": "",
|
||||
"loadingPeople": "",
|
||||
"noResults": "",
|
||||
"noValidNumbers": "",
|
||||
"searchNumbers": "",
|
||||
"searchPeople": "",
|
||||
"searchPeopleAndNumbers": "",
|
||||
"telephone": "",
|
||||
"title": "",
|
||||
"failedToAdd": ""
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "",
|
||||
"retry": "",
|
||||
"support": "",
|
||||
"supportMsg": ""
|
||||
},
|
||||
"deviceError": {
|
||||
"cameraError": "",
|
||||
"microphoneError": "",
|
||||
"cameraPermission": "",
|
||||
"microphonePermission": ""
|
||||
},
|
||||
"feedback": {
|
||||
"average": "",
|
||||
"bad": "",
|
||||
"good": "",
|
||||
"rateExperience": "",
|
||||
"veryBad": "",
|
||||
"veryGood": ""
|
||||
},
|
||||
"info": {
|
||||
"addPassword": "",
|
||||
"cancelPassword": "",
|
||||
"conferenceURL": "",
|
||||
"country": "",
|
||||
"dialANumber": "",
|
||||
"dialInNumber": "",
|
||||
"dialInConferenceID": "",
|
||||
"dialInNotSupported": "",
|
||||
"genericError": "",
|
||||
"invitePhone": "",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURL": "",
|
||||
"moreNumbers": "",
|
||||
"noNumbers": "",
|
||||
"noPassword": "",
|
||||
"noRoom": "",
|
||||
"numbers": "",
|
||||
"password": "",
|
||||
"title": "",
|
||||
"tooltip": ""
|
||||
},
|
||||
"settingsView": {
|
||||
"alertOk": "",
|
||||
"alertTitle": "",
|
||||
"alertURLText": "",
|
||||
"conferenceSection": "",
|
||||
"displayName": "",
|
||||
"email": "",
|
||||
"header": "",
|
||||
"profileSection": "",
|
||||
"serverURL": "",
|
||||
"startWithAudioMuted": "",
|
||||
"startWithVideoMuted": ""
|
||||
},
|
||||
"calendarSync": {
|
||||
"later": "",
|
||||
"next": "",
|
||||
"nextMeeting": "",
|
||||
"now": ""
|
||||
},
|
||||
"recentList": {
|
||||
"today": "",
|
||||
"yesterday": "",
|
||||
"earlier": ""
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"contactlist": "__count__ Member",
|
||||
"contactlist_plural": "__count__ Members",
|
||||
"passwordSetRemotely": "set by another member",
|
||||
"poweredby": "powered by",
|
||||
@@ -22,7 +23,6 @@
|
||||
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"electronGrantPermissions": "Please grant permissions to use your camera and microphone",
|
||||
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
|
||||
"operaGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
|
||||
@@ -73,28 +73,21 @@
|
||||
"toolbar": {
|
||||
"addPeople": "Add people to your call",
|
||||
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
|
||||
"callQuality": "Manage call quality",
|
||||
"enterFullScreen": "View full screen",
|
||||
"exitFullScreen": "Exit full screen",
|
||||
"feedback": "Leave feedback",
|
||||
"moreActions": "More actions",
|
||||
"mute": "Mute / Unmute",
|
||||
"videomute": "Start / Stop camera",
|
||||
"authenticate": "Authenticate",
|
||||
"lock": "Lock / Unlock room",
|
||||
"chat": "Open / Close chat",
|
||||
"etherpad": "Open / Close shared document",
|
||||
"documentOpen": "Open shared document",
|
||||
"documentClose": "Close shared document",
|
||||
"sharedvideo": "Share a YouTube video",
|
||||
"sharescreen": "Screen share",
|
||||
"stopSharedVideo": "Stop YouTube video",
|
||||
"sharescreen": "Start / Stop screen sharing",
|
||||
"fullscreen": "View / Exit full screen",
|
||||
"sip": "Call SIP number",
|
||||
"Settings": "Settings",
|
||||
"hangup": "Leave",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"dialpad": "Open / Close dialpad",
|
||||
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
|
||||
"micMutedPopup": "Your microphone has been muted so that you would fully enjoy your shared video.",
|
||||
"talkWhileMutedPopup": "Trying to speak? You are muted.",
|
||||
@@ -103,9 +96,17 @@
|
||||
"micDisabled": "Microphone is not available",
|
||||
"filmstrip": "Show / Hide videos",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"shortcuts": "View shortcuts",
|
||||
"speakerStats": "Speaker stats"
|
||||
"raiseHand": "Raise / Lower your hand"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appNotInstalled": "Join this meeting with __app__ on your phone.",
|
||||
"downloadApp": "Download the app",
|
||||
"openApp": "Continue to __app__"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Open / close chat",
|
||||
"filmstrip": "Show / hide videos",
|
||||
"contactlist": "View and invite members"
|
||||
},
|
||||
"chat":{
|
||||
"nickname": {
|
||||
@@ -284,7 +285,6 @@
|
||||
"liveStreaming": "Live Streaming",
|
||||
"streamKey": "Live stream key",
|
||||
"startLiveStreaming": "Go live now",
|
||||
"startRecording": "Start recording",
|
||||
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
|
||||
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
|
||||
"stopLiveStreaming": "Stop live streaming",
|
||||
@@ -445,13 +445,10 @@
|
||||
"videoStatus": {
|
||||
"callQuality": "Call Quality",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Viewing high definition video",
|
||||
"highDefinition": "High definition",
|
||||
"labelTooltipAudioOnly": "Audio-only mode enabled",
|
||||
"labelTooiltipNoVideo": "No video",
|
||||
"labelTooltipVideo": "Current video quality",
|
||||
"labelTooltipAudioOnly": "Audio-only mode enabled",
|
||||
"ld": "LD",
|
||||
"ldTooltip": "Viewing low definition video",
|
||||
"lowDefinition": "Low definition",
|
||||
"onlyAudioAvailable": "Only audio is available",
|
||||
"onlyAudioSupported": "We only support audio in this browser.",
|
||||
@@ -459,7 +456,6 @@
|
||||
"p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
|
||||
"recHighDefinitionOnly": "Will prefer high definition.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Viewing standard definition video",
|
||||
"standardDefinition": "Standard definition",
|
||||
"qualityButtonTip": "Change received video quality"
|
||||
},
|
||||
@@ -467,7 +463,7 @@
|
||||
"statusMessage": "is now __status__"
|
||||
},
|
||||
"addPeople": {
|
||||
"add": "Invite",
|
||||
"add": "Add",
|
||||
"countryNotSupported": "We do not support this destination yet.",
|
||||
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
|
||||
"disabled": "You can't invite people.",
|
||||
@@ -477,12 +473,11 @@
|
||||
"loadingPeople": "Searching for people to invite",
|
||||
"noResults": "No matching search results",
|
||||
"noValidNumbers": "Please enter a phone number",
|
||||
"notAvailable": "You can't invite people.",
|
||||
"searchNumbers": "Add phone numbers",
|
||||
"searchPeople": "Search for people",
|
||||
"searchPeopleAndNumbers": "Search for people or add their phone numbers",
|
||||
"searchNumbers": "Enter a phone number to invite",
|
||||
"searchPeople": "Enter a name to invite",
|
||||
"searchPeopleAndNumbers": "Enter a name or phone number to invite",
|
||||
"telephone": "Telephone: __number__",
|
||||
"title": "Invite people to this meeting",
|
||||
"title": "Invite people to your meeting",
|
||||
"failedToAdd": "Failed to add members"
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
@@ -501,19 +496,18 @@
|
||||
"average": "Average",
|
||||
"bad": "Bad",
|
||||
"good": "Good",
|
||||
"detailsLabel": "Tell us more about it.",
|
||||
"rateExperience": "Rate your meeting experience",
|
||||
"rateExperience": "Please rate your meeting experience.",
|
||||
"veryBad": "Very Bad",
|
||||
"veryGood": "Very Good"
|
||||
},
|
||||
"info": {
|
||||
"addPassword": "Add password",
|
||||
"cancelPassword": "Cancel password",
|
||||
"conferenceURL": "Link:",
|
||||
"conferenceURL": "Link: __url__",
|
||||
"country": "Country",
|
||||
"dialANumber": "To join your meeting, dial one of these numbers and then enter this PIN: __conferenceID__#",
|
||||
"dialInNumber": "Dial-in:",
|
||||
"dialInConferenceID": "PIN:",
|
||||
"dialInNumber": "Dial-in: __phoneNumber__",
|
||||
"dialInConferenceID": "PIN: __conferenceID__#",
|
||||
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
|
||||
"genericError": "Whoops, something went wrong.",
|
||||
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
|
||||
@@ -525,7 +519,7 @@
|
||||
"noRoom": "No room was specified to dial-in into.",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "Password:",
|
||||
"title": "Share",
|
||||
"title": "Call info",
|
||||
"tooltip": "Get access info about the meeting"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -545,25 +539,11 @@
|
||||
"later": "Later",
|
||||
"next": "Upcoming",
|
||||
"nextMeeting": "next meeting",
|
||||
"now": "Now",
|
||||
"permissionButton": "Open settings",
|
||||
"permissionMessage": "Calendar permission is required to list your meetings in the app."
|
||||
"now": "Now"
|
||||
},
|
||||
"recentList": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"earlier": "Earlier"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Pull to refresh"
|
||||
},
|
||||
"deepLinking": {
|
||||
"title": "Launching your meeting in __app__...",
|
||||
"description": "Nothing happened? We tried launching your meeting in the __app__ desktop app. Try again or launch it in the __app__ web app.",
|
||||
"tryAgainButton": "Try again in desktop",
|
||||
"launchWebButton": "Launch in web",
|
||||
"appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
|
||||
"downloadApp": "Download the app",
|
||||
"openApp": "Continue to the app"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,10 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('chat.toggled'));
|
||||
APP.UI.toggleChat();
|
||||
},
|
||||
'toggle-contact-list': () => {
|
||||
sendAnalytics(createApiEvent('contact.list.toggled'));
|
||||
APP.UI.toggleContactList();
|
||||
},
|
||||
'toggle-share-screen': () => {
|
||||
sendAnalytics(createApiEvent('screen.sharing.toggled'));
|
||||
toggleScreenSharing();
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -24,6 +24,7 @@ const commands = {
|
||||
submitFeedback: 'submit-feedback',
|
||||
toggleAudio: 'toggle-audio',
|
||||
toggleChat: 'toggle-chat',
|
||||
toggleContactList: 'toggle-contact-list',
|
||||
toggleFilmStrip: 'toggle-film-strip',
|
||||
toggleShareScreen: 'toggle-share-screen',
|
||||
toggleVideo: 'toggle-video'
|
||||
@@ -550,6 +551,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* toggleVideo - mutes / unmutes video. no arguments
|
||||
* toggleFilmStrip - hides / shows the filmstrip. no arguments
|
||||
* toggleChat - hides / shows chat. no arguments.
|
||||
* toggleContactList - hides / shows contact list. no arguments.
|
||||
* toggleShareScreen - starts / stops screen sharing. no arguments.
|
||||
*
|
||||
* @param {Object} commandList - The object with commands to be executed.
|
||||
|
||||
100
modules/UI/UI.js
100
modules/UI/UI.js
@@ -30,13 +30,19 @@ import {
|
||||
} from '../../react/features/base/participants';
|
||||
import { destroyLocalTracks } from '../../react/features/base/tracks';
|
||||
import { openDisplayNamePrompt } from '../../react/features/display-name';
|
||||
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
|
||||
import {
|
||||
setNotificationsEnabled,
|
||||
showWarningNotification
|
||||
} from '../../react/features/notifications';
|
||||
import {
|
||||
checkAutoEnableDesktopSharing,
|
||||
clearButtonPopup,
|
||||
dockToolbox,
|
||||
setButtonPopupTimeout,
|
||||
setToolbarButton,
|
||||
showDialPadButton,
|
||||
showEtherpadButton,
|
||||
showSharedVideoButton,
|
||||
showToolbox
|
||||
} from '../../react/features/toolbox';
|
||||
|
||||
@@ -94,6 +100,9 @@ const UIListeners = new Map([
|
||||
], [
|
||||
UIEvents.SHARED_VIDEO_CLICKED,
|
||||
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
|
||||
], [
|
||||
UIEvents.TOGGLE_FULLSCREEN,
|
||||
() => UI.toggleFullScreen()
|
||||
], [
|
||||
UIEvents.TOGGLE_CHAT,
|
||||
() => UI.toggleChat()
|
||||
@@ -111,6 +120,9 @@ const UIListeners = new Map([
|
||||
UI.toggleSidePanel('settings_container');
|
||||
}
|
||||
}
|
||||
], [
|
||||
UIEvents.TOGGLE_CONTACT_LIST,
|
||||
() => UI.toggleContactList()
|
||||
], [
|
||||
UIEvents.TOGGLE_PROFILE,
|
||||
() => UI.toggleSidePanel('profile_container')
|
||||
@@ -123,6 +135,14 @@ const UIListeners = new Map([
|
||||
]
|
||||
]);
|
||||
|
||||
/**
|
||||
* Toggles the application in and out of full screen mode
|
||||
* (a.k.a. presentation mode in Chrome).
|
||||
*/
|
||||
UI.toggleFullScreen = function() {
|
||||
UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates if we're currently in full screen mode.
|
||||
*
|
||||
@@ -235,20 +255,12 @@ UI.showLocalConnectionInterrupted = function(isInterrupted) {
|
||||
|
||||
/**
|
||||
* Sets the "raised hand" status for a participant.
|
||||
*
|
||||
* @param {string} id - The id of the participant whose raised hand UI should
|
||||
* be updated.
|
||||
* @param {string} name - The name of the participant with the raised hand
|
||||
* update.
|
||||
* @param {boolean} raisedHandStatus - Whether the participant's hand is raised
|
||||
* or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
|
||||
VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
|
||||
UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
|
||||
VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus);
|
||||
if (raisedHandStatus) {
|
||||
messageHandler.participantNotification(
|
||||
name,
|
||||
participant.getDisplayName(),
|
||||
'notify.somebody',
|
||||
'connected',
|
||||
'notify.raisedHand');
|
||||
@@ -268,7 +280,7 @@ UI.setLocalRaisedHandStatus
|
||||
* Initialize conference UI.
|
||||
*/
|
||||
UI.initConference = function() {
|
||||
const { getState } = APP.store;
|
||||
const { dispatch, getState } = APP.store;
|
||||
const { email, id, name } = getLocalParticipant(getState);
|
||||
|
||||
// Update default button states before showing the toolbar
|
||||
@@ -288,6 +300,8 @@ UI.initConference = function() {
|
||||
UI.setUserEmail(id, email);
|
||||
}
|
||||
|
||||
dispatch(checkAutoEnableDesktopSharing());
|
||||
|
||||
// FollowMe attempts to copy certain aspects of the moderator's UI into the
|
||||
// other participants' UI. Consequently, it needs (1) read and write access
|
||||
// to the UI (depending on the moderator role of the local participant) and
|
||||
@@ -360,13 +374,6 @@ UI.start = function() {
|
||||
$('body').addClass('vertical-filmstrip');
|
||||
}
|
||||
|
||||
// TODO: remove this class once the old toolbar has been removed. This class
|
||||
// is set so that any CSS changes needed to adjust elements outside of the
|
||||
// new toolbar can be scoped to just the app with the new toolbar enabled.
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
$('body').addClass('use-new-toolbox');
|
||||
}
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
};
|
||||
|
||||
@@ -397,7 +404,12 @@ UI.bindEvents = () => {
|
||||
// Resize and reposition videos in full screen mode.
|
||||
$(document).on(
|
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
|
||||
onResize);
|
||||
() => {
|
||||
eventEmitter.emit(
|
||||
UIEvents.FULLSCREEN_TOGGLED,
|
||||
UIUtil.isFullScreen());
|
||||
onResize();
|
||||
});
|
||||
|
||||
$(window).resize(onResize);
|
||||
};
|
||||
@@ -462,7 +474,7 @@ UI.initEtherpad = name => {
|
||||
etherpadManager
|
||||
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
|
||||
|
||||
APP.store.dispatch(setEtherpadHasInitialzied());
|
||||
APP.store.dispatch(showEtherpadButton());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -530,6 +542,10 @@ UI.onPeerVideoTypeChanged
|
||||
UI.updateLocalRole = isModerator => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
APP.store.dispatch(showSharedVideoButton());
|
||||
|
||||
Recording.showRecordingButton(isModerator);
|
||||
|
||||
if (isModerator) {
|
||||
if (!interfaceConfig.DISABLE_FOCUS_INDICATOR) {
|
||||
messageHandler.participantNotification(
|
||||
@@ -623,6 +639,11 @@ UI.isChatVisible = () => Chat.isVisible();
|
||||
*/
|
||||
UI.toggleChat = () => UI.toggleSidePanel('chat_container');
|
||||
|
||||
/**
|
||||
* Toggles contact list panel.
|
||||
*/
|
||||
UI.toggleContactList = () => UI.toggleSidePanel('contacts_container');
|
||||
|
||||
/**
|
||||
* Toggles the given side panel.
|
||||
*
|
||||
@@ -638,6 +659,27 @@ UI.inputDisplayNameHandler = function(newDisplayName) {
|
||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show custom popup/tooltip for a specified button.
|
||||
*
|
||||
* @param {string} buttonName - The name of the button as specified in the
|
||||
* button configurations for the toolbar.
|
||||
* @param {string} popupSelectorID - The id of the popup to show as specified in
|
||||
* the button configurations for the toolbar.
|
||||
* @param {boolean} show - True or false/show or hide the popup
|
||||
* @param {number} timeout - The time to show the popup
|
||||
* @returns {void}
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
UI.showCustomToolbarPopup = function(buttonName, popupID, show, timeout) {
|
||||
const action
|
||||
= show
|
||||
? setButtonPopupTimeout(buttonName, popupID, timeout)
|
||||
: clearButtonPopup(buttonName);
|
||||
|
||||
APP.store.dispatch(action);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the type of the remote video.
|
||||
* @param jid the jid for the remote video
|
||||
@@ -860,6 +902,17 @@ UI.promptDisplayName = () => {
|
||||
*/
|
||||
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
|
||||
|
||||
/**
|
||||
* Update state of desktop sharing buttons.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.updateDesktopSharingButtons
|
||||
= () =>
|
||||
APP.store.dispatch(setToolbarButton('desktop', {
|
||||
toggled: APP.conference.isSharingScreen
|
||||
}));
|
||||
|
||||
/**
|
||||
* Hide connection quality statistics from UI.
|
||||
*/
|
||||
@@ -891,6 +944,9 @@ UI.addMessage = function(from, displayName, message, stamp) {
|
||||
Chat.updateChatConversation(from, displayName, message, stamp);
|
||||
};
|
||||
|
||||
UI.updateDTMFSupport
|
||||
= isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
|
||||
|
||||
UI.updateRecordingState = function(state) {
|
||||
Recording.updateRecordingState(state);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
import { setDocumentEditingState } from '../../../react/features/etherpad';
|
||||
import { getToolboxHeight } from '../../../react/features/toolbox';
|
||||
/* global $, interfaceConfig */
|
||||
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
@@ -129,7 +126,7 @@ class Etherpad extends LargeContainer {
|
||||
let height, width;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
height = containerHeight - getToolboxHeight();
|
||||
height = containerHeight;
|
||||
width = containerWidth - Filmstrip.getFilmstripWidth();
|
||||
} else {
|
||||
height = containerHeight - Filmstrip.getFilmstripHeight();
|
||||
@@ -155,9 +152,6 @@ class Etherpad extends LargeContainer {
|
||||
document.body.style.background = '#eeeeee';
|
||||
$iframe.css({ visibility: 'visible' });
|
||||
$container.css({ zIndex: 2 });
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(true));
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -176,9 +170,6 @@ class Etherpad extends LargeContainer {
|
||||
$iframe.fadeOut(300, () => {
|
||||
$iframe.css({ visibility: 'hidden' });
|
||||
$container.css({ zIndex: 0 });
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(false));
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -251,7 +242,5 @@ export default class EtherpadManager {
|
||||
|
||||
this.eventEmitter
|
||||
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(!isVisible));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global APP, config, interfaceConfig */
|
||||
/* global APP, $, config, interfaceConfig */
|
||||
/*
|
||||
* Copyright @ 2015 Atlassian Pty Ltd
|
||||
*
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
StartLiveStreamDialog,
|
||||
StopLiveStreamDialog,
|
||||
hideRecordingLabel,
|
||||
setRecordingType,
|
||||
updateRecordingState
|
||||
} from '../../../react/features/recording';
|
||||
|
||||
@@ -192,7 +191,8 @@ function isStartingStatus(status) {
|
||||
|
||||
/**
|
||||
* Manages the recording user interface and user experience.
|
||||
* @type {{init, updateRecordingState, updateRecordingUI, checkAutoRecord}}
|
||||
* @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
|
||||
* updateRecordingUI, checkAutoRecord}}
|
||||
*/
|
||||
const Recording = {
|
||||
/**
|
||||
@@ -202,8 +202,6 @@ const Recording = {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.recordingType = recordingType;
|
||||
|
||||
APP.store.dispatch(setRecordingType(recordingType));
|
||||
|
||||
this.updateRecordingState(APP.conference.getRecordingState());
|
||||
|
||||
if (recordingType === 'jibri') {
|
||||
@@ -214,8 +212,12 @@ const Recording = {
|
||||
Object.assign(this, RECORDING_TRANSLATION_KEYS);
|
||||
}
|
||||
|
||||
this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
|
||||
() => this._onToolbarButtonClick());
|
||||
// XXX Due to the React-ification of Toolbox, the HTMLElement with id
|
||||
// toolbar_button_record may not exist yet.
|
||||
$(document).on(
|
||||
'click',
|
||||
'#toolbar_button_record',
|
||||
ev => this._onToolbarButtonClick(ev));
|
||||
|
||||
// If I am a recorder then I publish my recorder custom role to notify
|
||||
// everyone.
|
||||
@@ -234,6 +236,28 @@ const Recording = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialise the recording button.
|
||||
*/
|
||||
initRecordingButton() {
|
||||
const selector = $('#toolbar_button_record');
|
||||
|
||||
selector.addClass(this.baseClass);
|
||||
selector.attr('data-i18n', `[content]${this.recordingButtonTooltip}`);
|
||||
APP.translation.translateElement(selector);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows or hides the 'recording' button.
|
||||
* @param show {true} to show the recording button, {false} to hide it
|
||||
*/
|
||||
showRecordingButton(show) {
|
||||
const shouldShow = show && _isRecordingButtonEnabled();
|
||||
const id = 'toolbar_button_record';
|
||||
|
||||
UIUtil.setVisible(id, shouldShow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the recording state UI.
|
||||
* @param recordingState gives us the current recording state
|
||||
@@ -263,7 +287,6 @@ const Recording = {
|
||||
this.currentState = recordingState;
|
||||
|
||||
let labelDisplayConfiguration;
|
||||
let isRecording = false;
|
||||
|
||||
switch (recordingState) {
|
||||
case JitsiRecordingStatus.ON:
|
||||
@@ -274,7 +297,7 @@ const Recording = {
|
||||
showSpinner: recordingState === JitsiRecordingStatus.RETRYING
|
||||
};
|
||||
|
||||
isRecording = true;
|
||||
this._setToolbarButtonToggled(true);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -299,6 +322,8 @@ const Recording = {
|
||||
: this.recordingOffKey
|
||||
};
|
||||
|
||||
this._setToolbarButtonToggled(false);
|
||||
|
||||
setTimeout(() => {
|
||||
APP.store.dispatch(hideRecordingLabel());
|
||||
}, 5000);
|
||||
@@ -312,6 +337,8 @@ const Recording = {
|
||||
key: this.recordingPendingKey
|
||||
};
|
||||
|
||||
this._setToolbarButtonToggled(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -321,6 +348,8 @@ const Recording = {
|
||||
key: this.recordingErrorKey
|
||||
};
|
||||
|
||||
this._setToolbarButtonToggled(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -333,7 +362,6 @@ const Recording = {
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateRecordingState({
|
||||
isRecording,
|
||||
labelDisplayConfiguration,
|
||||
recordingState
|
||||
}));
|
||||
@@ -451,6 +479,16 @@ const Recording = {
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the toggled state of the recording toolbar button.
|
||||
*
|
||||
* @param {boolean} isToggled indicates if the button should be toggled
|
||||
* or not
|
||||
*/
|
||||
_setToolbarButtonToggled(isToggled) {
|
||||
$('#toolbar_button_record').toggleClass('toggled', isToggled);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ import {
|
||||
participantJoined,
|
||||
participantLeft
|
||||
} from '../../../react/features/base/participants';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight,
|
||||
showToolbox
|
||||
} from '../../../react/features/toolbox';
|
||||
import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
|
||||
|
||||
import SharedVideoThumb from './SharedVideoThumb';
|
||||
|
||||
@@ -286,7 +282,7 @@ export default class SharedVideoManager {
|
||||
|
||||
thumb.setDisplayName('YouTube');
|
||||
VideoLayout.addRemoteVideoContainer(self.url, thumb);
|
||||
VideoLayout.resizeThumbnails(true);
|
||||
VideoLayout.resizeThumbnails(false, true);
|
||||
|
||||
const iframe = player.getIframe();
|
||||
|
||||
@@ -364,6 +360,7 @@ export default class SharedVideoManager {
|
||||
|
||||
player.setVolume(attributes.volume);
|
||||
logger.info(`Player change of volume:${attributes.volume}`);
|
||||
this.showSharedVideoMutedPopup(false);
|
||||
}
|
||||
|
||||
if (isPlayerPaused) {
|
||||
@@ -563,6 +560,8 @@ export default class SharedVideoManager {
|
||||
this.smartAudioMute();
|
||||
}
|
||||
}
|
||||
|
||||
this.showSharedVideoMutedPopup(mute);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -577,6 +576,7 @@ export default class SharedVideoManager {
|
||||
sendAnalytics(createEvent('audio.unmuted'));
|
||||
logger.log('Shared video: audio unmuted');
|
||||
this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
|
||||
this.showMicMutedPopup(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,8 +590,38 @@ export default class SharedVideoManager {
|
||||
sendAnalytics(createEvent('audio.muted'));
|
||||
logger.log('Shared video: audio muted');
|
||||
this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
|
||||
this.showMicMutedPopup(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup under the microphone toolbar icon that notifies the user
|
||||
* of automatic mute after a shared video has started.
|
||||
* @param show boolean, show or hide the notification
|
||||
*/
|
||||
showMicMutedPopup(show) {
|
||||
if (show) {
|
||||
this.showSharedVideoMutedPopup(false);
|
||||
}
|
||||
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'microphone', 'micMutedPopup', show, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup under the shared video toolbar icon that notifies the user
|
||||
* of automatic mute of the shared video after the user has unmuted their
|
||||
* mic.
|
||||
* @param show boolean, show or hide the notification
|
||||
*/
|
||||
showSharedVideoMutedPopup(show) {
|
||||
if (show) {
|
||||
this.showMicMutedPopup(false);
|
||||
}
|
||||
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'sharedvideo', 'sharedVideoMutedPopup', show, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -665,7 +695,7 @@ class SharedVideoContainer extends LargeContainer {
|
||||
let height, width;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
height = containerHeight - getToolboxHeight();
|
||||
height = containerHeight;
|
||||
width = containerWidth - Filmstrip.getFilmstripWidth();
|
||||
} else {
|
||||
height = containerHeight - Filmstrip.getFilmstripHeight();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* global $, APP */
|
||||
/* global $ */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { setVisiblePanel } from '../../../react/features/side-panel';
|
||||
|
||||
/**
|
||||
* Handles open and close of the extended toolbar side panel
|
||||
@@ -58,7 +57,6 @@ const SideContainerToggler = {
|
||||
|
||||
if (isSelectorVisible) {
|
||||
this.hide();
|
||||
APP.store.dispatch(setVisiblePanel(null));
|
||||
} else {
|
||||
if (this.isVisible()) {
|
||||
$('#sideToolbarContainer').children()
|
||||
@@ -76,7 +74,6 @@ const SideContainerToggler = {
|
||||
}
|
||||
|
||||
this.showInnerContainer(elementSelector);
|
||||
APP.store.dispatch(setVisiblePanel(elementId));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Chat from './chat/Chat';
|
||||
import SettingsMenu from './settings/SettingsMenu';
|
||||
import Profile from './profile/Profile';
|
||||
import ContactListView from './contactlist/ContactListView';
|
||||
import { isButtonEnabled } from '../../../react/features/toolbox';
|
||||
|
||||
const SidePanels = {
|
||||
@@ -19,6 +20,11 @@ const SidePanels = {
|
||||
if (isButtonEnabled('profile')) {
|
||||
Profile.init(eventEmitter);
|
||||
}
|
||||
|
||||
// Initialize contact list view
|
||||
if (isButtonEnabled('contacts')) {
|
||||
ContactListView.init();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9,12 +9,7 @@ import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { smileys } from './smileys';
|
||||
|
||||
import { addMessage, markAllRead } from '../../../../react/features/chat';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight,
|
||||
setSubject
|
||||
} from '../../../../react/features/toolbox';
|
||||
import { dockToolbox, setSubject } from '../../../../react/features/toolbox';
|
||||
|
||||
let unreadMessages = 0;
|
||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||
@@ -168,8 +163,6 @@ function addSmileys() {
|
||||
* Resizes the chat conversation.
|
||||
*/
|
||||
function resizeChatConversation() {
|
||||
// FIXME: this function can all be done with CSS. If Chat is ever rewritten,
|
||||
// do not copy over this logic.
|
||||
const msgareaHeight = $('#usermsg').outerHeight();
|
||||
const chatspace = $(`#${CHAT_CONTAINER_ID}`);
|
||||
const width = chatspace.width();
|
||||
@@ -180,12 +173,7 @@ function resizeChatConversation() {
|
||||
$('#smileys').css('bottom', (msgareaHeight - 26) / 2);
|
||||
$('#smileysContainer').css('bottom', msgareaHeight);
|
||||
chat.width(width - 10);
|
||||
|
||||
const maybeAMagicNumberForPaddingAndMargin = 100;
|
||||
const offset = maybeAMagicNumberForPaddingAndMargin
|
||||
+ msgareaHeight + getToolboxHeight();
|
||||
|
||||
chat.height(window.innerHeight - offset);
|
||||
chat.height(window.innerHeight - 15 - msgareaHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,7 +249,6 @@ const Chat = {
|
||||
}
|
||||
|
||||
unreadMessages = 0;
|
||||
APP.store.dispatch(markAllRead());
|
||||
updateVisualNotification();
|
||||
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
@@ -287,10 +274,9 @@ const Chat = {
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
updateChatConversation(id, displayName, message, stamp) {
|
||||
const isFromLocalParticipant = APP.conference.isLocalId(id);
|
||||
let divClassName = '';
|
||||
|
||||
if (isFromLocalParticipant) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
divClassName = 'localuser';
|
||||
} else {
|
||||
divClassName = 'remoteuser';
|
||||
@@ -308,7 +294,6 @@ const Chat = {
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
const escDisplayName = UIUtil.escapeHtml(displayName);
|
||||
const timestamp = getCurrentTime(stamp);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message = processReplacements(escMessage);
|
||||
@@ -317,18 +302,13 @@ const Chat = {
|
||||
= `${'<div class="chatmessage">'
|
||||
+ '<img src="images/chatArrow.svg" class="chatArrow">'
|
||||
+ '<div class="username '}${divClassName}">${escDisplayName
|
||||
}</div><div class="timestamp">${timestamp
|
||||
}</div><div class="timestamp">${getCurrentTime(stamp)
|
||||
}</div><div class="usermessage">${message}</div>`
|
||||
+ '</div>';
|
||||
|
||||
$('#chatconversation').append(messageContainer);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
||||
|
||||
const markAsRead = Chat.isVisible() || isFromLocalParticipant;
|
||||
|
||||
APP.store.dispatch(addMessage(
|
||||
escDisplayName, message, timestamp, markAsRead));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
56
modules/UI/side_pannels/contactlist/ContactListView.js
Normal file
56
modules/UI/side_pannels/contactlist/ContactListView.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/* global $, APP */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../../react/features/base/i18n';
|
||||
import { ContactListPanel } from '../../../../react/features/contact-list';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
|
||||
/**
|
||||
* Contact list.
|
||||
*
|
||||
* FIXME: One day this view should no longer be called "contact list" because
|
||||
* the term "contact" is not used elsewhere. Normally people in the conference
|
||||
* are internally refered to as "participants" or externally as "members".
|
||||
*/
|
||||
const ContactListView = {
|
||||
/**
|
||||
* Creates and appends the contact list to the side panel.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
init() {
|
||||
const contactListPanelContainer = document.createElement('div');
|
||||
|
||||
contactListPanelContainer.id = 'contacts_container';
|
||||
contactListPanelContainer.className = 'sideToolbarContainer__inner';
|
||||
|
||||
$('#sideToolbarContainer').append(contactListPanelContainer);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<ContactListPanel />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
contactListPanelContainer
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the contact list is currently visible.
|
||||
*
|
||||
* @return {boolean) true if the contact list is currently visible.
|
||||
*/
|
||||
isVisible() {
|
||||
return UIUtil.isVisible(document.getElementById('contactlist'));
|
||||
}
|
||||
};
|
||||
|
||||
export default ContactListView;
|
||||
@@ -227,10 +227,43 @@ const UIUtil = {
|
||||
* mode, {false} otherwise
|
||||
*/
|
||||
isFullScreen() {
|
||||
return Boolean(document.fullscreenElement
|
||||
return document.fullscreenElement
|
||||
|| document.mozFullScreenElement
|
||||
|| document.webkitFullscreenElement
|
||||
|| document.msFullscreenElement);
|
||||
|| document.msFullscreenElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* Exits full screen mode.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
*/
|
||||
exitFullScreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enter full screen mode.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
*/
|
||||
enterFullScreen() {
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (document.documentElement.msRequestFullscreen) {
|
||||
document.documentElement.msRequestFullscreen();
|
||||
} else if (document.documentElement.mozRequestFullScreen) {
|
||||
document.documentElement.mozRequestFullScreen();
|
||||
} else if (document.documentElement.webkitRequestFullscreen) {
|
||||
document.documentElement
|
||||
.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -438,56 +438,87 @@ const Filmstrip = {
|
||||
* Resizes thumbnails
|
||||
* @param local
|
||||
* @param remote
|
||||
* @param animate
|
||||
* @param forceUpdate
|
||||
* @returns {Promise}
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
resizeThumbnails(local, remote, forceUpdate = false) {
|
||||
const thumbs = this.getThumbs(!forceUpdate);
|
||||
resizeThumbnails(local, remote, animate = false, forceUpdate = false) {
|
||||
return new Promise(resolve => {
|
||||
const thumbs = this.getThumbs(!forceUpdate);
|
||||
const promises = [];
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
if (thumbs.localThumb) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
promises.push(new Promise(resolve => {
|
||||
thumbs.localThumb.animate({
|
||||
height: local.thumbHeight,
|
||||
'min-height': local.thumbHeight,
|
||||
'min-width': local.thumbWidth,
|
||||
width: local.thumbWidth
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}));
|
||||
}
|
||||
if (thumbs.remoteThumbs) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
promises.push(new Promise(resolve => {
|
||||
thumbs.remoteThumbs.animate({
|
||||
height: remote.thumbHeight,
|
||||
'min-height': remote.thumbHeight,
|
||||
'min-width': remote.thumbWidth,
|
||||
width: remote.thumbWidth
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line no-shadow
|
||||
thumbs.localThumb.css({
|
||||
display: 'inline-block',
|
||||
height: `${local.thumbHeight}px`,
|
||||
'min-height': `${local.thumbHeight}px`,
|
||||
'min-width': `${local.thumbWidth}px`,
|
||||
width: `${local.thumbWidth}px`
|
||||
});
|
||||
}
|
||||
promises.push(new Promise(resolve => {
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$('#filmstripLocalVideo').animate({
|
||||
// adds 4 px because of small video 2px border
|
||||
width: local.thumbWidth + 4
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
} else {
|
||||
this.filmstrip.animate({
|
||||
// adds 4 px because of small video 2px border
|
||||
height: remote.thumbHeight + 4
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}
|
||||
}));
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
thumbs.remoteThumbs.css({
|
||||
display: 'inline-block',
|
||||
height: `${remote.thumbHeight}px`,
|
||||
'min-height': `${remote.thumbHeight}px`,
|
||||
'min-width': `${remote.thumbWidth}px`,
|
||||
width: `${remote.thumbWidth}px`
|
||||
});
|
||||
}
|
||||
promises.push(new Promise(() => {
|
||||
const { localThumb } = this.getThumbs();
|
||||
const height = localThumb ? localThumb.height() : 0;
|
||||
const fontSize = UIUtil.getIndicatorFontSize(height);
|
||||
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$('#filmstripLocalVideo').css({
|
||||
// adds 4 px because of small video 2px border
|
||||
width: `${local.thumbWidth + 4}px`
|
||||
});
|
||||
} else {
|
||||
this.filmstrip.css({
|
||||
// adds 4 px because of small video 2px border
|
||||
height: `${remote.thumbHeight + 4}px`
|
||||
});
|
||||
}
|
||||
this.filmstrip.find('.indicator').animate({
|
||||
fontSize
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}));
|
||||
|
||||
const { localThumb } = this.getThumbs();
|
||||
const height = localThumb ? localThumb.height() : 0;
|
||||
const fontSize = UIUtil.getIndicatorFontSize(height);
|
||||
if (!animate) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
this.filmstrip.find('.indicator').css({
|
||||
'font-size': `${fontSize}px`
|
||||
Promise.all(promises).then(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method. Returns options for jQuery animation
|
||||
* @param animate {Boolean} - animation flag
|
||||
* @param cb {Function} - complete callback
|
||||
* @returns {Object} - animation options object
|
||||
* @private
|
||||
*/
|
||||
_getAnimateOptions(animate, cb = $.noop) {
|
||||
return {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0,
|
||||
complete: cb
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns thumbnails of the filmstrip
|
||||
* @param onlyVisible
|
||||
|
||||
@@ -95,7 +95,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
||||
|
||||
this.updateRemoteVideoMenu();
|
||||
|
||||
this.VideoLayout.resizeThumbnails(true);
|
||||
this.VideoLayout.resizeThumbnails(false, true);
|
||||
|
||||
this.addAudioLevelIndicator();
|
||||
|
||||
|
||||
@@ -66,6 +66,9 @@ const VideoLayout = {
|
||||
init(emitter) {
|
||||
eventEmitter = emitter;
|
||||
|
||||
// Unregister listeners in case of reinitialization
|
||||
this.unregisterListeners();
|
||||
|
||||
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
|
||||
|
||||
// sets default video type of local video
|
||||
@@ -74,7 +77,7 @@ const VideoLayout = {
|
||||
|
||||
// if we do not resize the thumbs here, if there is no video device
|
||||
// the local video thumb maybe one pixel
|
||||
this.resizeThumbnails(true);
|
||||
this.resizeThumbnails(false, true);
|
||||
|
||||
this.handleVideoThumbClicked = this.handleVideoThumbClicked.bind(this);
|
||||
|
||||
@@ -101,6 +104,20 @@ const VideoLayout = {
|
||||
registerListeners() {
|
||||
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
|
||||
onLocalFlipXChanged);
|
||||
eventEmitter.addListener(UIEvents.CONTACT_CLICKED,
|
||||
this.handleVideoThumbClicked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unregistering listeners for UI events in Video layout component.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
unregisterListeners() {
|
||||
if (this._onContactClicked) {
|
||||
eventEmitter.removeListener(UIEvents.CONTACT_CLICKED,
|
||||
this.handleVideoThumbClicked);
|
||||
}
|
||||
},
|
||||
|
||||
initLargeVideo() {
|
||||
@@ -468,7 +485,7 @@ const VideoLayout = {
|
||||
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
|
||||
}
|
||||
|
||||
VideoLayout.resizeThumbnails(true);
|
||||
VideoLayout.resizeThumbnails(false, true);
|
||||
|
||||
// Initialize the view
|
||||
remoteVideo.updateView();
|
||||
@@ -480,7 +497,7 @@ const VideoLayout = {
|
||||
logger.info(`${resourceJid} video is now active`, videoElement);
|
||||
|
||||
VideoLayout.resizeThumbnails(
|
||||
false, () => {
|
||||
false, false, () => {
|
||||
if (videoElement) {
|
||||
$(videoElement).show();
|
||||
}
|
||||
@@ -575,16 +592,19 @@ const VideoLayout = {
|
||||
* Resizes thumbnails.
|
||||
*/
|
||||
resizeThumbnails(
|
||||
animate = false,
|
||||
forceUpdate = false,
|
||||
onComplete = null) {
|
||||
const { localVideo, remoteVideo }
|
||||
= Filmstrip.calculateThumbnailSize();
|
||||
|
||||
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
|
||||
|
||||
if (onComplete && typeof onComplete === 'function') {
|
||||
onComplete();
|
||||
}
|
||||
Filmstrip.resizeThumbnails(localVideo, remoteVideo,
|
||||
animate, forceUpdate)
|
||||
.then(() => {
|
||||
if (onComplete && typeof onComplete === 'function') {
|
||||
onComplete();
|
||||
}
|
||||
});
|
||||
|
||||
return { localVideo,
|
||||
remoteVideo };
|
||||
@@ -876,7 +896,7 @@ const VideoLayout = {
|
||||
}
|
||||
|
||||
// Resize the thumbnails first.
|
||||
this.resizeThumbnails(forceUpdate);
|
||||
this.resizeThumbnails(false, forceUpdate);
|
||||
|
||||
// Resize the video area element.
|
||||
$('#videospace').animate({
|
||||
@@ -1092,7 +1112,7 @@ const VideoLayout = {
|
||||
* Currently used by tests (torture).
|
||||
*/
|
||||
getLargeVideoID() {
|
||||
return largeVideo && largeVideo.id;
|
||||
return largeVideo.id;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,9 +15,9 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Map of shortcuts. When a shortcut is registered it enters the mapping.
|
||||
* @type {Map}
|
||||
* @type {{}}
|
||||
*/
|
||||
const _shortcuts = new Map();
|
||||
const _shortcuts = {};
|
||||
|
||||
/**
|
||||
* Map of registered keyboard keys and translation keys describing the
|
||||
@@ -49,8 +49,8 @@ const KeyboardShortcut = {
|
||||
if (!($(':focus').is('input[type=text]')
|
||||
|| $(':focus').is('input[type=password]')
|
||||
|| $(':focus').is('textarea'))) {
|
||||
if (_shortcuts.has(key)) {
|
||||
_shortcuts.get(key).function(e);
|
||||
if (_shortcuts.hasOwnProperty(key)) {
|
||||
_shortcuts[key].function(e);
|
||||
} else if (!isNaN(num) && num >= 0 && num <= 9) {
|
||||
APP.UI.clickOnVideo(num);
|
||||
}
|
||||
@@ -90,17 +90,6 @@ const KeyboardShortcut = {
|
||||
enabled = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the {@KeyboardShortcutsDialog} dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
openDialog() {
|
||||
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
|
||||
shortcutDescriptions: _shortcutsHelp
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers a new shortcut.
|
||||
*
|
||||
@@ -117,11 +106,11 @@ const KeyboardShortcut = {
|
||||
shortcutAttr,
|
||||
exec,
|
||||
helpDescription) {
|
||||
_shortcuts.set(shortcutChar, {
|
||||
_shortcuts[shortcutChar] = {
|
||||
character: shortcutChar,
|
||||
function: exec,
|
||||
shortcutAttr
|
||||
});
|
||||
shortcutAttr,
|
||||
function: exec
|
||||
};
|
||||
|
||||
if (helpDescription) {
|
||||
this._addShortcutToHelp(shortcutChar, helpDescription);
|
||||
@@ -135,7 +124,7 @@ const KeyboardShortcut = {
|
||||
* no longer be usable
|
||||
*/
|
||||
unregisterShortcut(shortcutChar) {
|
||||
_shortcuts.delete(shortcutChar);
|
||||
_shortcuts.remove(shortcutChar);
|
||||
_shortcutsHelp.delete(shortcutChar);
|
||||
},
|
||||
|
||||
@@ -188,7 +177,9 @@ const KeyboardShortcut = {
|
||||
_initGlobalShortcuts() {
|
||||
this.registerShortcut('?', null, () => {
|
||||
sendAnalytics(createShortcutEvent('help'));
|
||||
this.openDialog();
|
||||
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
|
||||
shortcutDescriptions: _shortcutsHelp
|
||||
}));
|
||||
}, 'keyboardShortcuts.toggleShortcuts');
|
||||
|
||||
// register SPACE shortcut in two steps to insure visibility of help
|
||||
|
||||
@@ -22,6 +22,7 @@ const LEGACY_INCOMING_METHODS = [
|
||||
'email',
|
||||
'toggle-audio',
|
||||
'toggle-chat',
|
||||
'toggle-contact-list',
|
||||
'toggle-film-strip',
|
||||
'toggle-share-screen',
|
||||
'toggle-video',
|
||||
|
||||
478
package-lock.json
generated
478
package-lock.json
generated
@@ -91,7 +91,7 @@
|
||||
"@atlaskit/layer": "2.7.2",
|
||||
"@atlaskit/spinner": "4.0.0",
|
||||
"@atlaskit/theme": "2.4.0",
|
||||
"@atlaskit/tooltip": "6.2.2",
|
||||
"@atlaskit/tooltip": "6.0.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"classnames": "2.2.5",
|
||||
"keycode": "2.1.9",
|
||||
@@ -109,19 +109,6 @@
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
},
|
||||
"@atlaskit/tooltip": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-6.2.2.tgz",
|
||||
"integrity": "sha1-zHE3McH4BNnYAXYRGzk6c3DXUdo=",
|
||||
"requires": {
|
||||
"@atlaskit/layer": "2.7.2",
|
||||
"@atlaskit/theme": "2.4.0",
|
||||
"@atlaskit/util-shared-styles": "2.10.6",
|
||||
"babel-runtime": "6.26.0",
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
},
|
||||
"styled-components": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-1.4.6.tgz",
|
||||
@@ -422,91 +409,49 @@
|
||||
}
|
||||
},
|
||||
"@atlaskit/inline-dialog": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.3.0.tgz",
|
||||
"integrity": "sha512-4bEeC5rZwtb4YO9BxW1UCJYCp/dyCVXqcygRW1BDnYVbveAI8wdym6qEi4BRvIwXCT4qgNhsVsqcxSrn0X6CKQ==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.0.2.tgz",
|
||||
"integrity": "sha1-xwPi9seo0M+nrcPXgJK0bE7ShkQ=",
|
||||
"requires": {
|
||||
"@atlaskit/layer": "2.9.1",
|
||||
"@atlaskit/layer": "2.7.2",
|
||||
"@atlaskit/theme": "2.4.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/layer": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-2.9.1.tgz",
|
||||
"integrity": "sha512-nyIVGeS2OhuGR5gIMTYUfRmCG8z/9KMgUzTpbpsB70sH6+d4KSFhfkz+KhKNIa8gvKI6zBc+3UBYSlUW1t1qmQ==",
|
||||
"requires": {
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@atlaskit/inline-message": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/inline-message/-/inline-message-4.0.0.tgz",
|
||||
"integrity": "sha512-JhZf3Oux7kp+7RMFIGuaVblGiwWPd8EfCQ2kJXSvxYFTRo798gPFBKz4LU/NKQU3Fl2Rht6o5qmvjbdbzoxcZg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/inline-message/-/inline-message-3.0.1.tgz",
|
||||
"integrity": "sha1-VIF4Yg+CHx/nFBI+E4dQh9Upn78=",
|
||||
"requires": {
|
||||
"@atlaskit/button": "7.0.0",
|
||||
"@atlaskit/icon": "11.0.0",
|
||||
"@atlaskit/inline-dialog": "6.0.0",
|
||||
"@atlaskit/theme": "3.0.0",
|
||||
"@atlaskit/button": "3.6.0",
|
||||
"@atlaskit/icon": "7.1.0",
|
||||
"@atlaskit/inline-dialog": "5.0.2",
|
||||
"@atlaskit/theme": "2.4.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/analytics-next": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-2.0.0.tgz",
|
||||
"integrity": "sha512-HHRifSrETd/hR7vj3Vqscz4374G1hhEnSehFkPvP+2BIL64TwsqlDLjwL/y1j+/5tFpSYRjfIAVBM/RcCuuzzg==",
|
||||
"requires": {
|
||||
"prop-types": "15.6.0"
|
||||
}
|
||||
},
|
||||
"@atlaskit/button": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-7.0.0.tgz",
|
||||
"integrity": "sha512-FXozSxBcmigSt3+ff+ca8kfEox+mP6dJsXG4i/fkYfsmZr8WXYnyPWndWvOJYs7jvqqP9FMIGSJJQeOJHI60Ow==",
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-3.6.0.tgz",
|
||||
"integrity": "sha1-tx5uySqBkHWAfqqbItyV59NSgqE=",
|
||||
"requires": {
|
||||
"@atlaskit/analytics-next": "2.0.0",
|
||||
"@atlaskit/theme": "3.0.0",
|
||||
"@atlaskit/util-shared-styles": "2.10.6",
|
||||
"babel-runtime": "6.26.0",
|
||||
"classnames": "2.2.5",
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
},
|
||||
"@atlaskit/icon": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-11.0.0.tgz",
|
||||
"integrity": "sha512-KxGxDwbuWeGZ5ivP1JFlPOhHipQQzgm0lAUGzziaYi6yOaNmfwmNjpj2xBpOhlkkymvhnFkzV5JSwVUH+0rW1w==",
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-7.1.0.tgz",
|
||||
"integrity": "sha1-czQVCEhzUmPeShO8mPTUTU8r6N8=",
|
||||
"requires": {
|
||||
"@atlaskit/theme": "3.0.0",
|
||||
"babel-runtime": "6.26.0",
|
||||
"styled-components": "1.4.6",
|
||||
"uuid": "3.1.0"
|
||||
}
|
||||
},
|
||||
"@atlaskit/inline-dialog": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-6.0.0.tgz",
|
||||
"integrity": "sha512-ITGozbuJg6UFzZHQjrWOsx17jKMFR4TE6YdTYA2UTKmKJalHvH414APuNyZdIz8XCFFnQo8NuGdkAjiBYCKG0w==",
|
||||
"requires": {
|
||||
"@atlaskit/layer": "3.1.0",
|
||||
"@atlaskit/theme": "3.0.0",
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
},
|
||||
"@atlaskit/layer": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-3.1.0.tgz",
|
||||
"integrity": "sha512-QdOiu/c0u+YZKuC5z1hYZj/TW0axO5pL2kWBU2MLyMnThfHA+8UCMvIKtpI5/DoMevR5bEuLTX6ivos15l2ulA==",
|
||||
"requires": {
|
||||
"react-scrolllock": "2.0.2",
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
},
|
||||
"@atlaskit/theme": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.0.0.tgz",
|
||||
"integrity": "sha512-j9n0x+WjpwLelPR6Mf64meuPSWsHBK7hEU6q3dLK5YerqXaocu9Z5Dcnv6E8F+zfzGvPn/9ISgO2L3ts5Gv/3g==",
|
||||
"requires": {
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
@@ -893,50 +838,16 @@
|
||||
}
|
||||
},
|
||||
"@atlaskit/tooltip": {
|
||||
"version": "9.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-9.1.1.tgz",
|
||||
"integrity": "sha512-LGMg+OP9qRIMzr8DJtMUBAkHbCN9Vwvh30QWGu/pXktBaxSM5JbxGnUughnoE1JFYyeycJGmyZ0ibtQDnmiB7w==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-6.0.0.tgz",
|
||||
"integrity": "sha1-rn3xMCmvO1iZqcuKNPPIT8K7mpc=",
|
||||
"requires": {
|
||||
"@atlaskit/layer-manager": "3.0.1",
|
||||
"@atlaskit/theme": "3.0.0",
|
||||
"react-deprecate": "0.1.0",
|
||||
"react-transition-group": "2.3.0",
|
||||
"@atlaskit/layer": "2.7.2",
|
||||
"@atlaskit/theme": "2.4.0",
|
||||
"@atlaskit/util-shared-styles": "2.10.6",
|
||||
"babel-runtime": "6.26.0",
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/layer-manager": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/layer-manager/-/layer-manager-3.0.1.tgz",
|
||||
"integrity": "sha512-UPACU1LI+mseZ+GM5HDAHVAONemCQlVMVDlGAW77jYyG4rrCwQUzjZNS8OULvCfHZyULDrvT/w9FqG6eKPTo+A==",
|
||||
"requires": {
|
||||
"focusin": "2.0.0",
|
||||
"prop-types": "15.6.0",
|
||||
"react-transition-group": "2.3.0",
|
||||
"styled-components": "1.4.6",
|
||||
"tabbable": "1.1.2"
|
||||
}
|
||||
},
|
||||
"@atlaskit/theme": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.0.0.tgz",
|
||||
"integrity": "sha512-j9n0x+WjpwLelPR6Mf64meuPSWsHBK7hEU6q3dLK5YerqXaocu9Z5Dcnv6E8F+zfzGvPn/9ISgO2L3ts5Gv/3g==",
|
||||
"requires": {
|
||||
"prop-types": "15.6.0",
|
||||
"styled-components": "1.4.6"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.0.tgz",
|
||||
"integrity": "sha512-OU3/swEL8y233u5ajTn3FIcQQ/b3XWjLXB6e2LnM1OK5JATtsyfJvPTZ8c/dawHNqjUltcdHRSpgMtPe7v07pw==",
|
||||
"requires": {
|
||||
"chain-function": "1.0.0",
|
||||
"dom-helpers": "3.3.1",
|
||||
"loose-envify": "1.3.1",
|
||||
"prop-types": "15.6.0",
|
||||
"warning": "3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@atlaskit/util-common": {
|
||||
@@ -4784,11 +4695,6 @@
|
||||
"strip-eof": "1.0.0"
|
||||
}
|
||||
},
|
||||
"exenv": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
|
||||
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
|
||||
},
|
||||
"exit": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||
@@ -6330,21 +6236,6 @@
|
||||
"globule": "1.2.0"
|
||||
}
|
||||
},
|
||||
"generate-function": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
|
||||
"integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
|
||||
"dev": true
|
||||
},
|
||||
"generate-object-property": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
|
||||
"integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-property": "1.0.2"
|
||||
}
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
|
||||
@@ -6894,6 +6785,11 @@
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.2.tgz",
|
||||
"integrity": "sha512-pH3vDzpczdsKHdZ9xxR3O46unSjisgVx0IImay7Zz2EdhRVbCkj+nthx9OuuWEhakx9FAO+fNVGrF0rZ2oMOvw=="
|
||||
},
|
||||
"immutable": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
|
||||
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
|
||||
},
|
||||
"import-local": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz",
|
||||
@@ -7170,25 +7066,6 @@
|
||||
"is-extglob": "1.0.0"
|
||||
}
|
||||
},
|
||||
"is-my-ip-valid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
|
||||
"integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-my-json-valid": {
|
||||
"version": "2.17.2",
|
||||
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
|
||||
"integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"generate-function": "2.0.0",
|
||||
"generate-object-property": "1.2.0",
|
||||
"is-my-ip-valid": "1.0.0",
|
||||
"jsonpointer": "4.0.1",
|
||||
"xtend": "4.0.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
|
||||
@@ -7250,12 +7127,6 @@
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
|
||||
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
|
||||
},
|
||||
"is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
|
||||
"dev": true
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
|
||||
@@ -7515,12 +7386,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
|
||||
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
|
||||
},
|
||||
"jsonpointer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
|
||||
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
|
||||
"dev": true
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@@ -7608,7 +7473,7 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#39eea17e7e2f8ff4cc273239a6e73f2a149b96e2",
|
||||
"version": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
|
||||
"requires": {
|
||||
"async": "0.9.0",
|
||||
"current-executing-script": "0.1.3",
|
||||
@@ -7774,12 +7639,6 @@
|
||||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.mergewith": {
|
||||
"version": "4.6.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
|
||||
"integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.pad": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz",
|
||||
@@ -8456,8 +8315,7 @@
|
||||
"nan": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz",
|
||||
"integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==",
|
||||
"optional": true
|
||||
"integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw=="
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
@@ -8630,9 +8488,9 @@
|
||||
}
|
||||
},
|
||||
"node-sass": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.8.3.tgz",
|
||||
"integrity": "sha512-tfFWhUsCk/Y19zarDcPo5xpj+IW3qCfOjVdHtYeG6S1CKbQOh1zqylnQK6cV3z9k80yxAnFX9Y+a9+XysDhhfg==",
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-3.13.1.tgz",
|
||||
"integrity": "sha1-ckD7v/I5YwS0IjUn7TAgWJwAT8I=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"async-foreach": "0.1.3",
|
||||
@@ -8644,45 +8502,15 @@
|
||||
"in-publish": "2.0.0",
|
||||
"lodash.assign": "4.2.0",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"lodash.mergewith": "4.6.1",
|
||||
"meow": "3.7.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"nan": "2.10.0",
|
||||
"nan": "2.9.2",
|
||||
"node-gyp": "3.6.2",
|
||||
"npmlog": "4.1.2",
|
||||
"request": "2.79.0",
|
||||
"sass-graph": "2.2.4",
|
||||
"stdout-stream": "1.4.0",
|
||||
"true-case-path": "1.0.2"
|
||||
"request": "2.83.0",
|
||||
"sass-graph": "2.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"assert-plus": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
|
||||
"integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
|
||||
"dev": true
|
||||
},
|
||||
"aws-sign2": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
|
||||
"integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
|
||||
"dev": true
|
||||
},
|
||||
"boom": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
|
||||
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "2.16.3"
|
||||
}
|
||||
},
|
||||
"caseless": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
|
||||
"integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
|
||||
"dev": true
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
|
||||
@@ -8693,26 +8521,6 @@
|
||||
"which": "1.3.0"
|
||||
}
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
|
||||
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boom": "2.10.1"
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
|
||||
"integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"mime-types": "2.1.18"
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
@@ -8729,47 +8537,6 @@
|
||||
"wide-align": "1.1.2"
|
||||
}
|
||||
},
|
||||
"har-validator": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
|
||||
"integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "1.1.3",
|
||||
"commander": "2.14.1",
|
||||
"is-my-json-valid": "2.17.2",
|
||||
"pinkie-promise": "2.0.1"
|
||||
}
|
||||
},
|
||||
"hawk": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
|
||||
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"boom": "2.10.1",
|
||||
"cryptiles": "2.0.5",
|
||||
"hoek": "2.16.3",
|
||||
"sntp": "1.0.9"
|
||||
}
|
||||
},
|
||||
"hoek": {
|
||||
"version": "2.16.3",
|
||||
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
|
||||
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
|
||||
"dev": true
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
|
||||
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"assert-plus": "0.2.0",
|
||||
"jsprim": "1.4.1",
|
||||
"sshpk": "1.13.1"
|
||||
}
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
@@ -8779,12 +8546,6 @@
|
||||
"number-is-nan": "1.0.1"
|
||||
}
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
|
||||
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
|
||||
"dev": true
|
||||
},
|
||||
"npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
@@ -8797,49 +8558,6 @@
|
||||
"set-blocking": "2.0.0"
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
|
||||
"integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
|
||||
"dev": true
|
||||
},
|
||||
"request": {
|
||||
"version": "2.79.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
|
||||
"integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aws-sign2": "0.6.0",
|
||||
"aws4": "1.6.0",
|
||||
"caseless": "0.11.0",
|
||||
"combined-stream": "1.0.6",
|
||||
"extend": "3.0.1",
|
||||
"forever-agent": "0.6.1",
|
||||
"form-data": "2.1.4",
|
||||
"har-validator": "2.0.6",
|
||||
"hawk": "3.1.3",
|
||||
"http-signature": "1.1.1",
|
||||
"is-typedarray": "1.0.0",
|
||||
"isstream": "0.1.2",
|
||||
"json-stringify-safe": "5.0.1",
|
||||
"mime-types": "2.1.18",
|
||||
"oauth-sign": "0.8.2",
|
||||
"qs": "6.3.2",
|
||||
"stringstream": "0.0.5",
|
||||
"tough-cookie": "2.3.3",
|
||||
"tunnel-agent": "0.4.3",
|
||||
"uuid": "3.1.0"
|
||||
}
|
||||
},
|
||||
"sntp": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
|
||||
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hoek": "2.16.3"
|
||||
}
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
@@ -8850,12 +8568,6 @@
|
||||
"is-fullwidth-code-point": "1.0.0",
|
||||
"strip-ansi": "3.0.1"
|
||||
}
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
|
||||
"integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8932,6 +8644,14 @@
|
||||
"gauge": "1.2.7"
|
||||
}
|
||||
},
|
||||
"nuclear-js": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/nuclear-js/-/nuclear-js-1.4.0.tgz",
|
||||
"integrity": "sha1-bJwAGwZz8K6dj4sYjE2gTtaTp74=",
|
||||
"requires": {
|
||||
"immutable": "3.8.2"
|
||||
}
|
||||
},
|
||||
"num2fraction": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
|
||||
@@ -10290,11 +10010,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz",
|
||||
"integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw="
|
||||
},
|
||||
"react-deprecate": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-deprecate/-/react-deprecate-0.1.0.tgz",
|
||||
"integrity": "sha512-9ooyaovhANHgfuOxXRgrEiEfWjEhvygeSxrRTGxNlXErnXnyHBGjxCxrKYsT/Gsc62lS9rFOBeK0c2wwdyUnvQ=="
|
||||
},
|
||||
"react-devtools-core": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-2.5.2.tgz",
|
||||
@@ -10456,7 +10171,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-fetch-blob": {
|
||||
"version": "github:flatfox-ag/react-native-fetch-blob#01f38a4537baecd3ea0cb93c27e84553f3fc5231",
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/react-native-fetch-blob/-/react-native-fetch-blob-0.10.8.tgz",
|
||||
"integrity": "sha1-T8JWq64MtfEOfEHyjBGz/zMNcqk=",
|
||||
"requires": {
|
||||
"base-64": "0.1.0",
|
||||
"glob": "7.0.6"
|
||||
@@ -10504,9 +10221,9 @@
|
||||
"integrity": "sha1-QeDsKqfdjxLzo+6Dr51jxLZw+KE="
|
||||
},
|
||||
"react-native-sound": {
|
||||
"version": "0.10.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.9.tgz",
|
||||
"integrity": "sha1-awCw9K/QF83gn7udFx3xtdW4Uag="
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.4.tgz",
|
||||
"integrity": "sha512-V9v4CjKgv8ekQRLOJSoKA7pxJ03F4Ih3T/RfMIlMWLktz7v/O4sdJPjRBLOzZRqAnr9FWTLbSk1ZCjioXh3mjQ=="
|
||||
},
|
||||
"react-native-vector-icons": {
|
||||
"version": "4.4.2",
|
||||
@@ -10541,7 +10258,7 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "github:jitsi/react-native-webrtc#84aee6693195c5be6b6f5f794a1e52dc7913fb3b",
|
||||
"version": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
|
||||
"requires": {
|
||||
"base64-js": "1.2.3",
|
||||
"event-target-shim": "1.1.1",
|
||||
@@ -10577,14 +10294,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-scrolllock": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-2.0.2.tgz",
|
||||
"integrity": "sha512-Kz0cTwHnGdTEvvNmKWKhFYrwpap8jGLbdTSjzZE1X37OJW72TVDWsL4yQ9oKS/uAsHUlbrg7AZt8Z1vgcRHmFQ==",
|
||||
"requires": {
|
||||
"exenv": "1.2.2"
|
||||
}
|
||||
},
|
||||
"react-syntax-highlighter": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-3.0.2.tgz",
|
||||
@@ -11758,47 +11467,6 @@
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
|
||||
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
|
||||
},
|
||||
"stdout-stream": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
|
||||
"integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "2.3.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "1.0.2",
|
||||
"inherits": "2.0.3",
|
||||
"isarray": "1.0.0",
|
||||
"process-nextick-args": "2.0.0",
|
||||
"safe-buffer": "5.1.1",
|
||||
"string_decoder": "1.1.1",
|
||||
"util-deprecate": "1.0.2"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "5.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stream-browserify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
|
||||
@@ -12279,30 +11947,6 @@
|
||||
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
|
||||
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
|
||||
},
|
||||
"true-case-path": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz",
|
||||
"integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "6.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"glob": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
|
||||
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inflight": "1.0.6",
|
||||
"inherits": "2.0.3",
|
||||
"minimatch": "3.0.4",
|
||||
"once": "1.4.0",
|
||||
"path-is-absolute": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tsscmp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz",
|
||||
|
||||
17
package.json
17
package.json
@@ -23,8 +23,8 @@
|
||||
"@atlaskit/field-text-area": "1.2.0",
|
||||
"@atlaskit/flag": "6.1.0",
|
||||
"@atlaskit/icon": "10.0.0",
|
||||
"@atlaskit/inline-dialog": "5.3.0",
|
||||
"@atlaskit/inline-message": "4.0.0",
|
||||
"@atlaskit/inline-dialog": "5.0.2",
|
||||
"@atlaskit/inline-message": "3.0.1",
|
||||
"@atlaskit/layer-manager": "2.8.0",
|
||||
"@atlaskit/lozenge": "3.4.2",
|
||||
"@atlaskit/modal-dialog": "3.4.0",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@atlaskit/spinner": "4.0.0",
|
||||
"@atlaskit/tabs": "4.0.1",
|
||||
"@atlaskit/theme": "2.4.0",
|
||||
"@atlaskit/tooltip": "9.1.1",
|
||||
"@atlaskit/tooltip": "6.0.0",
|
||||
"autosize": "1.18.13",
|
||||
"es6-iterator": "2.0.3",
|
||||
"es6-symbol": "3.1.1",
|
||||
@@ -46,9 +46,10 @@
|
||||
"jquery-i18next": "1.2.0",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#39eea17e7e2f8ff4cc273239a6e73f2a149b96e2",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
|
||||
"lodash": "4.17.4",
|
||||
"moment": "2.19.4",
|
||||
"nuclear-js": "1.4.0",
|
||||
"postis": "2.2.0",
|
||||
"prop-types": "15.6.0",
|
||||
"react": "16.2.0",
|
||||
@@ -58,15 +59,15 @@
|
||||
"react-native-background-timer": "2.0.0",
|
||||
"react-native-calendar-events": "1.4.3",
|
||||
"react-native-callstats": "3.27.0",
|
||||
"react-native-fetch-blob": "github:flatfox-ag/react-native-fetch-blob#exception_fixes",
|
||||
"react-native-fetch-blob": "0.10.8",
|
||||
"react-native-img-cache": "1.5.2",
|
||||
"react-native-immersive": "1.1.0",
|
||||
"react-native-keep-awake": "2.0.6",
|
||||
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#cc76092fc4335488a28a9529c8b50afae2c3ecdc",
|
||||
"react-native-prompt": "1.0.0",
|
||||
"react-native-sound": "0.10.9",
|
||||
"react-native-sound": "0.10.4",
|
||||
"react-native-vector-icons": "4.4.2",
|
||||
"react-native-webrtc": "github:jitsi/react-native-webrtc#84aee6693195c5be6b6f5f794a1e52dc7913fb3b",
|
||||
"react-native-webrtc": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
|
||||
"react-redux": "5.0.6",
|
||||
"redux": "3.7.2",
|
||||
"redux-thunk": "2.2.0",
|
||||
@@ -98,7 +99,7 @@
|
||||
"file-loader": "1.1.5",
|
||||
"flow-bin": "0.57.3",
|
||||
"imports-loader": "0.7.1",
|
||||
"node-sass": "4.8.3",
|
||||
"node-sass": "3.13.1",
|
||||
"precommit-hook": "3.0.0",
|
||||
"string-replace-loader": "1.3.0",
|
||||
"style-loader": "0.19.0",
|
||||
|
||||
@@ -2,10 +2,54 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import ToolboxAlwaysOnTop from './ToolboxAlwaysOnTop';
|
||||
import StatelessToolbar from '../toolbox/components/StatelessToolbar';
|
||||
import StatelessToolbarButton
|
||||
from '../toolbox/components/StatelessToolbarButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
/**
|
||||
* Map with toolbar button descriptors.
|
||||
*/
|
||||
const TOOLBAR_BUTTONS = {
|
||||
/**
|
||||
* The descriptor of the camera toolbar button.
|
||||
*/
|
||||
camera: {
|
||||
classNames: [ 'button', 'icon-camera' ],
|
||||
enabled: true,
|
||||
id: 'toolbar_button_camera',
|
||||
onClick() {
|
||||
api.executeCommand('toggleVideo');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The descriptor of the toolbar button which hangs up the call/conference.
|
||||
*/
|
||||
hangup: {
|
||||
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
|
||||
enabled: true,
|
||||
id: 'toolbar_button_hangup',
|
||||
onClick() {
|
||||
api.executeCommand('hangup');
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The descriptor of the microphone toolbar button.
|
||||
*/
|
||||
microphone: {
|
||||
classNames: [ 'button', 'icon-microphone' ],
|
||||
enabled: true,
|
||||
id: 'toolbar_button_mute',
|
||||
onClick() {
|
||||
api.executeCommand('toggleAudio');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The timeout in ms for hidding the toolbar.
|
||||
*/
|
||||
@@ -341,16 +385,62 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const className
|
||||
= `toolbar_primary always-on-top ${
|
||||
this.state.visible ? 'fadeIn' : 'fadeOut'}`;
|
||||
|
||||
return (
|
||||
<div id = 'alwaysOnTop'>
|
||||
<ToolboxAlwaysOnTop
|
||||
audioAvailable = { this.state.audioAvailable }
|
||||
audioMuted = { this.state.audioMuted }
|
||||
className = { this.state.visible ? 'fadeIn' : 'fadeOut' }
|
||||
<StatelessToolbar
|
||||
className = { className }
|
||||
onMouseOut = { this._onMouseOut }
|
||||
onMouseOver = { this._onMouseOver }
|
||||
videoAvailable = { this.state.videoAvailable }
|
||||
videoMuted = { this.state.videoMuted } />
|
||||
onMouseOver = { this._onMouseOver }>
|
||||
{
|
||||
Object.entries(TOOLBAR_BUTTONS).map(
|
||||
([ key, button ]) => {
|
||||
// XXX The following silences a couple of flow
|
||||
// errors:
|
||||
if (button === null
|
||||
|| typeof button !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { onClick } = button;
|
||||
let enabled = false;
|
||||
let toggled = false;
|
||||
|
||||
switch (key) {
|
||||
case 'microphone':
|
||||
enabled = this.state.audioAvailable;
|
||||
toggled = enabled
|
||||
? this.state.audioMuted : true;
|
||||
break;
|
||||
case 'camera':
|
||||
enabled = this.state.videoAvailable;
|
||||
toggled = enabled
|
||||
? this.state.videoMuted : true;
|
||||
break;
|
||||
default: // hangup button
|
||||
toggled = false;
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
const updatedButton = {
|
||||
...button,
|
||||
enabled,
|
||||
toggled
|
||||
};
|
||||
|
||||
return (
|
||||
<StatelessToolbarButton
|
||||
button = { updatedButton }
|
||||
key = { key }
|
||||
onClick = { onClick } />
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</StatelessToolbar>
|
||||
{
|
||||
this._renderVideoNotAvailableScreen()
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// FIXME: AlwaysOnTop imports the button directly in order to avoid bringing in
|
||||
// other components that use lib-jitsi-meet, which always on top does not
|
||||
// import.
|
||||
import ToolbarButton from '../toolbox/components/ToolbarButton';
|
||||
|
||||
const { api } = window.alwaysOnTop;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ToolboxAlwaysOnTop}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether or not microphone access is available.
|
||||
*/
|
||||
audioAvailable: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the user is currently audio muted.
|
||||
*/
|
||||
audioMuted: boolean,
|
||||
|
||||
/**
|
||||
* Additional CSS class names to add to the root of the toolbar.
|
||||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* Callback invoked when no longer moused over the toolbar.
|
||||
*/
|
||||
onMouseOut: Function,
|
||||
|
||||
/**
|
||||
* Callback invoked when the mouse has moved over the toolbar.
|
||||
*/
|
||||
onMouseOver: Function,
|
||||
|
||||
/**
|
||||
* Whether or not camera access is available.
|
||||
*/
|
||||
videoAvailable: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the user is currently video muted.
|
||||
*/
|
||||
videoMuted: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents the toolbar in the Always On Top window.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class ToolboxAlwaysOnTop extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ToolboxAlwaysOnTop} instance.
|
||||
*
|
||||
* @param {Props} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onToolbarHangup = this._onToolbarHangup.bind(this);
|
||||
this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this);
|
||||
this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
audioAvailable,
|
||||
audioMuted,
|
||||
className = '',
|
||||
onMouseOut,
|
||||
onMouseOver,
|
||||
videoAvailable,
|
||||
videoMuted
|
||||
} = this.props;
|
||||
|
||||
const videoMuteIcon = `${videoMuted || !videoAvailable
|
||||
? 'icon-camera-disabled toggled' : 'icon-camera'} ${
|
||||
videoAvailable ? '' : 'disabled'}`;
|
||||
const audioMuteIcon = `${audioMuted || !audioAvailable
|
||||
? 'icon-mic-disabled toggled' : 'icon-microphone'} ${
|
||||
audioAvailable ? '' : 'disabled'}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `always-on-top-toolbox ${className}` }
|
||||
onMouseOut = { onMouseOut }
|
||||
onMouseOver = { onMouseOver }>
|
||||
<ToolbarButton
|
||||
accessibilityLabel = 'Video mute'
|
||||
iconName = { videoMuteIcon }
|
||||
onClick = { this._onToolbarToggleVideo } />
|
||||
<ToolbarButton
|
||||
accessibilityLabel = 'Hangup'
|
||||
iconName = 'icon-hangup'
|
||||
onClick = { this._onToolbarHangup } />
|
||||
<ToolbarButton
|
||||
accessibilityLabel = 'Audio mute'
|
||||
iconName = { audioMuteIcon }
|
||||
onClick = { this._onToolbarToggleAudio } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onToolbarHangup: () => void;
|
||||
|
||||
/**
|
||||
* Ends the conference call and closes the always on top window.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarHangup() {
|
||||
api.executeCommand('hangup');
|
||||
window.close();
|
||||
}
|
||||
|
||||
_onToolbarToggleAudio: () => void;
|
||||
|
||||
/**
|
||||
* Toggles audio mute if audio is avaiable.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleAudio() {
|
||||
if (this.props.audioAvailable) {
|
||||
api.executeCommand('toggleAudio');
|
||||
}
|
||||
}
|
||||
|
||||
_onToolbarToggleVideo: () => void;
|
||||
|
||||
/**
|
||||
* Toggles video mute if video is avaiable.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarToggleVideo() {
|
||||
if (this.props.videoAvailable) {
|
||||
api.executeCommand('toggleVideo');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,21 +129,16 @@ export function createFeedbackOpenEvent() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event for an action regarding the AddPeopleDialog (invites).
|
||||
* Creates an event which indicates that the invite dialog was closed. This is
|
||||
* not a TYPE_UI event, since it is not necessarily the result of a user
|
||||
* interaction.
|
||||
*
|
||||
* @param {string} action - The action that the event represents.
|
||||
* @param {string} actionSubject - The subject that was acted upon.
|
||||
* @param {boolean} attributes - Additional attributes to attach to the event.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createInviteDialogEvent(
|
||||
action, actionSubject, attributes = {}) {
|
||||
export function createInviteDialogClosedEvent() {
|
||||
return {
|
||||
action,
|
||||
actionSubject,
|
||||
attributes,
|
||||
source: 'inviteDialog'
|
||||
action: 'invite.dialog.closed'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -124,8 +124,10 @@ function _appNavigateToMandatoryLocation(
|
||||
});
|
||||
}
|
||||
|
||||
const profile = getState()['features/base/profile'];
|
||||
|
||||
return promise.then(() =>
|
||||
dispatch(setConfig(config)));
|
||||
dispatch(setConfig(_mergeConfigWithProfile(config, profile))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,3 +290,23 @@ function _loadConfig({ contextRoot, host, protocol, room }) {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the downloaded config with the current profile values. The profile
|
||||
* values are named the same way as the config values in the config.js so
|
||||
* a clean merge is possible.
|
||||
*
|
||||
* @param {Object|undefined} config - The downloaded config.
|
||||
* @param {Object} profile - The persisted profile.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mergeConfigWithProfile(config, profile) {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
...profile
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @flow
|
||||
|
||||
/* @flow */
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
export * from './getRouteToRender';
|
||||
@@ -12,13 +11,3 @@ export * from './getRouteToRender';
|
||||
export function getName() {
|
||||
return NativeModules.AppInfo.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the Jitsi Meet SDK bundle on iOS. On Android it will be
|
||||
* undefined.
|
||||
*
|
||||
* @returns {string|undefined}
|
||||
*/
|
||||
export function getSdkBundlePath() {
|
||||
return NativeModules.AppInfo.sdkBundlePath;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* @flow */
|
||||
|
||||
import { Platform } from '../base/react';
|
||||
import { toState } from '../base/redux';
|
||||
import { getDeepLinkingPage } from '../deep-linking';
|
||||
import {
|
||||
NoMobileApp,
|
||||
PluginRequiredBrowser,
|
||||
UnsupportedDesktopBrowser
|
||||
UnsupportedDesktopBrowser,
|
||||
UnsupportedMobileBrowser
|
||||
} from '../unsupported-browser';
|
||||
|
||||
import {
|
||||
@@ -22,18 +24,43 @@ declare var loggingConfig: Object;
|
||||
*
|
||||
* @private
|
||||
* @param {Object} state - Object containing current redux state.
|
||||
* @returns {Promise<ReactElement>|void}
|
||||
* @returns {ReactElement|void}
|
||||
* @type {Function[]}
|
||||
*/
|
||||
const _INTERCEPT_COMPONENT_RULES = [
|
||||
getDeepLinkingPage,
|
||||
|
||||
/**
|
||||
* This rule describes case when user opens application using mobile
|
||||
* browser. In order to promote the app, we choose to suggest the mobile
|
||||
* app even if the browser supports the app (e.g. Google Chrome with
|
||||
* WebRTC support on Android).
|
||||
*
|
||||
* @param {Object} state - The redux state of the app.
|
||||
* @returns {UnsupportedMobileBrowser|void} If the rule is satisfied then
|
||||
* we should intercept existing component by UnsupportedMobileBrowser.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
state => {
|
||||
const OS = Platform.OS;
|
||||
|
||||
if (OS === 'android' || OS === 'ios') {
|
||||
const mobileAppPromo
|
||||
= typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.MOBILE_APP_PROMO;
|
||||
|
||||
return (
|
||||
typeof mobileAppPromo === 'undefined' || Boolean(mobileAppPromo)
|
||||
? UnsupportedMobileBrowser
|
||||
: NoMobileApp);
|
||||
}
|
||||
},
|
||||
state => {
|
||||
const { webRTCReady } = state['features/base/lib-jitsi-meet'];
|
||||
|
||||
switch (typeof webRTCReady) {
|
||||
case 'boolean':
|
||||
if (webRTCReady === false) {
|
||||
return Promise.resolve(UnsupportedDesktopBrowser);
|
||||
return UnsupportedDesktopBrowser;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -42,10 +69,8 @@ const _INTERCEPT_COMPONENT_RULES = [
|
||||
break;
|
||||
|
||||
default:
|
||||
return Promise.resolve(PluginRequiredBrowser);
|
||||
return PluginRequiredBrowser;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
];
|
||||
|
||||
@@ -55,19 +80,16 @@ const _INTERCEPT_COMPONENT_RULES = [
|
||||
*
|
||||
* @param {(Object|Function)} stateOrGetState - The redux state or
|
||||
* {@link getState} function.
|
||||
* @returns {Promise<Route>}
|
||||
* @returns {Route}
|
||||
*/
|
||||
export function _getRouteToRender(stateOrGetState: Object | Function): Object {
|
||||
export function _getRouteToRender(stateOrGetState: Object | Function) {
|
||||
const route = _super_getRouteToRender(stateOrGetState);
|
||||
|
||||
// Intercepts route components if any of component interceptor rules is
|
||||
// satisfied.
|
||||
return _interceptComponent(stateOrGetState, route.component).then(
|
||||
(component: React$Element<*>) => {
|
||||
route.component = component;
|
||||
route.component = _interceptComponent(stateOrGetState, route.component);
|
||||
|
||||
return route;
|
||||
}, () => Promise.resolve(route));
|
||||
return route;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,24 +99,23 @@ export function _getRouteToRender(stateOrGetState: Object | Function): Object {
|
||||
* {@link getState} function.
|
||||
* @param {ReactElement} component - Current route component to render.
|
||||
* @private
|
||||
* @returns {Promise<ReactElement>} If any of the pre-defined rules is
|
||||
* satisfied, returns intercepted component.
|
||||
* @returns {ReactElement} If any of the pre-defined rules is satisfied, returns
|
||||
* intercepted component.
|
||||
*/
|
||||
function _interceptComponent(
|
||||
stateOrGetState: Object | Function,
|
||||
component: React$Element<*>) {
|
||||
let result;
|
||||
const state = toState(stateOrGetState);
|
||||
|
||||
const promises = [];
|
||||
for (const rule of _INTERCEPT_COMPONENT_RULES) {
|
||||
result = rule(state);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_INTERCEPT_COMPONENT_RULES.forEach(rule => {
|
||||
promises.push(rule(state));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(
|
||||
results =>
|
||||
results.find(result => typeof result !== 'undefined') || component,
|
||||
() => Promise.resolve(component));
|
||||
return result || component;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,18 +79,7 @@ function _navigate({ getState }) {
|
||||
const { app } = state['features/app'];
|
||||
const routeToRender = _getRouteToRender(state);
|
||||
|
||||
// XXX Web changed _getRouteToRender to return Promsie instead of Route.
|
||||
// Unfortunately, the commit left mobile to return Route.
|
||||
let routeToRenderPromise;
|
||||
|
||||
if (routeToRender && typeof routeToRender.then === 'function') {
|
||||
routeToRenderPromise = routeToRender;
|
||||
}
|
||||
if (!routeToRenderPromise) {
|
||||
routeToRenderPromise = Promise.resolve(routeToRender);
|
||||
}
|
||||
|
||||
routeToRenderPromise.then(app._navigate.bind(app));
|
||||
return app._navigate(routeToRender);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -62,17 +62,6 @@ export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
|
||||
*/
|
||||
export const DATA_CHANNEL_OPENED = Symbol('DATA_CHANNEL_OPENED');
|
||||
|
||||
/**
|
||||
* The type of action which signals that the user has been kicked out from
|
||||
* the conference.
|
||||
*
|
||||
* {
|
||||
* type: KICKED_OUT,
|
||||
* conference: JitsiConference
|
||||
* }
|
||||
*/
|
||||
export const KICKED_OUT = Symbol('KICKED_OUT');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the lock state of a specific
|
||||
* {@code JitsiConference} changed.
|
||||
@@ -107,18 +96,6 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
|
||||
*/
|
||||
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the desktop sharing enabled flag for
|
||||
* the current conference.
|
||||
*
|
||||
* {
|
||||
* type: SET_DESKTOP_SHARING_ENABLED,
|
||||
* desktopSharingEnabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_DESKTOP_SHARING_ENABLED
|
||||
= Symbol('SET_DESKTOP_SHARING_ENABLED');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Follow Me feature.
|
||||
|
||||
@@ -28,11 +28,9 @@ import {
|
||||
CONFERENCE_WILL_JOIN,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
DATA_CHANNEL_OPENED,
|
||||
KICKED_OUT,
|
||||
LOCK_STATE_CHANGED,
|
||||
P2P_STATUS_CHANGED,
|
||||
SET_AUDIO_ONLY,
|
||||
SET_DESKTOP_SHARING_ENABLED,
|
||||
SET_FOLLOW_ME,
|
||||
SET_LASTN,
|
||||
SET_PASSWORD,
|
||||
@@ -79,10 +77,6 @@ function _addConferenceListeners(conference, dispatch) {
|
||||
JitsiConferenceEvents.CONFERENCE_LEFT,
|
||||
(...args) => dispatch(conferenceLeft(conference, ...args)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.KICKED,
|
||||
() => dispatch(kickedOut(conference)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.LOCK_STATE_CHANGED,
|
||||
(...args) => dispatch(lockStateChanged(conference, ...args)));
|
||||
@@ -363,23 +357,6 @@ export function dataChannelOpened() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that we've been kicked out of the conference.
|
||||
*
|
||||
* @param {JitsiConference} conference - The {@link JitsiConference} instance
|
||||
* for which the event is being signaled.
|
||||
* @returns {{
|
||||
* type: KICKED_OUT,
|
||||
* conference: JitsiConference
|
||||
* }}
|
||||
*/
|
||||
export function kickedOut(conference: Object) {
|
||||
return {
|
||||
type: KICKED_OUT,
|
||||
conference
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the lock state of a specific JitsiConference changed.
|
||||
*
|
||||
@@ -456,22 +433,6 @@ export function setAudioOnly(audioOnly: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag for indicating if desktop sharing is enabled.
|
||||
*
|
||||
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
|
||||
* @returns {{
|
||||
* type: SET_DESKTOP_SHARING_ENABLED,
|
||||
* desktopSharingEnabled: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
|
||||
return {
|
||||
type: SET_DESKTOP_SHARING_ENABLED,
|
||||
desktopSharingEnabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the Follow Me feature.
|
||||
*
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
LOCK_STATE_CHANGED,
|
||||
P2P_STATUS_CHANGED,
|
||||
SET_AUDIO_ONLY,
|
||||
SET_DESKTOP_SHARING_ENABLED,
|
||||
SET_FOLLOW_ME,
|
||||
SET_PASSWORD,
|
||||
SET_RECEIVE_VIDEO_QUALITY,
|
||||
@@ -58,9 +57,6 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
|
||||
case SET_AUDIO_ONLY:
|
||||
return _setAudioOnly(state, action);
|
||||
|
||||
case SET_DESKTOP_SHARING_ENABLED:
|
||||
return _setDesktopSharingEnabled(state, action);
|
||||
|
||||
case SET_FOLLOW_ME:
|
||||
return {
|
||||
...state,
|
||||
@@ -333,21 +329,6 @@ function _setAudioOnly(state, action) {
|
||||
return set(state, 'audioOnly', action.audioOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
|
||||
* base/conference.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature base/conference.
|
||||
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
|
||||
* reduce.
|
||||
* @private
|
||||
* @returns {Object} The new state of the feature base/conference after the
|
||||
* reduction of the specified action.
|
||||
*/
|
||||
function _setDesktopSharingEnabled(state, action) {
|
||||
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ const WHITELISTED_KEYS = [
|
||||
'_peerConnStatusRtcMuteTimeout',
|
||||
'abTesting',
|
||||
'alwaysVisibleToolbar',
|
||||
'autoEnableDesktopSharing',
|
||||
'autoRecord',
|
||||
'autoRecordToken',
|
||||
'avgRtpStatsN',
|
||||
@@ -64,7 +65,6 @@ const WHITELISTED_KEYS = [
|
||||
'firefox_fake_device',
|
||||
'forceJVB121Ratio',
|
||||
'gatherStats',
|
||||
'googleApiApplicationClientID',
|
||||
'hiddenDomain',
|
||||
'hosts',
|
||||
'iAmRecorder',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
@@ -54,12 +54,10 @@ export function connect() {
|
||||
/**
|
||||
* Closes connection.
|
||||
*
|
||||
* @param {boolean} [requestFeedback] - Whether or not to attempt showing a
|
||||
* request for call feedback.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function disconnect(requestFeedback: boolean = false) {
|
||||
export function disconnect() {
|
||||
// XXX For web based version we use conference hanging up logic from the old
|
||||
// app.
|
||||
return () => APP.conference.hangup(requestFeedback);
|
||||
return () => APP.conference.hangup();
|
||||
}
|
||||
|
||||
@@ -52,24 +52,6 @@ export function getURLWithoutParams(url: URL): URL {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a URL string without hash and query/search params from a specific
|
||||
* {@code URL}.
|
||||
*
|
||||
* @param {URL} url - The {@code URL} which may have hash and query/search
|
||||
* params.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getURLWithoutParamsNormalized(url: URL): string {
|
||||
const urlWithoutParams = getURLWithoutParams(url).href;
|
||||
|
||||
if (urlWithoutParams) {
|
||||
return urlWithoutParams.toLowerCase();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a specific id to jid if it's not jid yet.
|
||||
*
|
||||
|
||||
@@ -39,12 +39,6 @@ type Props = {
|
||||
*/
|
||||
disableBlanketClickDismiss: boolean,
|
||||
|
||||
/**
|
||||
* If true, the cancel button will not display but cancel actions, like
|
||||
* clicking the blanket, will cancel.
|
||||
*/
|
||||
hideCancelButton: boolean,
|
||||
|
||||
/**
|
||||
* Whether the dialog is modal. This means clicking on the blanket will
|
||||
* leave the dialog open. No cancel button.
|
||||
@@ -269,9 +263,7 @@ class StatelessDialog extends Component<Props> {
|
||||
* not modal.
|
||||
*/
|
||||
_renderCancelButton(options = {}) {
|
||||
if (options.cancelDisabled
|
||||
|| options.isModal
|
||||
|| options.hideCancelButton) {
|
||||
if (options.cancelDisabled || options.isModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -231,54 +231,8 @@ function _setRemoteDescription(sessionDescription) {
|
||||
});
|
||||
}
|
||||
|
||||
// XXX The function _synthesizeIPv6FromIPv4Address is not placed relative to the
|
||||
// other functions in the file according to alphabetical sorting rule of the
|
||||
// coding style. But eslint wants constants to be defined before they are used.
|
||||
|
||||
/**
|
||||
* Synthesizes an IPv6 address from a specific IPv4 address.
|
||||
*
|
||||
* @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be
|
||||
* synthesized.
|
||||
* @returns {Promise<?string>} A {@code Promise} which gets resolved with the
|
||||
* IPv6 address synthesized from the specified {@code ipv4} or a falsy value to
|
||||
* be treated as inability to synthesize an IPv6 address from the specified
|
||||
* {@code ipv4}.
|
||||
*/
|
||||
const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
|
||||
// POSIX.getaddrinfo
|
||||
const { POSIX } = NativeModules;
|
||||
|
||||
if (POSIX) {
|
||||
const { getaddrinfo } = POSIX;
|
||||
|
||||
if (typeof getaddrinfo === 'function') {
|
||||
return ipv4 =>
|
||||
getaddrinfo(/* hostname */ ipv4, /* servname */ undefined)
|
||||
.then(([ { ai_addr: ipv6 } ]) => ipv6);
|
||||
}
|
||||
}
|
||||
|
||||
// NAT64AddrInfo.getIPv6Address
|
||||
const { NAT64AddrInfo } = NativeModules;
|
||||
|
||||
if (NAT64AddrInfo) {
|
||||
const { getIPv6Address } = NAT64AddrInfo;
|
||||
|
||||
if (typeof getIPv6Address === 'function') {
|
||||
return getIPv6Address;
|
||||
}
|
||||
}
|
||||
|
||||
// There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address.
|
||||
return () =>
|
||||
Promise.reject(
|
||||
'The impossible just happened! No POSIX.getaddrinfo or'
|
||||
+ ' NAT64AddrInfo.getIPv6Address!');
|
||||
})();
|
||||
|
||||
/**
|
||||
* Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
|
||||
* Synthesize IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
|
||||
*
|
||||
* @param {RTCSessionDescription} sdp - The RTCSessionDescription which
|
||||
* specifies the configuration of the remote end of the connection.
|
||||
@@ -286,6 +240,12 @@ const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _synthesizeIPv6Addresses(sdp) {
|
||||
// The synthesis of IPv6 addresses is implemented on iOS only at the time of
|
||||
// this writing.
|
||||
if (!NativeModules.POSIX) {
|
||||
return Promise.resolve(sdp);
|
||||
}
|
||||
|
||||
return (
|
||||
new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp)))
|
||||
.then(({ ips, lines }) =>
|
||||
@@ -312,6 +272,7 @@ function _synthesizeIPv6Addresses0(sessionDescription) {
|
||||
let start = 0;
|
||||
const lines = [];
|
||||
const ips = new Map();
|
||||
const { getaddrinfo } = NativeModules.POSIX;
|
||||
|
||||
do {
|
||||
const end = sdp.indexOf('\r\n', start);
|
||||
@@ -350,10 +311,9 @@ function _synthesizeIPv6Addresses0(sessionDescription) {
|
||||
if (v && typeof v === 'string') {
|
||||
resolve(v);
|
||||
} else {
|
||||
_synthesizeIPv6FromIPv4Address(ip).then(
|
||||
value => {
|
||||
if (!value
|
||||
|| value.indexOf(':') === -1
|
||||
getaddrinfo(ip, undefined).then(
|
||||
([ { ai_addr: value } ]) => {
|
||||
if (value.indexOf(':') === -1
|
||||
|| value === ips.get(ip)) {
|
||||
ips.delete(ip);
|
||||
} else {
|
||||
|
||||
@@ -195,18 +195,6 @@ function _visitNode(node, callback) {
|
||||
};
|
||||
}
|
||||
|
||||
// Element.remove
|
||||
//
|
||||
// Required by:
|
||||
// - lib-jitsi-meet ChatRoom#onPresence parsing
|
||||
if (typeof elementPrototype.remove === 'undefined') {
|
||||
elementPrototype.remove = function() {
|
||||
if (this.parentNode !== null) {
|
||||
this.parentNode.removeChild(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Element.innerHTML
|
||||
//
|
||||
// Required by:
|
||||
@@ -243,31 +231,6 @@ function _visitNode(node, callback) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Element.children
|
||||
//
|
||||
// Required by:
|
||||
// - lib-jitsi-meet ChatRoom#onPresence parsing
|
||||
if (!elementPrototype.hasOwnProperty('children')) {
|
||||
Object.defineProperty(elementPrototype, 'children', {
|
||||
get() {
|
||||
const nodes = this.childNodes;
|
||||
const children = [];
|
||||
let i = 0;
|
||||
let node = nodes[i];
|
||||
|
||||
while (node) {
|
||||
if (node.nodeType === 1) {
|
||||
children.push(node);
|
||||
}
|
||||
i += 1;
|
||||
node = nodes[i];
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME There is a weird infinite loop related to console.log and
|
||||
|
||||
@@ -49,18 +49,6 @@ export const SET_VIDEO_AVAILABLE = Symbol('SET_VIDEO_AVAILABLE');
|
||||
*/
|
||||
export const SET_VIDEO_MUTED = Symbol('SET_VIDEO_MUTED');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to store the last video {@link Transform} applied
|
||||
* to a stream.
|
||||
*
|
||||
* {
|
||||
* type: STORE_VIDEO_TRANSFORM,
|
||||
* streamId: string,
|
||||
* transform: Transform
|
||||
* }
|
||||
*/
|
||||
export const STORE_VIDEO_TRANSFORM = Symbol('STORE_VIDEO_TRANSFORM');
|
||||
|
||||
/**
|
||||
* The type of (redux) action to toggle the local video camera facing mode. In
|
||||
* contrast to SET_CAMERA_FACING_MODE, allows the toggling to be optimally
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
SET_CAMERA_FACING_MODE,
|
||||
SET_VIDEO_AVAILABLE,
|
||||
SET_VIDEO_MUTED,
|
||||
STORE_VIDEO_TRANSFORM,
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
|
||||
@@ -19,9 +18,9 @@ import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
|
||||
* @param {boolean} available - True if the local audio is to be marked as
|
||||
* available or false if the local audio is not available.
|
||||
* @returns {{
|
||||
* type: SET_AUDIO_AVAILABLE,
|
||||
* available: boolean
|
||||
* }}
|
||||
* type: SET_AUDIO_AVAILABLE,
|
||||
* available: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setAudioAvailable(available: boolean) {
|
||||
return {
|
||||
@@ -113,26 +112,6 @@ export function setVideoMuted(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action to store the last video {@link Transform} applied to a
|
||||
* stream.
|
||||
*
|
||||
* @param {string} streamId - The ID of the stream.
|
||||
* @param {Object} transform - The {@code Transform} to store.
|
||||
* @returns {{
|
||||
* type: STORE_VIDEO_TRANSFORM,
|
||||
* streamId: string,
|
||||
* transform: Object
|
||||
* }}
|
||||
*/
|
||||
export function storeVideoTransform(streamId: string, transform: Object) {
|
||||
return {
|
||||
type: STORE_VIDEO_TRANSFORM,
|
||||
streamId,
|
||||
transform
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the camera facing mode. Most commonly, for example, mobile devices
|
||||
* such as phones have a front/user-facing and a back/environment-facing
|
||||
|
||||
@@ -21,12 +21,6 @@ export default class AbstractVideoTrack extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the {@link Video} of
|
||||
* {@code AbstractVideoTrack} is clicked/pressed.
|
||||
*/
|
||||
onPress: PropTypes.func,
|
||||
|
||||
videoTrack: PropTypes.object,
|
||||
|
||||
waitForVideoStarted: PropTypes.bool,
|
||||
@@ -36,12 +30,7 @@ export default class AbstractVideoTrack extends Component {
|
||||
* of all Videos. For more details, refer to the zOrder property of the
|
||||
* Video class for React Native.
|
||||
*/
|
||||
zOrder: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: PropTypes.bool
|
||||
zOrder: PropTypes.number
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -85,7 +74,7 @@ export default class AbstractVideoTrack extends Component {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { videoTrack } = this.state;
|
||||
const videoTrack = this.state.videoTrack;
|
||||
let render;
|
||||
|
||||
if (this.props.waitForVideoStarted) {
|
||||
@@ -113,21 +102,12 @@ export default class AbstractVideoTrack extends Component {
|
||||
const stream
|
||||
= render ? videoTrack.jitsiTrack.getOriginalStream() : null;
|
||||
|
||||
// Actual zoom is currently only enabled if the stream is a desktop
|
||||
// stream.
|
||||
const zoomEnabled
|
||||
= this.props.zoomEnabled
|
||||
&& stream
|
||||
&& videoTrack.videoType === 'desktop';
|
||||
|
||||
return (
|
||||
<Video
|
||||
mirror = { videoTrack && videoTrack.mirror }
|
||||
onPlaying = { this._onVideoPlaying }
|
||||
onPress = { this.props.onPress }
|
||||
stream = { stream }
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { zoomEnabled } />
|
||||
zOrder = { this.props.zOrder } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,9 +118,10 @@ export default class AbstractVideoTrack extends Component {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onVideoPlaying() {
|
||||
const { videoTrack } = this.props;
|
||||
const videoTrack = this.props.videoTrack;
|
||||
|
||||
if (videoTrack && !videoTrack.videoStarted) {
|
||||
if (videoTrack
|
||||
&& !videoTrack.videoStarted) {
|
||||
this.props.dispatch(trackVideoStarted(videoTrack.jitsiTrack));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class Audio extends AbstractAudio {
|
||||
this._sound
|
||||
= this.props.src
|
||||
? new Sound(
|
||||
this.props.src, null,
|
||||
this.props.src,
|
||||
this._soundLoadedCallback.bind(this))
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { RTCView } from 'react-native-webrtc';
|
||||
|
||||
import styles from './styles';
|
||||
import VideoTransform from './VideoTransform';
|
||||
|
||||
/**
|
||||
* The React Native {@link Component} which is similar to Web's
|
||||
@@ -20,14 +19,7 @@ export default class Video extends Component<*> {
|
||||
*/
|
||||
static propTypes = {
|
||||
mirror: PropTypes.bool,
|
||||
|
||||
onPlaying: PropTypes.func,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the {@code Video} is clicked/pressed.
|
||||
*/
|
||||
onPress: PropTypes.func,
|
||||
|
||||
stream: PropTypes.object,
|
||||
|
||||
/**
|
||||
@@ -53,12 +45,7 @@ export default class Video extends Component<*> {
|
||||
* values: 0 for the remote video(s) which appear in the background, and
|
||||
* 1 for the local video(s) which appear above the remote video(s).
|
||||
*/
|
||||
zOrder: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: PropTypes.bool
|
||||
zOrder: PropTypes.number
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,29 +68,28 @@ export default class Video extends Component<*> {
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
const { stream, zoomEnabled } = this.props;
|
||||
const { stream } = this.props;
|
||||
|
||||
if (stream) {
|
||||
const streamURL = stream.toURL();
|
||||
const style = styles.video;
|
||||
const objectFit
|
||||
= zoomEnabled
|
||||
? 'contain'
|
||||
: (style && style.objectFit) || 'cover';
|
||||
|
||||
// XXX The CSS style object-fit that we utilize on Web is not
|
||||
// supported on React Native. Adding objectFit to React Native's
|
||||
// StyleSheet appears to be impossible without hacking and an
|
||||
// unjustified amount of effort. Consequently, I've chosen to define
|
||||
// objectFit on RTCView itself. Anyway, prepare to accommodate a
|
||||
// future definition of objectFit in React Native's StyleSheet.
|
||||
const style = styles.video;
|
||||
const objectFit = (style && style.objectFit) || 'cover';
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
return (
|
||||
<VideoTransform
|
||||
enabled = { zoomEnabled }
|
||||
onPress = { this.props.onPress }
|
||||
streamId = { stream.id }
|
||||
style = { style }>
|
||||
<RTCView
|
||||
mirror = { this.props.mirror }
|
||||
objectFit = { objectFit }
|
||||
streamURL = { streamURL }
|
||||
style = { style }
|
||||
zOrder = { this.props.zOrder } />
|
||||
</VideoTransform>
|
||||
<RTCView
|
||||
mirror = { this.props.mirror }
|
||||
objectFit = { objectFit }
|
||||
streamURL = { streamURL }
|
||||
style = { style }
|
||||
zOrder = { this.props.zOrder } />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ class VideoTrack extends AbstractVideoTrack {
|
||||
return (
|
||||
<View style = { styles.video } >
|
||||
{ super.render() }
|
||||
<Animated.View
|
||||
pointerEvents = 'none'
|
||||
style = { animatedStyles } />
|
||||
<Animated.View style = { animatedStyles } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,714 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { PanResponder, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { storeVideoTransform } from '../../actions';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The default/initial transform (= no transform).
|
||||
*/
|
||||
const DEFAULT_TRANSFORM = {
|
||||
scale: 1,
|
||||
translateX: 0,
|
||||
translateY: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* The minimum scale (magnification) multiplier. 1 is equal to objectFit
|
||||
* = 'contain'.
|
||||
*/
|
||||
const MIN_SCALE = 1;
|
||||
|
||||
/*
|
||||
* The max distance from the edge of the screen where we let the user move the
|
||||
* view to. This is large enough now to let the user drag the view to a position
|
||||
* where no other displayed components cover it (such as filmstrip). If a
|
||||
* ViewPort (hint) support is added to the LargeVideo component then this
|
||||
* contant will not be necessary anymore.
|
||||
*/
|
||||
const MAX_OFFSET = 100;
|
||||
|
||||
/**
|
||||
* The max allowed scale (magnification) multiplier.
|
||||
*/
|
||||
const MAX_SCALE = 5;
|
||||
|
||||
/**
|
||||
* The length of a minimum movement after which we consider a gesture a move
|
||||
* instead of a tap/long tap.
|
||||
*/
|
||||
const MOVE_THRESHOLD_DISMISSES_TOUCH = 2;
|
||||
|
||||
/**
|
||||
* A tap timeout after which we consider a gesture a long tap and will not
|
||||
* trigger onPress (unless long tap gesture support is added in the future).
|
||||
*/
|
||||
const TAP_TIMEOUT_MS = 400;
|
||||
|
||||
/**
|
||||
* Type of a transform object this component is capable of handling.
|
||||
*/
|
||||
type Transform = {
|
||||
scale: number,
|
||||
translateX: number,
|
||||
translateY: number
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The children components of this view.
|
||||
*/
|
||||
children: Object,
|
||||
|
||||
/**
|
||||
* Transformation is only enabled when this flag is true.
|
||||
*/
|
||||
enabled: boolean,
|
||||
|
||||
/**
|
||||
* Function to invoke when a press event is detected.
|
||||
*/
|
||||
onPress?: Function,
|
||||
|
||||
/**
|
||||
* The id of the current stream that is displayed.
|
||||
*/
|
||||
streamId: string,
|
||||
|
||||
/**
|
||||
* Style of the top level transformable view.
|
||||
*/
|
||||
style: Object,
|
||||
|
||||
/**
|
||||
* The stored transforms retreived from Redux to be initially applied
|
||||
* to different streams.
|
||||
*/
|
||||
_transforms: Object,
|
||||
|
||||
/**
|
||||
* Action to dispatch when the component is unmounted.
|
||||
*/
|
||||
_onUnmount: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The current (non-transformed) layout of the View.
|
||||
*/
|
||||
layout: ?Object,
|
||||
|
||||
/**
|
||||
* The current transform that is applied.
|
||||
*/
|
||||
transform: Transform
|
||||
};
|
||||
|
||||
/**
|
||||
* An container that captures gestures such as pinch&zoom, touch or move.
|
||||
*/
|
||||
class VideoTransform extends Component<Props, State> {
|
||||
/**
|
||||
* The gesture handler object.
|
||||
*/
|
||||
gestureHandlers: PanResponder;
|
||||
|
||||
/**
|
||||
* The initial distance of the fingers on pinch start.
|
||||
*/
|
||||
initialDistance: number;
|
||||
|
||||
/**
|
||||
* The initial position of the finger on touch start.
|
||||
*/
|
||||
initialPosition: {
|
||||
x: number,
|
||||
y: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Time of the last tap.
|
||||
*/
|
||||
lastTap: number;
|
||||
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
layout: null,
|
||||
transform: DEFAULT_TRANSFORM
|
||||
};
|
||||
|
||||
this._getTransformStyle = this._getTransformStyle.bind(this);
|
||||
this._onGesture = this._onGesture.bind(this);
|
||||
this._onLayout = this._onLayout.bind(this);
|
||||
this._onMoveShouldSetPanResponder
|
||||
= this._onMoveShouldSetPanResponder.bind(this);
|
||||
this._onPanResponderGrant = this._onPanResponderGrant.bind(this);
|
||||
this._onPanResponderMove = this._onPanResponderMove.bind(this);
|
||||
this._onPanResponderRelease = this._onPanResponderRelease.bind(this);
|
||||
this._onStartShouldSetPanResponder
|
||||
= this._onStartShouldSetPanResponder.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillMount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillMount() {
|
||||
this.gestureHandlers = PanResponder.create({
|
||||
onPanResponderGrant: this._onPanResponderGrant,
|
||||
onPanResponderMove: this._onPanResponderMove,
|
||||
onPanResponderRelease: this._onPanResponderRelease,
|
||||
onPanResponderTerminationRequest: () => true,
|
||||
onMoveShouldSetPanResponder: this._onMoveShouldSetPanResponder,
|
||||
onShouldBlockNativeResponder: () => false,
|
||||
onStartShouldSetPanResponder: this._onStartShouldSetPanResponder
|
||||
});
|
||||
|
||||
const { streamId } = this.props;
|
||||
|
||||
this._restoreTransform(streamId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillReceiveProps.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillReceiveProps({ streamId: newStreamId }) {
|
||||
if (this.props.streamId !== newStreamId) {
|
||||
this._storeTransform();
|
||||
this._restoreTransform(newStreamId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React Component's componentWillUnmount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._storeTransform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the empty component that captures the gestures.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { children, style } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout = { this._onLayout }
|
||||
pointerEvents = 'box-only'
|
||||
style = { [
|
||||
styles.videoTransformedViewContaier,
|
||||
style
|
||||
] }
|
||||
{ ...this.gestureHandlers.panHandlers }>
|
||||
<View
|
||||
style = { [
|
||||
styles.videoTranformedView,
|
||||
this._getTransformStyle()
|
||||
] }>
|
||||
{ children }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the new transformation to be applied by merging the current
|
||||
* transform values with the newly received incremental values.
|
||||
*
|
||||
* @param {Transform} transform - The new transform object.
|
||||
* @private
|
||||
* @returns {Transform}
|
||||
*/
|
||||
_calculateTransformIncrement(transform: Transform) {
|
||||
let {
|
||||
scale,
|
||||
translateX,
|
||||
translateY
|
||||
} = this.state.transform;
|
||||
const {
|
||||
scale: newScale,
|
||||
translateX: newTranslateX,
|
||||
translateY: newTranslateY
|
||||
} = transform;
|
||||
|
||||
// Note: We don't limit MIN_SCALE here yet, as we need to detect a scale
|
||||
// down gesture even if the scale is already at MIN_SCALE to let the
|
||||
// user return the screen to center with that gesture. Scale is limited
|
||||
// to MIN_SCALE right before it gets applied.
|
||||
scale = Math.min(scale * (newScale || 1), MAX_SCALE);
|
||||
|
||||
translateX = translateX + ((newTranslateX || 0) / scale);
|
||||
translateY = translateY + ((newTranslateY || 0) / scale);
|
||||
|
||||
return {
|
||||
scale,
|
||||
translateX,
|
||||
translateY
|
||||
};
|
||||
}
|
||||
|
||||
_didMove: Object => boolean
|
||||
|
||||
/**
|
||||
* Determines if there was large enough movement to be handled.
|
||||
*
|
||||
* @param {Object} gestureState - The gesture state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_didMove({ dx, dy }) {
|
||||
return Math.abs(dx) > MOVE_THRESHOLD_DISMISSES_TOUCH
|
||||
|| Math.abs(dy) > MOVE_THRESHOLD_DISMISSES_TOUCH;
|
||||
}
|
||||
|
||||
_getTouchDistance: Object => number;
|
||||
|
||||
/**
|
||||
* Calculates the touch distance on a pinch event.
|
||||
*
|
||||
* @param {Object} evt - The touch event.
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_getTouchDistance({ nativeEvent: { touches } }) {
|
||||
const dx = Math.abs(touches[0].pageX - touches[1].pageX);
|
||||
const dy = Math.abs(touches[0].pageY - touches[1].pageY);
|
||||
|
||||
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
|
||||
}
|
||||
|
||||
_getTouchPosition: Object => Object
|
||||
|
||||
/**
|
||||
* Calculates the position of the touch event.
|
||||
*
|
||||
* @param {Object} evt - The touch event.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getTouchPosition({ nativeEvent: { touches } }) {
|
||||
return {
|
||||
x: touches[0].pageX,
|
||||
y: touches[0].pageY
|
||||
};
|
||||
}
|
||||
|
||||
_getTransformStyle: () => Object
|
||||
|
||||
/**
|
||||
* Generates a transform style object to be used on the component.
|
||||
*
|
||||
* @returns {{string: Array<{string: number}>}}
|
||||
*/
|
||||
_getTransformStyle() {
|
||||
const { enabled } = this.props;
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
scale,
|
||||
translateX,
|
||||
translateY
|
||||
} = this.state.transform;
|
||||
|
||||
return {
|
||||
transform: [
|
||||
{ scale },
|
||||
{ translateX },
|
||||
{ translateY }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the move matrix and then applies the transformation to the
|
||||
* component (updates state).
|
||||
*
|
||||
* Note: Points A (top-left) and D (bottom-right) are opposite points of
|
||||
* the View rectangle.
|
||||
*
|
||||
* @param {Transform} transform - The transformation object.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_limitAndApplyTransformation(transform: Transform) {
|
||||
const { layout } = this.state;
|
||||
|
||||
if (layout) {
|
||||
const { scale } = this.state.transform;
|
||||
const { scale: newScaleUnlimited } = transform;
|
||||
let {
|
||||
translateX: newTranslateX,
|
||||
translateY: newTranslateY
|
||||
} = transform;
|
||||
|
||||
// Scale is only limited to MIN_SCALE here to detect downscale
|
||||
// gesture later.
|
||||
const newScale = Math.max(newScaleUnlimited, MIN_SCALE);
|
||||
|
||||
// The A and D points of the original View (before transform).
|
||||
const originalLayout = {
|
||||
a: {
|
||||
x: layout.x,
|
||||
y: layout.y
|
||||
},
|
||||
d: {
|
||||
x: layout.x + layout.width,
|
||||
y: layout.y + layout.height
|
||||
}
|
||||
};
|
||||
|
||||
// The center point (midpoint) of the transformed View.
|
||||
const transformedCenterPoint = {
|
||||
x: ((layout.x + layout.width) / 2) + (newTranslateX * newScale),
|
||||
y: ((layout.y + layout.height) / 2) + (newTranslateY * newScale)
|
||||
};
|
||||
|
||||
// The size of the transformed View.
|
||||
const transformedSize = {
|
||||
height: layout.height * newScale,
|
||||
width: layout.width * newScale
|
||||
};
|
||||
|
||||
// The A and D points of the transformed View.
|
||||
const transformedLayout = {
|
||||
a: {
|
||||
x: transformedCenterPoint.x - (transformedSize.width / 2),
|
||||
y: transformedCenterPoint.y - (transformedSize.height / 2)
|
||||
},
|
||||
d: {
|
||||
x: transformedCenterPoint.x + (transformedSize.width / 2),
|
||||
y: transformedCenterPoint.y + (transformedSize.height / 2)
|
||||
}
|
||||
};
|
||||
|
||||
let _MAX_OFFSET = MAX_OFFSET;
|
||||
|
||||
if (newScaleUnlimited < scale) {
|
||||
// This is a negative scale event so we dynamycally reduce the
|
||||
// MAX_OFFSET to get the screen back to the center on
|
||||
// downscaling.
|
||||
_MAX_OFFSET = Math.min(MAX_OFFSET, MAX_OFFSET * (newScale - 1));
|
||||
}
|
||||
|
||||
// Correct move matrix if it goes out of the view
|
||||
// too much (_MAX_OFFSET).
|
||||
newTranslateX
|
||||
-= Math.max(
|
||||
transformedLayout.a.x - originalLayout.a.x - _MAX_OFFSET,
|
||||
0);
|
||||
newTranslateX
|
||||
+= Math.max(
|
||||
originalLayout.d.x - transformedLayout.d.x - _MAX_OFFSET,
|
||||
0);
|
||||
newTranslateY
|
||||
-= Math.max(
|
||||
transformedLayout.a.y - originalLayout.a.y - _MAX_OFFSET,
|
||||
0);
|
||||
newTranslateY
|
||||
+= Math.max(
|
||||
originalLayout.d.y - transformedLayout.d.y - _MAX_OFFSET,
|
||||
0);
|
||||
|
||||
this.setState({
|
||||
transform: {
|
||||
scale: newScale,
|
||||
translateX: Math.round(newTranslateX),
|
||||
translateY: Math.round(newTranslateY)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onGesture: (string, ?Object | number) => void
|
||||
|
||||
/**
|
||||
* Handles gestures and converts them to transforms.
|
||||
*
|
||||
* Currently supported gestures:
|
||||
* - scale (punch&zoom-type scale).
|
||||
* - move
|
||||
* - press.
|
||||
*
|
||||
* Note: This component supports onPress solely to overcome the problem of
|
||||
* not being able to register gestures via the PanResponder due to the fact
|
||||
* that the entire Conference component was a single touch responder
|
||||
* component in the past (see base/react/.../Container with an onPress
|
||||
* event) - and stock touch responder components seem to have exclusive
|
||||
* priority in handling touches in React.
|
||||
*
|
||||
* @param {string} type - The type of the gesture.
|
||||
* @param {?Object | number} value - The value of the gesture, if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onGesture(type, value) {
|
||||
let transform;
|
||||
|
||||
switch (type) {
|
||||
case 'move':
|
||||
transform = {
|
||||
...DEFAULT_TRANSFORM,
|
||||
translateX: value.x,
|
||||
translateY: value.y
|
||||
};
|
||||
break;
|
||||
case 'scale':
|
||||
transform = {
|
||||
...DEFAULT_TRANSFORM,
|
||||
scale: value
|
||||
};
|
||||
break;
|
||||
|
||||
case 'press': {
|
||||
const { onPress } = this.props;
|
||||
|
||||
typeof onPress === 'function' && onPress();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (transform) {
|
||||
this._limitAndApplyTransformation(
|
||||
this._calculateTransformIncrement(transform));
|
||||
}
|
||||
|
||||
this.lastTap = 0;
|
||||
}
|
||||
|
||||
_onLayout: Object => void
|
||||
|
||||
/**
|
||||
* Callback for the onLayout of the component.
|
||||
*
|
||||
* @param {Object} event - The native props of the onLayout event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLayout({ nativeEvent: { layout: { x, y, width, height } } }) {
|
||||
this.setState({
|
||||
layout: {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onMoveShouldSetPanResponder: (Object, Object) => boolean
|
||||
|
||||
/**
|
||||
* Function to decide whether the responder should respond to a move event.
|
||||
*
|
||||
* @param {Object} evt - The event.
|
||||
* @param {Object} gestureState - Gesture state.
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onMoveShouldSetPanResponder(evt, gestureState) {
|
||||
return this.props.enabled
|
||||
&& (this._didMove(gestureState)
|
||||
|| gestureState.numberActiveTouches === 2);
|
||||
}
|
||||
|
||||
_onPanResponderGrant: (Object, Object) => void
|
||||
|
||||
/**
|
||||
* Calculates the initial touch distance.
|
||||
*
|
||||
* @param {Object} evt - Touch event.
|
||||
* @param {Object} gestureState - Gesture state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPanResponderGrant(evt, { numberActiveTouches }) {
|
||||
if (numberActiveTouches === 1) {
|
||||
this.initialPosition = this._getTouchPosition(evt);
|
||||
this.lastTap = Date.now();
|
||||
|
||||
} else if (numberActiveTouches === 2) {
|
||||
this.initialDistance = this._getTouchDistance(evt);
|
||||
}
|
||||
}
|
||||
|
||||
_onPanResponderMove: (Object, Object) => void
|
||||
|
||||
/**
|
||||
* Handles the PanResponder move (touch move) event.
|
||||
*
|
||||
* @param {Object} evt - Touch event.
|
||||
* @param {Object} gestureState - Gesture state.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPanResponderMove(evt, gestureState) {
|
||||
if (gestureState.numberActiveTouches === 2) {
|
||||
// this is a zoom event
|
||||
if (
|
||||
this.initialDistance === undefined
|
||||
|| isNaN(this.initialDistance)
|
||||
) {
|
||||
// there is no initial distance because the user started
|
||||
// with only one finger. We calculate it now.
|
||||
this.initialDistance = this._getTouchDistance(evt);
|
||||
} else {
|
||||
const distance = this._getTouchDistance(evt);
|
||||
const scale = distance / (this.initialDistance || 1);
|
||||
|
||||
this.initialDistance = distance;
|
||||
|
||||
this._onGesture('scale', scale);
|
||||
}
|
||||
} else if (gestureState.numberActiveTouches === 1
|
||||
&& isNaN(this.initialDistance)
|
||||
&& this._didMove(gestureState)) {
|
||||
// this is a move event
|
||||
const position = this._getTouchPosition(evt);
|
||||
const move = {
|
||||
x: position.x - this.initialPosition.x,
|
||||
y: position.y - this.initialPosition.y
|
||||
};
|
||||
|
||||
this.initialPosition = position;
|
||||
|
||||
this._onGesture('move', move);
|
||||
}
|
||||
}
|
||||
|
||||
_onPanResponderRelease: () => void
|
||||
|
||||
/**
|
||||
* Handles the PanResponder gesture end event.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPanResponderRelease() {
|
||||
if (this.lastTap && Date.now() - this.lastTap < TAP_TIMEOUT_MS) {
|
||||
this._onGesture('press');
|
||||
}
|
||||
delete this.initialDistance;
|
||||
delete this.initialPosition;
|
||||
}
|
||||
|
||||
_onStartShouldSetPanResponder: () => boolean
|
||||
|
||||
/**
|
||||
* Function to decide whether the responder should respond to a start
|
||||
* (thouch) event.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onStartShouldSetPanResponder() {
|
||||
return typeof this.props.onPress === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the last applied transform when the component is mounted, or
|
||||
* a new stream is about to be rendered.
|
||||
*
|
||||
* @param {string} streamId - The stream id to restore transform for.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_restoreTransform(streamId) {
|
||||
const { enabled, _transforms } = this.props;
|
||||
|
||||
if (enabled) {
|
||||
const initialTransform = _transforms[streamId];
|
||||
|
||||
if (initialTransform) {
|
||||
this.setState({
|
||||
transform: initialTransform
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores/saves the current transform when the component is destroyed, or a
|
||||
* new stream is about to be rendered.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_storeTransform() {
|
||||
const { _onUnmount, enabled, streamId } = this.props;
|
||||
|
||||
if (enabled) {
|
||||
_onUnmount(streamId, this.state.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dispatching of some action to React component props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _onUnmount: Function
|
||||
* }}
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches actions to store the last applied transform to a video.
|
||||
*
|
||||
* @param {string} streamId - The ID of the stream.
|
||||
* @param {Transform} transform - The last applied transform.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUnmount(streamId, transform) {
|
||||
dispatch(storeVideoTransform(streamId, transform));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the component's props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The component's own props.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _transforms: Object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
/**
|
||||
* The stored transforms retreived from Redux to be initially applied to
|
||||
* different streams.
|
||||
*
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
_transforms: state['features/base/media'].video.transforms
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(VideoTransform);
|
||||
@@ -6,23 +6,6 @@ import { ColorPalette } from '../../../styles';
|
||||
* The styles of the feature base/media.
|
||||
*/
|
||||
export default StyleSheet.create({
|
||||
|
||||
/**
|
||||
* Base style of the transformed video view.
|
||||
*/
|
||||
videoTranformedView: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* A basic style to avoid rendering a transformed view off the component,
|
||||
* that can be visible on special occasions, such as during device rotate
|
||||
* animation, or PiP mode.
|
||||
*/
|
||||
videoTransformedViewContaier: {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
/**
|
||||
* Make {@code Video} fill its container.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import {
|
||||
createStartAudioOnlyEvent,
|
||||
@@ -6,18 +6,14 @@ import {
|
||||
createSyncTrackStateEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { isRoomValid, SET_ROOM, setAudioOnly } from '../conference';
|
||||
import { SET_ROOM, setAudioOnly } from '../conference';
|
||||
import { parseURLParams } from '../config';
|
||||
import JitsiMeetJS from '../lib-jitsi-meet';
|
||||
import { getPropertyValue } from '../profile';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { setTrackMuted, TRACK_ADDED } from '../tracks';
|
||||
|
||||
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
|
||||
import { CAMERA_FACING_MODE } from './constants';
|
||||
import {
|
||||
_AUDIO_INITIAL_MEDIA_STATE,
|
||||
_VIDEO_INITIAL_MEDIA_STATE
|
||||
} from './reducer';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -34,9 +30,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case TRACK_ADDED: {
|
||||
const result = next(action);
|
||||
const { track } = action;
|
||||
|
||||
track.local && _syncTrackMutedState(store, track);
|
||||
action.track.local && _syncTrackMutedState(store, action.track);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -60,51 +55,45 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
* specified {@code action}.
|
||||
*/
|
||||
function _setRoom({ dispatch, getState }, next, action) {
|
||||
// Figure out the desires/intents i.e. the state of base/media. There are
|
||||
// multiple desires/intents ordered by precedence such as server-side
|
||||
// config, config overrides in the user-supplied URL, user's own app
|
||||
// settings, etc.
|
||||
const { room } = action;
|
||||
|
||||
// Read the config.
|
||||
|
||||
const state = getState();
|
||||
const { room } = action;
|
||||
const roomIsValid = isRoomValid(room);
|
||||
let urlParams;
|
||||
let audioMuted;
|
||||
let videoMuted;
|
||||
|
||||
// XXX The configurations/preferences/settings startWithAudioMuted,
|
||||
// startWithVideoMuted, and startAudioOnly were introduced for
|
||||
// conferences/meetings. So it makes sense for these to not be considered
|
||||
// outside of conferences/meetings (e.g. WelcomePage). Later on, though, we
|
||||
// introduced a "Video <-> Voice" toggle on the WelcomePage which utilizes
|
||||
// startAudioOnly outside of conferences/meetings so that particular
|
||||
// configuration/preference/setting employs slightly exclusive logic.
|
||||
const mutedSources = {
|
||||
// We have startWithAudioMuted and startWithVideoMuted here:
|
||||
config: true,
|
||||
profile: true,
|
||||
if (room) {
|
||||
// The Jitsi Meet client may override the Jitsi Meet deployment in the
|
||||
// (location) URL on the subject of the following:
|
||||
// - startAudioOnly
|
||||
// - startWithAudioMuted
|
||||
// - startWithVideoMuted
|
||||
urlParams
|
||||
= parseURLParams(state['features/base/connection'].locationURL);
|
||||
|
||||
// XXX We've already overwritten base/config with urlParams. However,
|
||||
// profile is more important than the server-side config. Consequently,
|
||||
// we need to read from urlParams anyway:
|
||||
urlParams: true,
|
||||
audioMuted = urlParams['config.startWithAudioMuted'];
|
||||
videoMuted = urlParams['config.startWithVideoMuted'];
|
||||
}
|
||||
|
||||
// We don't have startWithAudioMuted and startWithVideoMuted here:
|
||||
jwt: false
|
||||
};
|
||||
const audioMuted
|
||||
= roomIsValid
|
||||
? Boolean(
|
||||
getPropertyValue(state, 'startWithAudioMuted', mutedSources))
|
||||
: _AUDIO_INITIAL_MEDIA_STATE.muted;
|
||||
const videoMuted
|
||||
= roomIsValid
|
||||
? Boolean(
|
||||
getPropertyValue(state, 'startWithVideoMuted', mutedSources))
|
||||
: _VIDEO_INITIAL_MEDIA_STATE.muted;
|
||||
// Of course, the Jitsi Meet deployment defines config.js which should be
|
||||
// respected if the client did not override it.
|
||||
const config = state['features/base/config'];
|
||||
|
||||
sendAnalytics(
|
||||
createStartMutedConfigurationEvent('local', audioMuted, videoMuted));
|
||||
logger.log(
|
||||
`Start muted: ${audioMuted ? 'audio, ' : ''}${
|
||||
videoMuted ? 'video' : ''}`);
|
||||
typeof audioMuted === 'undefined'
|
||||
&& (audioMuted = config.startWithAudioMuted);
|
||||
typeof videoMuted === 'undefined'
|
||||
&& (videoMuted = config.startWithVideoMuted);
|
||||
|
||||
audioMuted = Boolean(audioMuted);
|
||||
videoMuted = Boolean(videoMuted);
|
||||
|
||||
sendAnalytics(createStartMutedConfigurationEvent(
|
||||
'local', audioMuted, videoMuted));
|
||||
|
||||
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${
|
||||
videoMuted ? 'video' : ''}`);
|
||||
|
||||
// Unconditionally express the desires/expectations/intents of the app and
|
||||
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
|
||||
@@ -113,57 +102,32 @@ function _setRoom({ dispatch, getState }, next, action) {
|
||||
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
|
||||
dispatch(setVideoMuted(videoMuted));
|
||||
|
||||
// startAudioOnly
|
||||
// config.startAudioOnly
|
||||
//
|
||||
// FIXME Technically, the audio-only feature is owned by base/conference,
|
||||
// not base/media so the following should be in base/conference.
|
||||
// Practically, I presume it was easier to write the source code here
|
||||
// because it looks like startWithAudioMuted and startWithVideoMuted.
|
||||
//
|
||||
// XXX After the introduction of the "Video <-> Voice" toggle on the
|
||||
// WelcomePage, startAudioOnly is utilized even outside of
|
||||
// conferences/meetings.
|
||||
let audioOnly;
|
||||
// because it looks like config.startWithAudioMuted and
|
||||
// config.startWithVideoMuted.
|
||||
if (room) {
|
||||
let audioOnly;
|
||||
|
||||
if (JitsiMeetJS.mediaDevices.supportsVideo()) {
|
||||
audioOnly
|
||||
= Boolean(
|
||||
getPropertyValue(
|
||||
state,
|
||||
'startAudioOnly',
|
||||
/* sources */ {
|
||||
// FIXME Practically, base/config is (really) correct
|
||||
// only if roomIsValid. At the time of this writing,
|
||||
// base/config is overwritten by URL params which leaves
|
||||
// base/config incorrect on the WelcomePage after
|
||||
// leaving a conference which explicitly overwrites
|
||||
// base/config with URL params.
|
||||
config: roomIsValid,
|
||||
if (JitsiMeetJS.mediaDevices.supportsVideo()) {
|
||||
audioOnly = urlParams && urlParams['config.startAudioOnly'];
|
||||
typeof audioOnly === 'undefined'
|
||||
&& (audioOnly = config.startAudioOnly);
|
||||
audioOnly = Boolean(audioOnly);
|
||||
} else {
|
||||
// Always default to being audio only if the current environment
|
||||
// does not support sending or receiving video.
|
||||
audioOnly = true;
|
||||
}
|
||||
|
||||
// XXX We've already overwritten base/config with
|
||||
// urlParams if roomIsValid. However, profile is more
|
||||
// important than the server-side config. Consequently,
|
||||
// we need to read from urlParams anyway. We also
|
||||
// probably want to read from urlParams when
|
||||
// !roomIsValid.
|
||||
urlParams: true,
|
||||
|
||||
// The following don't have complications around whether
|
||||
// they are defined or not:
|
||||
jwt: false,
|
||||
profile: true
|
||||
}));
|
||||
} else {
|
||||
// Default to audio-only if the (execution) environment does not
|
||||
// support (sending and/or receiving) video.
|
||||
audioOnly = true;
|
||||
sendAnalytics(createStartAudioOnlyEvent(audioOnly));
|
||||
logger.log(`Start audio only set to ${audioOnly.toString()}`);
|
||||
dispatch(setAudioOnly(audioOnly));
|
||||
}
|
||||
|
||||
sendAnalytics(createStartAudioOnlyEvent(audioOnly));
|
||||
logger.log(`Start audio only set to ${audioOnly.toString()}`);
|
||||
|
||||
dispatch(setAudioOnly(audioOnly));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -186,10 +150,8 @@ function _syncTrackMutedState({ getState }, track) {
|
||||
// fired before track gets to state.
|
||||
if (track.muted !== muted) {
|
||||
sendAnalytics(createSyncTrackStateEvent(track.mediaType, muted));
|
||||
logger.log(
|
||||
`Sync ${track.mediaType} track muted state to ${
|
||||
muted ? 'muted' : 'unmuted'}`);
|
||||
|
||||
logger.log(`Sync ${track.mediaType} track muted state to ${
|
||||
muted ? 'muted' : 'unmuted'}`);
|
||||
track.muted = muted;
|
||||
setTrackMuted(track.jitsiTrack, muted);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference';
|
||||
import { ReducerRegistry } from '../redux';
|
||||
import { TRACK_REMOVED } from '../tracks';
|
||||
|
||||
import {
|
||||
SET_AUDIO_AVAILABLE,
|
||||
@@ -10,7 +8,6 @@ import {
|
||||
SET_CAMERA_FACING_MODE,
|
||||
SET_VIDEO_AVAILABLE,
|
||||
SET_VIDEO_MUTED,
|
||||
STORE_VIDEO_TRANSFORM,
|
||||
TOGGLE_CAMERA_FACING_MODE
|
||||
} from './actionTypes';
|
||||
import { CAMERA_FACING_MODE } from './constants';
|
||||
@@ -22,16 +19,12 @@ import { CAMERA_FACING_MODE } from './constants';
|
||||
* @property {boolean} muted=false - Audio muted state.
|
||||
*/
|
||||
|
||||
// FIXME Technically, _AUDIO_INITIAL_MEDIA_STATE is a constant internal to the
|
||||
// feature base/media and used in multiple files so it should be in
|
||||
// constants.js. Practically though, AudioMediaState would then be used in
|
||||
// multiple files as well so I don't know where and how to move it.
|
||||
/**
|
||||
* Initial state for local audio.
|
||||
*
|
||||
* @type {AudioMediaState}
|
||||
*/
|
||||
export const _AUDIO_INITIAL_MEDIA_STATE = {
|
||||
const AUDIO_INITIAL_MEDIA_STATE = {
|
||||
available: true,
|
||||
muted: false
|
||||
};
|
||||
@@ -45,7 +38,7 @@ export const _AUDIO_INITIAL_MEDIA_STATE = {
|
||||
* @private
|
||||
* @returns {AudioMediaState}
|
||||
*/
|
||||
function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
|
||||
function _audio(state = AUDIO_INITIAL_MEDIA_STATE, action) {
|
||||
switch (action.type) {
|
||||
case SET_AUDIO_AVAILABLE:
|
||||
return {
|
||||
@@ -72,25 +65,15 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
|
||||
* @property {boolean} muted=false - Video muted state.
|
||||
*/
|
||||
|
||||
// FIXME Technically, _VIDEO_INITIAL_MEDIA_STATE is a constant internal to the
|
||||
// feature base/media and used in multiple files so it should be in
|
||||
// constants.js. Practically though, VideoMediaState would then be used in
|
||||
// multiple files as well so I don't know where and how to move it.
|
||||
/**
|
||||
* Initial state for video.
|
||||
*
|
||||
* @type {VideoMediaState}
|
||||
*/
|
||||
export const _VIDEO_INITIAL_MEDIA_STATE = {
|
||||
const VIDEO_INITIAL_MEDIA_STATE = {
|
||||
available: true,
|
||||
facingMode: CAMERA_FACING_MODE.USER,
|
||||
muted: 0,
|
||||
|
||||
/**
|
||||
* The video {@link Transform}s applied to {@code MediaStream}s by
|
||||
* {@code id} i.e. "pinch to zoom".
|
||||
*/
|
||||
transforms: {}
|
||||
muted: 0
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -102,12 +85,8 @@ export const _VIDEO_INITIAL_MEDIA_STATE = {
|
||||
* @private
|
||||
* @returns {VideoMediaState}
|
||||
*/
|
||||
function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
function _video(state = VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_FAILED:
|
||||
case CONFERENCE_LEFT:
|
||||
return _clearAllVideoTransforms(state);
|
||||
|
||||
case SET_CAMERA_FACING_MODE:
|
||||
return {
|
||||
...state,
|
||||
@@ -126,9 +105,6 @@ function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
muted: action.muted
|
||||
};
|
||||
|
||||
case STORE_VIDEO_TRANSFORM:
|
||||
return _storeVideoTransform(state, action);
|
||||
|
||||
case TOGGLE_CAMERA_FACING_MODE: {
|
||||
let cameraFacingMode = state.facingMode;
|
||||
|
||||
@@ -143,9 +119,6 @@ function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
};
|
||||
}
|
||||
|
||||
case TRACK_REMOVED:
|
||||
return _trackRemoved(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -165,65 +138,3 @@ ReducerRegistry.register('features/base/media', combineReducers({
|
||||
audio: _audio,
|
||||
video: _video
|
||||
}));
|
||||
|
||||
/**
|
||||
* Removes all stored video {@link Transform}s.
|
||||
*
|
||||
* @param {Object} state - The {@code video} state of the feature base/media.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _clearAllVideoTransforms(state) {
|
||||
return {
|
||||
...state,
|
||||
transforms: _VIDEO_INITIAL_MEDIA_STATE.transforms
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the last applied transform to a stream.
|
||||
*
|
||||
* @param {Object} state - The {@code video} state of the feature base/media.
|
||||
* @param {Object} action - The redux action {@link STORE_VIDEO_TRANSFORM}.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _storeVideoTransform(state, { streamId, transform }) {
|
||||
return {
|
||||
...state,
|
||||
transforms: {
|
||||
...state.transforms,
|
||||
[streamId]: transform
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the stored video {@link Transform} associated with a
|
||||
* {@code MediaStream} when its respective track is removed.
|
||||
*
|
||||
* @param {Object} state - The {@code video} state of the feature base/media.
|
||||
* @param {Object} action - The redux action {@link TRACK_REMOVED}.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _trackRemoved(state, { track: { jitsiTrack } }) {
|
||||
if (jitsiTrack) {
|
||||
const streamId = jitsiTrack.getStreamId();
|
||||
|
||||
if (streamId && streamId in state.transforms) {
|
||||
const nextTransforms = {
|
||||
...state.transforms
|
||||
};
|
||||
|
||||
delete nextTransforms[streamId];
|
||||
|
||||
return {
|
||||
...state,
|
||||
transforms: nextTransforms
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -70,11 +70,6 @@ type Props = {
|
||||
*/
|
||||
avatarSize: number,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the {@code ParticipantView} is clicked/pressed.
|
||||
*/
|
||||
onPress: Function,
|
||||
|
||||
/**
|
||||
* The ID of the participant (to be) depicted by {@link ParticipantView}.
|
||||
*
|
||||
@@ -117,12 +112,7 @@ type Props = {
|
||||
* stacking space of all {@code Video}s. For more details, refer to the
|
||||
* {@code zOrder} property of the {@code Video} class for React Native.
|
||||
*/
|
||||
zOrder: number,
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: boolean
|
||||
zOrder: number
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,7 +122,6 @@ type Props = {
|
||||
* @extends Component
|
||||
*/
|
||||
class ParticipantView extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Renders the connection status label, if appropriate.
|
||||
*
|
||||
@@ -169,10 +158,8 @@ class ParticipantView extends Component<Props> {
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { containerStyle }>
|
||||
<Text style = { styles.connectionInfoText }>
|
||||
<View style = { containerStyle } >
|
||||
<Text style = { styles.connectionInfoText } >
|
||||
{ t(messageKey, { displayName }) }
|
||||
</Text>
|
||||
</View>
|
||||
@@ -187,7 +174,6 @@ class ParticipantView extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
onPress,
|
||||
_avatar: avatar,
|
||||
_connectionStatus: connectionStatus,
|
||||
_videoTrack: videoTrack
|
||||
@@ -202,26 +188,16 @@ class ParticipantView extends Component<Props> {
|
||||
// doesn't retain the last frame forever, so we would end up with a
|
||||
// black screen.
|
||||
const waitForVideoStarted = false;
|
||||
let renderVideo
|
||||
const renderVideo
|
||||
= !this.props._audioOnly
|
||||
&& (connectionStatus
|
||||
=== JitsiParticipantConnectionStatus.ACTIVE)
|
||||
&& shouldRenderVideoTrack(videoTrack, waitForVideoStarted);
|
||||
|
||||
// Is the avatar to be rendered?
|
||||
let renderAvatar = Boolean(!renderVideo && avatar);
|
||||
const renderAvatar = Boolean(!renderVideo && avatar);
|
||||
|
||||
// The consumer of this ParticipantView is allowed to forbid showing the
|
||||
// video if the private logic of this ParticipantView determines that
|
||||
// the video could be rendered.
|
||||
renderVideo = renderVideo && _toBoolean(this.props.showVideo, true);
|
||||
|
||||
// The consumer of this ParticipantView is allowed to forbid showing the
|
||||
// avatar if the private logic of this ParticipantView determines that
|
||||
// the avatar could be rendered.
|
||||
renderAvatar = renderAvatar && _toBoolean(this.props.showAvatar, true);
|
||||
|
||||
// If the connection has problems, we will "tint" the video / avatar.
|
||||
// If the connection has problems we will "tint" the video / avatar.
|
||||
const useTint
|
||||
= connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
|
||||
|| connectionStatus
|
||||
@@ -229,22 +205,30 @@ class ParticipantView extends Component<Props> {
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { renderVideo ? undefined : onPress }
|
||||
style = {{
|
||||
...styles.participantView,
|
||||
...this.props.style
|
||||
}}
|
||||
touchFeedback = { false }>
|
||||
}}>
|
||||
|
||||
{ renderVideo
|
||||
|
||||
// The consumer of this ParticipantView is allowed to forbid
|
||||
// showing the video if the private logic of this
|
||||
// ParticipantView determines that the video could be
|
||||
// rendered.
|
||||
&& _toBoolean(this.props.showVideo, true)
|
||||
&& <VideoTrack
|
||||
onPress = { renderVideo ? onPress : undefined }
|
||||
videoTrack = { videoTrack }
|
||||
waitForVideoStarted = { waitForVideoStarted }
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||
zOrder = { this.props.zOrder } /> }
|
||||
|
||||
{ renderAvatar
|
||||
|
||||
// The consumer of this ParticipantView is allowed to forbid
|
||||
// showing the avatar if the private logic of this
|
||||
// ParticipantView determines that the avatar could be
|
||||
// rendered.
|
||||
&& _toBoolean(this.props.showAvatar, true)
|
||||
&& <Avatar
|
||||
size = { this.props.avatarSize }
|
||||
uri = { avatar } /> }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// @flow
|
||||
/* @flow */
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
@@ -10,12 +10,8 @@ import {
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../sounds';
|
||||
|
||||
import { localParticipantIdChanged } from './actions';
|
||||
import {
|
||||
localParticipantIdChanged,
|
||||
participantUpdated
|
||||
} from './actions';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
KICK_PARTICIPANT,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
||||
@@ -31,10 +27,12 @@ import {
|
||||
import {
|
||||
getAvatarURLByParticipantId,
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount
|
||||
} from './functions';
|
||||
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
||||
import {
|
||||
PARTICIPANT_JOINED_SRC,
|
||||
PARTICIPANT_LEFT_SRC
|
||||
} from './sounds';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -49,7 +47,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
if (action.type === PARTICIPANT_JOINED
|
||||
|| action.type === PARTICIPANT_LEFT) {
|
||||
|| action.type === PARTICIPANT_LEFT) {
|
||||
_maybePlaySounds(store, action);
|
||||
}
|
||||
|
||||
@@ -68,27 +66,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
|
||||
break;
|
||||
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
// Ensure the raised hand state is cleared for the dominant speaker.
|
||||
const participant = getLocalParticipant(store.getState());
|
||||
|
||||
if (participant) {
|
||||
const local = participant.id === action.participant.id;
|
||||
|
||||
store.dispatch(participantUpdated({
|
||||
id: action.participant.id,
|
||||
local,
|
||||
raisedHand: false
|
||||
}));
|
||||
}
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
APP.UI.markDominantSpeaker(action.participant.id);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case KICK_PARTICIPANT:
|
||||
conference.kickParticipant(action.id);
|
||||
break;
|
||||
@@ -113,37 +90,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case PARTICIPANT_JOINED:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { participant } = action;
|
||||
const { id, local, raisedHand } = participant;
|
||||
if (typeof APP !== 'undefined') {
|
||||
const participant = action.participant;
|
||||
const { id, local } = participant;
|
||||
|
||||
// Send an external update of the local participant's raised hand state
|
||||
// if a new raised hand state is defined in the action.
|
||||
if (typeof raisedHand !== 'undefined') {
|
||||
if (local) {
|
||||
conference.setLocalParticipantProperty(
|
||||
'raisedHand',
|
||||
raisedHand);
|
||||
}
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
if (local) {
|
||||
APP.UI.onLocalRaiseHandChanged(raisedHand);
|
||||
APP.UI.setLocalRaisedHandStatus(raisedHand);
|
||||
} else {
|
||||
const remoteParticipant
|
||||
= getParticipantById(store.getState(), id);
|
||||
|
||||
remoteParticipant
|
||||
&& APP.UI.setRaisedHandStatus(
|
||||
remoteParticipant.id,
|
||||
remoteParticipant.name,
|
||||
raisedHand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const preUpdateAvatarURL
|
||||
= getAvatarURLByParticipantId(store.getState(), id);
|
||||
|
||||
@@ -211,9 +161,9 @@ function _maybePlaySounds({ getState, dispatch }, action) {
|
||||
*/
|
||||
function _registerSounds({ dispatch }) {
|
||||
dispatch(
|
||||
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
|
||||
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_SRC));
|
||||
dispatch(
|
||||
registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
|
||||
registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_SRC));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -225,7 +175,7 @@ function _registerSounds({ dispatch }) {
|
||||
*/
|
||||
function _unregisterSounds({ dispatch }) {
|
||||
dispatch(
|
||||
unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
|
||||
unregisterSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_SRC));
|
||||
dispatch(
|
||||
unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
|
||||
unregisterSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_SRC));
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* The name of the bundled sound file which will be played when new participant
|
||||
* joins the conference.
|
||||
*/
|
||||
export const PARTICIPANT_JOINED_FILE = 'joined.wav';
|
||||
|
||||
/**
|
||||
* The name of the bundled sound file which will be played when any participant
|
||||
* leaves the conference.
|
||||
*/
|
||||
export const PARTICIPANT_LEFT_FILE = 'left.wav';
|
||||
13
react/features/base/participants/sounds.native.js
Normal file
13
react/features/base/participants/sounds.native.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Points to the sound file which will be played when new participant joins
|
||||
* the conference.
|
||||
*/
|
||||
export const PARTICIPANT_JOINED_SRC
|
||||
= require('../../../../sounds/joined.wav');
|
||||
|
||||
/**
|
||||
* Points to the sound file which will be played when any participant leaves
|
||||
* the conference.
|
||||
*/
|
||||
export const PARTICIPANT_LEFT_SRC
|
||||
= require('../../../../sounds/left.wav');
|
||||
11
react/features/base/participants/sounds.web.js
Normal file
11
react/features/base/participants/sounds.web.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Points to the sound file which will be played when new participant joins
|
||||
* the conference.
|
||||
*/
|
||||
export const PARTICIPANT_JOINED_SRC = 'sounds/joined.wav';
|
||||
|
||||
/**
|
||||
* Points to the sound file which will be played when any participant leaves
|
||||
* the conference.
|
||||
*/
|
||||
export const PARTICIPANT_LEFT_SRC = 'sounds/left.wav';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user