Compare commits

...

46 Commits

Author SHA1 Message Date
Hristo Terezov
b2ddf55d44 feat(load-test): Initial implementation. 2019-11-28 13:20:49 +00:00
theunafraid
fb3a832a52 Add shortcut key for toggle tile view (#4882)
* Add shortcut key for toggle tile view

* Toggle tile view shortcut - undo main-enGB.json

* Add analytics

* Use already defined toolbar translations
2019-11-22 16:15:39 +00:00
Saúl Ibarra Corretgé
9c146c1245 subject: hide participant count for 1-1 calls
refs: https://github.com/jitsi/jitsi-meet/issues/4871
2019-11-22 10:49:24 +01:00
Saúl Ibarra Corretgé
792f506425 ios: drop support for iOS 10 2019-11-22 10:46:02 +01:00
Bettenbuk Zoltan
6121e9fc65 feat: improve chat UX 2019-11-21 18:11:58 +01:00
Bettenbuk Zoltan
955fa1f49f fix: undefined is not an object on bitrate 2019-11-21 18:11:58 +01:00
damencho
2544d0a084 Fixes the message for who kicked you. 2019-11-20 17:01:00 +02:00
Bettenbuk Zoltan
8f0a12016a fix: return room lock conference, when there is no other 2019-11-20 13:28:47 +01:00
Leonard Kim
34ccd3524f fix(chat): preserve intentional linebreaks in message display 2019-11-20 08:58:02 +01:00
Leonard Kim
563e99ecd3 fix(chat): wrap long text 2019-11-18 09:31:47 -08:00
Leonard Kim
70f14be50f fix(large-video): center dominant speaker avatar using css
The vertical alignment was being set with javascript.
Recent changes might make the setting of alignment exit
early due to height 0 video. As position can be set
declaratively with css, use css to set position.
2019-11-15 07:51:59 -08:00
Bettenbuk Zoltan
8bd0da886e feat: safe decodeURIComponent 2019-11-15 15:18:20 +01:00
damencho
1fd326f980 Fixes nginx match rule, containing wrong chars.
Also adds a missing '/'.
2019-11-15 14:10:55 +00:00
yanas
d9cc664ea6 Merge pull request #4865 from jitsi/position-status-message
fix(remote-status-message): position
2019-11-15 14:10:34 +00:00
Hristo Terezov
d65e241056 fix(remote-status-message): position 2019-11-15 12:33:01 +00:00
Saúl Ibarra Corretgé
fe2b1f3d9f rn: refactor aspect ratio and reduced UI detectors 2019-11-15 12:54:44 +01:00
virtuacoplenny
17c1f50fc3 fix(mobile-landing): do not attempt opening download link in new window
Instead let the mobile OS take care of opening the URL
in the appropriate application. Without target _blank,
iOS 13.2.2 on Chrome will open about:blank and immediately
close the tab instead of opening the store.
2019-11-15 09:43:18 +01:00
Saúl Ibarra Corretgé
5c1c022291 doc: add open beta links to README 2019-11-15 09:30:42 +01:00
Boris Grozev
72435dee56 Order fields alphabetically. 2019-11-14 17:49:06 -06:00
Boris Grozev
42f2eff02a Whitelists the "stereo" flag. 2019-11-14 17:49:06 -06:00
Saúl Ibarra Corretgé
0b68bef0be ios: set Fastlane test groups 2019-11-14 18:21:37 +01:00
Saúl Ibarra Corretgé
676e943d81 ios: fix typo in Fastlane file 2019-11-14 16:02:39 +01:00
Saúl Ibarra Corretgé
2b4307dee9 ios: fix Fastlane beta build submissions 2019-11-14 15:49:09 +01:00
Hristo Terezov
f3f936c196 fix(large-video): missing video. 2019-11-14 06:29:27 -08:00
Saúl Ibarra Corretgé
eb900ddbe1 android: fix track name in Fastlane 2019-11-14 15:27:32 +01:00
Saúl Ibarra Corretgé
c2c323347a rn: skip logging potentially sensitive data 2019-11-14 15:01:29 +01:00
Saúl Ibarra Corretgé
af6642b91b rn: allow for userInfo and token to be set from the SDK 2019-11-14 12:30:15 +01:00
drimovecz
ffded8d82a Drimovecz/speakerstats (#4851)
* Correctly process speaker stats events when the conference contains a subdomain
2019-11-13 15:37:09 +00:00
Saúl Ibarra Corretgé
00b57c7983 fix(transport): remove legacy code
It has been around bor > 2.5 years already.
2019-11-13 16:15:29 +01:00
Saúl Ibarra Corretgé
5d40a8992a ios: disable bitcode when building the SDK for a release
This makes it possible to compile the SDK with Xcode 10 and 11. The problem is
that the Google SDK (used for sign-in) is compiled with Xcode 11. This avoids
the issue.
2019-11-13 13:17:51 +01:00
Saúl Ibarra Corretgé
e543625295 rn,settings: set the placeholder text color 2019-11-13 10:38:05 +01:00
Saúl Ibarra Corretgé
0b25ff649e ios: fix not displaying TextInput values in SettingsView 2019-11-13 10:38:05 +01:00
Saúl Ibarra Corretgé
63344ac62d deps: react-native-webrtc@1.75.2
Fixes an Android crash on craptacular devices.
2019-11-13 08:31:05 +01:00
Saúl Ibarra Corretgé
2e60aafebf fastlane,ios: add ability to set the changelog 2019-11-12 18:14:02 +01:00
Saúl Ibarra Corretgé
131e8f4aea fastlane: prepare for open beta access 2019-11-12 16:06:15 +01:00
Bettenbuk Zoltan
53f01a39c9 feat: private message interface config flag 2019-11-12 15:48:53 +01:00
Дамян Минков
50f4796144 Adds an option to set email through iframe API init and to stats. (#4842)
* Adds an option to set email through iframe API init and to stats.

* Simplifies configuring email and displayName in stats.

Removes enableStatsID as not needed as when off we are sending as callstats id xmpp resource which is unique per call and id must be something that sticks between calls (callstatsUsername).

* Adds email and displayName in stats config for mobile.

* chore(deps): Updates lib-jitsi-meet to latest dd31f0a.

* Removes enableStatsID from config and whitelist.
2019-11-12 13:37:54 +00:00
Дамян Минков
5bdfae377f Adds a hook to insert body & head html. (#4843)
* Adds a hook to insert body html.

* Adds a hook to insert head html.
2019-11-12 13:37:48 +00:00
Saúl Ibarra Corretgé
44970648ea rn: now working on versions 19.5 / 2.5 2019-11-08 15:21:55 +01:00
Saúl Ibarra Corretgé
3cd7f0b77d settings: fix loading disableCallIntegration 2019-11-08 12:15:49 +01:00
Saúl Ibarra Corretgé
4d243f9b92 android: fix selecting the Bluetooth route
Samsung devices (of course) seem to stick with the earpiece if we first select
Bluetooth but then set speaker to false. Reverse the order to make everyone
happy.

This only applies to the generic and legacy handlers.
2019-11-08 12:15:49 +01:00
Saúl Ibarra Corretgé
6b716f8f56 android: fix initializing audio device handler modules too early
When ConnectionService is used (the default) we were attaching the handlers too
early, and since attaching them requires that the RNConnectionService module is
loaded, it silently failed. Instead, use the initialize() method, which gets
called after all the Catalyst (aka native) modules have been loaded.
2019-11-08 12:15:49 +01:00
Saúl Ibarra Corretgé
5b99219f29 android: log a warning if listeners could not be attached 2019-11-08 12:15:49 +01:00
Saúl Ibarra Corretgé
f0dcb51915 android: make code a bit more readable 2019-11-08 12:15:49 +01:00
Bettenbuk Zoltan
3ff658a13b fix: respect safe area in conference on ios 2019-11-07 12:26:54 +01:00
Bettenbuk Zoltan
3a46513d4b ref: remove unused code 2019-11-07 12:26:54 +01:00
82 changed files with 8075 additions and 593 deletions

View File

@@ -27,10 +27,18 @@ You can download Debian/Ubuntu binaries:
You can download source archives (produced by ```make source-package```):
* [source builds](https://download.jitsi.org/jitsi-meet/src/)
### Mobile apps
You can get our mobile versions from here:
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
You can also sign up for our open beta testing here:
* [Android](https://play.google.com/apps/testing/org.jitsi.meet)
* [iOS](https://testflight.apple.com/join/isy6ja7S)
## Development
For web development see [here](doc/development.md), and for mobile see [here](doc/mobile.md).

View File

@@ -24,7 +24,7 @@ platform :android do
# Upload built artifact to the Closed Beta track
upload_to_play_store(
track: "Closed Beta",
track: "beta",
json_key: ENV["JITSI_JSON_KEY_FILE"],
skip_upload_metadata: true,
skip_upload_images: true,

View File

@@ -20,5 +20,5 @@
android.useAndroidX=true
android.enableJetifier=true
appVersion=19.4.0
sdkVersion=2.4.0
appVersion=19.5.0
sdkVersion=2.5.0

View File

@@ -18,6 +18,7 @@ package org.jitsi.meet.sdk;
import android.content.Context;
import android.os.Build;
import android.telecom.CallAudioState;
import androidx.annotation.RequiresApi;
import java.util.HashSet;
@@ -52,20 +53,20 @@ class AudioDeviceHandlerConnectionService implements
*/
private static int audioDeviceToRouteInt(String audioDevice) {
if (audioDevice == null) {
return android.telecom.CallAudioState.ROUTE_EARPIECE;
return CallAudioState.ROUTE_EARPIECE;
}
switch (audioDevice) {
case AudioModeModule.DEVICE_BLUETOOTH:
return android.telecom.CallAudioState.ROUTE_BLUETOOTH;
return CallAudioState.ROUTE_BLUETOOTH;
case AudioModeModule.DEVICE_EARPIECE:
return android.telecom.CallAudioState.ROUTE_EARPIECE;
return CallAudioState.ROUTE_EARPIECE;
case AudioModeModule.DEVICE_HEADPHONES:
return android.telecom.CallAudioState.ROUTE_WIRED_HEADSET;
return CallAudioState.ROUTE_WIRED_HEADSET;
case AudioModeModule.DEVICE_SPEAKER:
return android.telecom.CallAudioState.ROUTE_SPEAKER;
return CallAudioState.ROUTE_SPEAKER;
default:
JitsiMeetLogger.e(TAG + " Unsupported device name: " + audioDevice);
return android.telecom.CallAudioState.ROUTE_EARPIECE;
return CallAudioState.ROUTE_EARPIECE;
}
}
@@ -78,20 +79,16 @@ class AudioDeviceHandlerConnectionService implements
*/
private static Set<String> routesToDeviceNames(int supportedRouteMask) {
Set<String> devices = new HashSet<>();
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_EARPIECE)
== android.telecom.CallAudioState.ROUTE_EARPIECE) {
if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
devices.add(AudioModeModule.DEVICE_EARPIECE);
}
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_BLUETOOTH)
== android.telecom.CallAudioState.ROUTE_BLUETOOTH) {
if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH) == CallAudioState.ROUTE_BLUETOOTH) {
devices.add(AudioModeModule.DEVICE_BLUETOOTH);
}
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_SPEAKER)
== android.telecom.CallAudioState.ROUTE_SPEAKER) {
if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
devices.add(AudioModeModule.DEVICE_SPEAKER);
}
if ((supportedRouteMask & android.telecom.CallAudioState.ROUTE_WIRED_HEADSET)
== android.telecom.CallAudioState.ROUTE_WIRED_HEADSET) {
if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET) == CallAudioState.ROUTE_WIRED_HEADSET) {
devices.add(AudioModeModule.DEVICE_HEADPHONES);
}
return devices;
@@ -109,13 +106,13 @@ class AudioDeviceHandlerConnectionService implements
}
@Override
public void onCallAudioStateChange(final android.telecom.CallAudioState callAudioState) {
public void onCallAudioStateChange(final CallAudioState state) {
module.runInAudioThread(new Runnable() {
@Override
public void run() {
boolean audioRouteChanged
= audioDeviceToRouteInt(module.getSelectedDevice()) != callAudioState.getRoute();
int newSupportedRoutes = callAudioState.getSupportedRouteMask();
= audioDeviceToRouteInt(module.getSelectedDevice()) != state.getRoute();
int newSupportedRoutes = state.getSupportedRouteMask();
boolean audioDevicesChanged = supportedRouteMask != newSupportedRoutes;
if (audioDevicesChanged) {
supportedRouteMask = newSupportedRoutes;
@@ -140,6 +137,8 @@ class AudioDeviceHandlerConnectionService implements
RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
if (rcs != null) {
rcs.setCallAudioStateListener(this);
} else {
JitsiMeetLogger.w(TAG + " Couldn't set call audio state listener, module is null");
}
}
@@ -148,6 +147,8 @@ class AudioDeviceHandlerConnectionService implements
RNConnectionService rcs = ReactInstanceManagerHolder.getNativeModule(RNConnectionService.class);
if (rcs != null) {
rcs.setCallAudioStateListener(null);
} else {
JitsiMeetLogger.w(TAG + " Couldn't set call audio state listener, module is null");
}
}

View File

@@ -198,11 +198,11 @@ class AudioDeviceHandlerGeneric implements
@Override
public void setAudioRoute(String device) {
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
// Turn speaker on / off
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
}
@Override

View File

@@ -196,11 +196,11 @@ class AudioDeviceHandlerLegacy implements
@Override
public void setAudioRoute(String device) {
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
// Turn speaker on / off
audioManager.setSpeakerphoneOn(device.equals(AudioModeModule.DEVICE_SPEAKER));
// Turn bluetooth on / off
setBluetoothAudioRoute(device.equals(AudioModeModule.DEVICE_BLUETOOTH));
}
@Override

View File

@@ -136,8 +136,6 @@ class AudioModeModule extends ReactContextBaseJavaModule {
*/
public AudioModeModule(ReactApplicationContext reactContext) {
super(reactContext);
setAudioDeviceHandler();
}
/**
@@ -193,6 +191,16 @@ class AudioModeModule extends ReactContextBaseJavaModule {
return NAME;
}
/**
* Initializes the audio device handler module. This function is called *after* all Catalyst
* modules have been created, and that's why we use it, because {@link AudioDeviceHandlerConnectionService}
* needs access to another Catalyst module, so doing this in the constructor would be too early.
*/
@Override
public void initialize() {
setAudioDeviceHandler();
}
private void setAudioDeviceHandler() {
if (audioDeviceHandler != null) {
audioDeviceHandler.stop();

View File

@@ -349,7 +349,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
urlProps.putString("jwt", token);
}
if (token == null && userInfo != null) {
if (userInfo != null) {
props.putBundle("userInfo", userInfo.asBundle());
}

0
body.html Normal file
View File

View File

@@ -1205,12 +1205,16 @@ export default {
_getConferenceOptions() {
const options = config;
const { email, name: nick } = getLocalParticipant(APP.store.getState());
const nick = APP.store.getState()['features/base/settings'].displayName;
const { locationURL } = APP.store.getState()['features/base/connection'];
if (nick) {
options.displayName = nick;
if (options.enableDisplayNameInStats && nick) {
options.statisticsDisplayName = nick;
}
if (options.enableEmailInStats && email) {
options.statisticsId = email;
}
options.applicationName = interfaceConfig.APP_NAME;

View File

@@ -292,13 +292,11 @@ var config = {
// callStatsID: '',
// callStatsSecret: '',
// enables callstatsUsername to be reported as statsId and used
// by callstats as repoted remote id
// enableStatsID: false
// enables sending participants display name to callstats
// enableDisplayNameInStats: false
// enables sending participants email if available to callstats and other analytics
// enableEmailInStats: false
// Privacy
//

View File

@@ -205,6 +205,10 @@
border-radius: 6px 0px 6px 6px;
}
.usermessage {
white-space: pre-wrap;
}
&.error {
border-radius: 0px;
@@ -226,6 +230,8 @@
.messagecontent {
margin: 5px 10px;
max-width: 100%;
overflow: hidden;
}
}

View File

@@ -489,6 +489,8 @@
height: 300px;
margin: auto;
position: relative;
top: 50%;
transform: translateY(-50%);
}
#mixedstream {

View File

@@ -30,6 +30,7 @@ Its constructor gets a number of options:
* **onload**: (optional) handler for the iframe onload event.
* **invitees**: (optional) Array of objects containing information about new participants that will be invited in the call.
* **devices**: (optional) A map containing information about the initial devices that will be used in the call.
* **userInfo**: (optional) JS object containing information about the participant opening the meeting, such as `email`.
Example:
@@ -84,6 +85,19 @@ const options = {
const api = new JitsiMeetExternalAPI(domain, options);
```
You can set the userInfo(email) for the call:
```javascript
var domain = "meet.jit.si";
var options = {
...
userInfo: {
email: 'email@jitsiexamplemail.com'
}
}
var api = new JitsiMeetExternalAPI(domain, options);
```
### Controlling the embedded Jitsi Meet Conference
Device management `JitsiMeetExternalAPI` methods:

View File

@@ -45,7 +45,7 @@ server {
proxy_set_header Host $http_host;
}
location ~ ^/([^?&:’“]+)$ {
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}

0
head.html Normal file
View File

View File

@@ -1,5 +1,6 @@
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
<head>
<!--#include virtual="head.html" -->
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -151,6 +152,7 @@
<!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
</head>
<body>
<!--#include virtual="body.html" -->
<div id="react"></div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
platform :ios, '10.0'
platform :ios, '11.0'
workspace 'jitsi-meet'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
@@ -82,7 +82,7 @@ post_install do |installer|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['SUPPORTS_MACCATALYST'] = 'NO'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '10.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end

View File

@@ -274,7 +274,7 @@ PODS:
- React
- react-native-netinfo (4.1.5):
- React
- react-native-webrtc (1.75.0):
- react-native-webrtc (1.75.2):
- React
- react-native-webview (7.4.1):
- React
@@ -534,7 +534,7 @@ SPEC CHECKSUMS:
react-native-calendar-events: 2fe35a9294af05de0ed819d3a1b5dac048d2c010
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-webrtc: c5e3d631179a933548a8e49bddbd8fad02586095
react-native-webrtc: f6783727706d8bec5fb302b76eda60c33dfe3191
react-native-webview: 4dbc1d2a4a6b9c5e9e723c62651917aa2b5e579e
React-RCTActionSheet: 94671eef55b01a93be735605822ef712d5ea208e
React-RCTAnimation: 524ae33e73de9c0fe6501a7a4bda8e01d26499d9
@@ -553,6 +553,6 @@ SPEC CHECKSUMS:
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
Yoga: 02036f6383c0008edb7ef0773a0e6beb6ce82bd1
PODFILE CHECKSUM: 63c90b1d33cd96709fb72bad6be440ae9c3deecb
PODFILE CHECKSUM: 0fdfa45ae809c9460c80be3e0d4bbb822fccc418
COCOAPODS: 1.8.1

View File

@@ -747,7 +747,6 @@
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
);
INFOPLIST_FILE = src/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -758,7 +757,6 @@
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet;
PRODUCT_NAME = "jitsi-meet";
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -783,7 +781,6 @@
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
);
INFOPLIST_FILE = src/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = "$(inherited)";
OTHER_LDFLAGS = (
@@ -794,7 +791,6 @@
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.meet;
PRODUCT_NAME = "jitsi-meet";
PROVISIONING_PROFILE_SPECIFIER = "";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
@@ -850,10 +846,11 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
);
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -902,10 +899,11 @@
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
);
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;

View File

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

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>19.4.0</string>
<string>19.5.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>19.4.0</string>
<string>19.5.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -61,7 +61,21 @@ platform :ios do
)
# Upload the build to TestFlight (but don't distribute it)
upload_to_testflight(skip_submission: true, skip_waiting_for_build_processing: true)
upload_to_testflight(
beta_app_description: ENV["JITSI_CHANGELOG"],
beta_app_feedback_email: ENV["JITSI_REVIEW_EMAIL"],
beta_app_review_info: {
contact_email: ENV["JITSI_REVIEW_EMAIL"],
demo_account_name: ENV["JITSI_DEMO_ACCOUNT"],
demo_account_password: ENV["JITSI_DEMO_PASSWORD"],
},
changelog: ENV["JITSI_CHANGELOG"],
demo_account_required: false,
distribute_external: true,
groups: ENV["JITSI_BETA_TESTING_GROUPS"],
reject_build_waiting_for_review: true,
uses_non_exempt_encryption: false
)
# Cleanup
clean_build_artifacts

View File

@@ -25,7 +25,7 @@ popd
# Build the SDK
pushd ${PROJECT_REPO}
rm -rf ios/sdk/JitsiMeet.framework
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release archive
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release ENABLE_BITCODE=NO clean archive
if [[ $DO_GIT_TAG == 1 ]]; then
git tag ios-sdk-${SDK_VERSION}
fi

View File

@@ -555,7 +555,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 = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -611,7 +611,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 = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -639,7 +639,6 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -650,7 +649,6 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -671,7 +669,6 @@
ENABLE_BITCODE = YES;
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeetSDK.ios;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -681,7 +678,6 @@
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.4.0</string>
<string>2.5.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -225,7 +225,7 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
urlProps[@"jwt"] = _token;
}
if (_token == nil && _userInfo != nil) {
if (_userInfo != nil) {
props[@"userInfo"] = [self.userInfo asDict];
}

View File

@@ -47,8 +47,10 @@
},
"chat": {
"error": "Error: your message was not sent. Reason: {{error}}",
"fieldPlaceHolder": "Type your message here",
"messagebox": "Type a message",
"messageTo": "Private message to {{recipient}}",
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
"nickname": {
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat"

View File

@@ -232,6 +232,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* information about new participants that will be invited in the call.
* @param {Array<Object>} [options.devices] - Array of objects containing
* information about the initial devices that will be used in the call.
* @param {Object} [options.userInfo] - Object containing information about
* the participant opening the meeting.
*/
constructor(domain, ...args) {
super();
@@ -246,7 +248,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
jwt = undefined,
onload = undefined,
invitees,
devices
devices,
userInfo
} = parseArguments(args);
this._parentNode = parentNode;
@@ -256,7 +259,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
jwt,
noSSL,
roomName,
devices
devices,
userInfo
});
this._createIFrame(height, width, onload);
this._transport = new Transport({

View File

@@ -683,8 +683,8 @@ export default class LargeVideoManager {
}
/**
* Dispatches an action to update the known resolution state of the
* large video and adjusts container sizes when the resolution changes.
* Dispatches an action to update the known resolution state of the large video and adjusts container sizes when the
* resolution changes.
*
* @private
* @returns {void}
@@ -697,7 +697,7 @@ export default class LargeVideoManager {
APP.store.dispatch(updateKnownLargeVideoResolution(height));
}
const currentAspectRatio = width / height;
const currentAspectRatio = height === 0 ? 0 : width / height;
if (this._videoAspectRatio !== currentAspectRatio) {
this._videoAspectRatio = currentAspectRatio;

View File

@@ -5,10 +5,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { browser } from '../../../react/features/base/lib-jitsi-meet';
import {
ORIENTATION,
LargeVideoBackground
} from '../../../react/features/large-video';
import { ORIENTATION, LargeVideoBackground } from '../../../react/features/large-video';
import { LAYOUTS, getCurrentLayout } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import Filmstrip from './Filmstrip';
@@ -55,8 +53,12 @@ function computeDesktopVideoSize( // eslint-disable-line max-params
videoHeight,
videoSpaceWidth,
videoSpaceHeight) {
const aspectRatio = videoWidth / videoHeight;
if (videoWidth === 0 || videoHeight === 0 || videoSpaceWidth === 0 || videoSpaceHeight === 0) {
// Avoid NaN values caused by devision by 0.
return [ 0, 0 ];
}
const aspectRatio = videoWidth / videoHeight;
let availableWidth = Math.max(videoWidth, videoSpaceWidth);
let availableHeight = Math.max(videoHeight, videoSpaceHeight);
@@ -99,6 +101,11 @@ function computeCameraVideoSize( // eslint-disable-line max-params
videoSpaceWidth,
videoSpaceHeight,
videoLayoutFit) {
if (videoWidth === 0 || videoHeight === 0 || videoSpaceWidth === 0 || videoSpaceHeight === 0) {
// Avoid NaN values caused by devision by 0.
return [ 0, 0 ];
}
const aspectRatio = videoWidth / videoHeight;
switch (videoLayoutFit) {
@@ -322,7 +329,7 @@ export class VideoContainer extends LargeContainer {
* @param {number} containerHeight container height
* @returns {{availableWidth, availableHeight}}
*/
getVideoSize(containerWidth, containerHeight) {
_getVideoSize(containerWidth, containerHeight) {
const { width, height } = this.getStreamSize();
if (this.stream && this.isScreenSharing()) {
@@ -414,13 +421,29 @@ export class VideoContainer extends LargeContainer {
if (this.$video.length === 0) {
return;
}
const currentLayout = getCurrentLayout(APP.store.getState());
const [ width, height ]
= this.getVideoSize(containerWidth, containerHeight);
if (currentLayout === LAYOUTS.TILE_VIEW) {
// We don't need to resize the large video since it won't be displayed and we'll resize when returning back
// to stage view.
return;
}
this.positionRemoteStatusMessages();
const [ width, height ] = this._getVideoSize(containerWidth, containerHeight);
if (width === 0 || height === 0) {
// We don't need to set 0 for width or height since the visibility is controled by the visibility css prop
// on the largeVideoElementsContainer. Also if the width/height of the video element is 0 the attached
// stream won't be played. Normally if we attach a new stream we won't resize the video element until the
// stream has been played. But setting width/height to 0 will prevent the video from playing.
return;
}
if ((containerWidth > width) || (containerHeight > height)) {
this._backgroundOrientation = containerWidth > width
? ORIENTATION.LANDSCAPE : ORIENTATION.PORTRAIT;
this._backgroundOrientation = containerWidth > width ? ORIENTATION.LANDSCAPE : ORIENTATION.PORTRAIT;
this._hideBackground = false;
} else {
this._hideBackground = true;
@@ -429,15 +452,7 @@ export class VideoContainer extends LargeContainer {
this._updateBackground();
const { horizontalIndent, verticalIndent }
= this.getVideoPosition(width, height,
containerWidth, containerHeight);
// update avatar position
const top = (containerHeight / 2) - (this.avatarHeight / 4 * 3);
this.$avatar.css('top', top);
this.positionRemoteStatusMessages();
= this.getVideoPosition(width, height, containerWidth, containerHeight);
this.$wrapper.animate({
width,

View File

@@ -22,7 +22,6 @@ import SharedVideoThumb from '../shared_video/SharedVideoThumb';
import Filmstrip from './Filmstrip';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
import RemoteVideo from './RemoteVideo';
import LargeVideoManager from './LargeVideoManager';
@@ -663,14 +662,6 @@ const VideoLayout = {
largeVideo.updateContainerSize();
largeVideo.resize(animate);
}
// Calculate available width and height.
const availableHeight = window.innerHeight;
const availableWidth = UIUtil.getAvailableVideoWidth();
if (availableWidth < 0 || availableHeight < 0) {
return;
}
},
getSmallVideo(id) {

View File

@@ -9,43 +9,6 @@ const DEFAULT_POSTIS_OPTIONS = {
window: window.opener || window.parent
};
/**
* The list of methods of incoming postis messages that we have to support for
* backward compatibility for the users that are directly sending messages to
* Jitsi Meet (without using external_api.js)
*
* @type {string[]}
*/
const LEGACY_INCOMING_METHODS = [
'avatar-url',
'display-name',
'email',
'toggle-audio',
'toggle-chat',
'toggle-film-strip',
'toggle-share-screen',
'toggle-video',
'video-hangup'
];
/**
* The list of methods of outgoing postis messages that we have to support for
* backward compatibility for the users that are directly listening to the
* postis messages send by Jitsi Meet(without using external_api.js).
*
* @type {string[]}
*/
const LEGACY_OUTGOING_METHODS = [
'display-name-change',
'incoming-message',
'outgoing-message',
'participant-joined',
'participant-left',
'video-conference-joined',
'video-conference-left',
'video-ready-to-close'
];
/**
* The postis method used for all messages.
*
@@ -63,34 +26,13 @@ export default class PostMessageTransportBackend {
* @param {Object} options - Optional parameters for configuration of the
* transport.
*/
constructor({ enableLegacyFormat, postisOptions } = {}) {
constructor({ postisOptions } = {}) {
// eslint-disable-next-line new-cap
this.postis = Postis({
...DEFAULT_POSTIS_OPTIONS,
...postisOptions
});
/**
* If true PostMessageTransportBackend will process and send messages
* using the legacy format and in the same time the current format.
* Otherwise all messages (outgoing and incoming) that are using the
* legacy format will be ignored.
*
* @type {boolean}
*/
this._enableLegacyFormat = enableLegacyFormat;
if (this._enableLegacyFormat) {
// backward compatibility
LEGACY_INCOMING_METHODS.forEach(method =>
this.postis.listen(
method,
params =>
this._legacyMessageReceivedCallback(method, params)
)
);
}
this._receiveCallback = () => {
// Do nothing until a callback is set by the consumer of
// PostMessageTransportBackend via setReceiveCallback.
@@ -101,37 +43,6 @@ export default class PostMessageTransportBackend {
message => this._receiveCallback(message));
}
/**
* Handles incoming legacy postis messages.
*
* @param {string} method - The method property from the postis message.
* @param {Any} params - The params property from the postis message.
* @returns {void}
*/
_legacyMessageReceivedCallback(method, params = {}) {
this._receiveCallback({
data: {
name: method,
data: params
}
});
}
/**
* Sends the passed message via postis using the old format.
*
* @param {Object} legacyMessage - The message to be sent.
* @returns {void}
*/
_sendLegacyMessage({ name, ...data }) {
if (name && LEGACY_OUTGOING_METHODS.indexOf(name) !== -1) {
this.postis.send({
method: name,
params: data
});
}
}
/**
* Disposes the allocated resources.
*
@@ -152,14 +63,6 @@ export default class PostMessageTransportBackend {
method: POSTIS_METHOD_NAME,
params: message
});
if (this._enableLegacyFormat) {
// For the legacy use case we don't need any new fields defined in
// Transport class. That's why we are passing only the original
// object passed by the consumer of the Transport class which is
// message.data.
this._sendLegacyMessage(message.data || {});
}
}
/**

View File

@@ -36,12 +36,7 @@ let transport;
*/
export function getJitsiMeetTransport() {
if (!transport) {
transport = new Transport({
backend: new PostMessageTransportBackend({
enableLegacyFormat: true,
postisOptions
})
});
transport = new Transport({ backend: new PostMessageTransportBackend({ postisOptions }) });
}
return transport;

9
package-lock.json generated
View File

@@ -10931,8 +10931,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#1de69abe22aa632c9a4255ee9f6ae48dab9be756",
"from": "github:jitsi/lib-jitsi-meet#1de69abe22aa632c9a4255ee9f6ae48dab9be756",
"version": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"from": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.2",
@@ -14887,8 +14887,9 @@
"integrity": "sha512-l3Quzbb+qa4in2U5RSt/lT0/pHrIpEChT1NnqrVAAXNrjkXjVOsxduaaEDdDhTzNJQEm/PcAcoyrFmgvGOohxw=="
},
"react-native-webrtc": {
"version": "github:react-native-webrtc/react-native-webrtc#a12a6cdfdefe53d03b388394e4cf10966bd99fca",
"from": "github:react-native-webrtc/react-native-webrtc#a12a6cdfdefe53d03b388394e4cf10966bd99fca",
"version": "1.75.2",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.75.2.tgz",
"integrity": "sha512-mdEukmHNhiyVIiwdooxk4kVXWG83OOENFV9YIkC7dtGU/sOdL81vDzynqd6Af9YbGMeOr0xdpFuEGsc1OFnKZg==",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",

View File

@@ -57,7 +57,7 @@
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#1de69abe22aa632c9a4255ee9f6ae48dab9be756",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#dd31f0aff0a38b3cfd8e808e457a2e3a0f966514",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -80,7 +80,7 @@
"react-native-svg-transformer": "0.13.0",
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#a12a6cdfdefe53d03b388394e4cf10966bd99fca",
"react-native-webrtc": "1.75.2",
"react-native-webview": "7.4.1",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",

View File

@@ -9,10 +9,7 @@ import { DialogContainer } from '../../base/dialog';
import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
import '../../base/jwt';
import { Platform } from '../../base/react';
import {
AspectRatioDetector,
ReducedUIDetector
} from '../../base/responsive-ui';
import '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import '../../google-api';
import '../../mobile/audio-mode';
@@ -110,22 +107,6 @@ export class App extends AbstractApp {
});
}
/**
* Injects {@link AspectRatioDetector} in order to detect the aspect ratio
* of this {@code App}'s user interface and afford {@link AspectRatioAware}.
*
* @override
*/
_createMainElement(component, props) {
return (
<AspectRatioDetector>
<ReducedUIDetector>
{ super._createMainElement(component, props) }
</ReducedUIDetector>
</AspectRatioDetector>
);
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in

View File

@@ -5,7 +5,6 @@ import React from 'react';
import { DialogContainer } from '../../base/dialog';
import '../../base/user-interaction';
import '../../base/responsive-ui';
import '../../chat';
import '../../external-api';
import '../../power-monitor';

View File

@@ -14,6 +14,7 @@ import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { setAudioMuted, setVideoMuted } from '../media';
import {
dominantSpeakerChanged,
getLocalParticipant,
getNormalizedDisplayName,
participantConnectionStatusChanged,
participantKicked,
@@ -393,14 +394,18 @@ export function createConference() {
throw new Error('Cannot join a conference without a room name!');
}
const config = state['features/base/config'];
const { email, name: nick } = getLocalParticipant(state);
const conference
= connection.initJitsiConference(
getBackendSafeRoomName(room), {
...state['features/base/config'],
...config,
applicationName: getName(),
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,
confID: `${locationURL.host}${locationURL.pathname}`
confID: `${locationURL.host}${locationURL.pathname}`,
statisticsDisplayName: config.enableDisplayNameInStats ? nick : undefined,
statisticsId: config.enableEmailInStats ? email : undefined
});
connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference;

View File

@@ -11,6 +11,7 @@ import {
participantLeft
} from '../participants';
import { toState } from '../redux';
import { safeDecodeURIComponent } from '../util';
import {
AVATAR_ID_COMMAND,
@@ -163,7 +164,7 @@ export function getConferenceName(stateful: Function | Object): string {
|| subject
|| callDisplayName
|| (callee && callee.name)
|| _.startCase(decodeURIComponent(room));
|| _.startCase(safeDecodeURIComponent(room));
}
/**
@@ -177,13 +178,15 @@ export function getConferenceName(stateful: Function | Object): string {
* @returns {JitsiConference|undefined}
*/
export function getCurrentConference(stateful: Function | Object) {
const { conference, joining, leaving }
const { conference, joining, leaving, passwordRequired }
= toState(stateful)['features/base/conference'];
return (
conference
? conference === leaving ? undefined : conference
: joining);
// There is a precendence
if (conference) {
return conference === leaving ? undefined : conference;
}
return joining || passwordRequired;
}
/**

View File

@@ -15,10 +15,6 @@ export default [
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
'callFlowsEnabled',
'callStatsConfIDNamespace',
'callStatsID',
'callStatsSecret',
/**
* The display name of the CallKit call representing the conference/meeting
@@ -34,6 +30,7 @@ export default [
* @type string
*/
'callDisplayName',
'callFlowsEnabled',
/**
* The handle
@@ -48,6 +45,9 @@ export default [
* @type string
*/
'callHandle',
'callStatsConfIDNamespace',
'callStatsID',
'callStatsSecret',
/**
* The UUID of the CallKit call representing the conference/meeting
@@ -73,8 +73,8 @@ export default [
'desktopSharingChromeExtId',
'desktopSharingChromeMinExtVersion',
'desktopSharingChromeSources',
'desktopSharingFrameRate',
'desktopSharingFirefoxDisabled',
'desktopSharingFrameRate',
'desktopSharingSources',
'disable1On1Mode',
'disableAEC',
@@ -84,6 +84,7 @@ export default [
'disableDeepLinking',
'disableH264',
'disableHPF',
'disableLocalVideoFlip',
'disableNS',
'disableRemoteControl',
'disableRtx',
@@ -91,11 +92,10 @@ export default [
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableEmailInStats',
'enableLayerSuspension',
'enableLipSync',
'disableLocalVideoFlip',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'etherpad_base',
@@ -123,11 +123,12 @@ export default [
'startAudioMuted',
'startAudioOnly',
'startBitrate',
'startSilent',
'startScreenSharing',
'startSilent',
'startVideoMuted',
'startWithAudioMuted',
'startWithVideoMuted',
'stereo',
'subject',
'testing',
'useIPv6',

View File

@@ -22,6 +22,7 @@ export default [
'DEFAULT_REMOTE_DISPLAY_NAME',
'DISABLE_DOMINANT_SPEAKER_INDICATOR',
'DISABLE_FOCUS_INDICATOR',
'DISABLE_PRIVATE_MESSAGES',
'DISABLE_RINGING',
'DISABLE_TRANSCRIPTION_SUBTITLES',
'DISABLE_VIDEO_BACKGROUND',

View File

@@ -2,7 +2,6 @@
import _ from 'lodash';
import Platform from '../react/Platform';
import { equals, ReducerRegistry, set } from '../redux';
import { _UPDATE_CONFIG, CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from './actionTypes';
@@ -21,15 +20,6 @@ import { _cleanupConfig } from './functions';
const INITIAL_NON_RN_STATE = {
};
/**
* When we should enable H.264 on mobile. iOS 10 crashes so we disable it there.
* See: https://bugs.chromium.org/p/webrtc/issues/detail?id=11002
* Note that this is only used for P2P calls.
*
* @type {boolean}
*/
const RN_ENABLE_H264 = navigator.product === 'ReactNative' && !(Platform.OS === 'ios' && Platform.Version === 10);
/**
* The initial state of the feature base/config when executing in a React Native
* environment. The mandatory configuration to be passed to JitsiMeetJS#init().
@@ -50,11 +40,9 @@ const INITIAL_RN_STATE = {
// fastest to merely disable them.
disableAudioLevels: true,
disableH264: !RN_ENABLE_H264,
p2p: {
disableH264: !RN_ENABLE_H264,
preferH264: RN_ENABLE_H264
disableH264: false,
preferH264: true
}
};

View File

@@ -1,7 +1,7 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import { Platform, SafeAreaView, ScrollView, View } from 'react-native';
import { SafeAreaView, ScrollView, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { SlidingView } from '../../../react';
@@ -61,41 +61,18 @@ class BottomSheet extends PureComponent<Props> {
styles.sheetItemContainer,
_styles.sheet
] }>
{ this._getWrappedContent() }
<SafeAreaView>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false } >
{ this.props.children }
</ScrollView>
</SafeAreaView>
</View>
</View>
</SlidingView>
);
}
/**
* Wraps the content when needed (iOS 11 and above), or just returns the original content.
*
* @returns {React$Element}
*/
_getWrappedContent() {
const content = (
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false } >
{ this.props.children }
</ScrollView>
);
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS > 10) {
return (
<SafeAreaView>
{ content }
</SafeAreaView>
);
}
}
return content;
}
}
/**

View File

@@ -1,18 +1,14 @@
// @flow
import React, { Component, type Node } from 'react';
import { Platform, SafeAreaView, StatusBar, View } from 'react-native';
import React, { PureComponent, type Node } from 'react';
import { SafeAreaView, StatusBar, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { connect } from '../../../redux';
import { isDarkColor } from '../../../styles';
import { HEADER_PADDING } from './headerstyles';
/**
* Compatibility header padding size for iOS 10 (and older) devices.
*/
const IOS10_PADDING = 20;
// Register style
import './headerstyles';
/**
* Constanst for the (currently) supported statusbar colors.
@@ -44,19 +40,7 @@ type Props = {
/**
* A generic screen header component.
*/
class Header extends Component<Props> {
/**
* Initializes a new {@code Header} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._getIOS10CompatiblePadding
= this._getIOS10CompatiblePadding.bind(this);
}
class Header extends PureComponent<Props> {
/**
* Implements React's {@link Component#render()}.
*
@@ -66,11 +50,7 @@ class Header extends Component<Props> {
const { _styles } = this.props;
return (
<View
style = { [
_styles.headerOverlay,
this._getIOS10CompatiblePadding()
] } >
<View style = { _styles.headerOverlay }>
<StatusBar
backgroundColor = { _styles.statusBar }
barStyle = { this._getStatusBarContentColor() }
@@ -90,32 +70,6 @@ class Header extends Component<Props> {
);
}
_getIOS10CompatiblePadding: () => Object;
/**
* Adds a padding for iOS 10 (and older) devices to avoid clipping with the
* status bar.
* Note: This is a workaround for iOS 10 (and older) devices only to fix
* usability, but it doesn't take orientation into account, so unnecessary
* padding is rendered in some cases.
*
* @private
* @returns {Object}
*/
_getIOS10CompatiblePadding() {
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS <= 10) {
return {
paddingTop: HEADER_PADDING + IOS10_PADDING
};
}
}
return null;
}
/**
* Calculates the color of the statusbar content (light or dark) based on
* certain criterias.

View File

@@ -7,8 +7,7 @@ import { BoxModel } from '../../../styles';
const HEADER_FONT_SIZE = 18;
const HEADER_HEIGHT = 48;
export const HEADER_PADDING = BoxModel.padding / 2;
const HEADER_PADDING = BoxModel.padding / 2;
ColorSchemeRegistry.register('Header', {

View File

@@ -12,9 +12,7 @@ import type { Dispatch } from 'redux';
* very brittle because it's completely disconnected from the UI which wants to
* be rendered and, naturally, it broke on iPad where even the secondary Toolbar
* didn't fit in the height. We do need to measure the actual UI at runtime and
* determine whether and how to render it. I'm bumping from 240 to 300 because I
* don't have the time now to refactor {@code ReducedUIDetector} or rip it out
* completely.
* determine whether and how to render it.
*/
const REDUCED_UI_THRESHOLD = 300;

View File

@@ -1,73 +0,0 @@
// @flow
import React, { Component, type Node } from 'react';
import { type Dispatch } from 'redux';
import { connect } from '../../redux';
import { setAspectRatio } from '../actions';
import DimensionsDetector from './DimensionsDetector';
/**
* AspectRatioDetector component's property types.
*/
type Props = {
/**
* The "onDimensionsHandler" handler.
*/
_onDimensionsChanged: Function,
/**
* Any nested components.
*/
children: Node
};
/**
* A root {@link View} which captures the 'onLayout' event and figures out
* the aspect ratio of the app.
*/
class AspectRatioDetector extends Component<Props> {
/**
* Renders the root view and it's children.
*
* @returns {Component}
*/
render() {
return (
<DimensionsDetector
onDimensionsChanged = { this.props._onDimensionsChanged } >
{ this.props.children }
</DimensionsDetector>
);
}
}
/**
* Maps dispatching of the aspect ratio actions to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @private
* @returns {{
* _onDimensionsChanged: Function
* }}
*/
function _mapDispatchToProps(dispatch: Dispatch<any>) {
return {
/**
* Handles the "on dimensions changed" event and dispatches aspect ratio
* changed action.
*
* @param {number} width - The new width for the component.
* @param {number} height - The new height for the component.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
dispatch(setAspectRatio(width, height));
}
};
}
export default connect(undefined, _mapDispatchToProps)(AspectRatioDetector);

View File

@@ -1,6 +1,6 @@
// @flow
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import { View } from 'react-native';
import styles from './styles';
@@ -25,7 +25,7 @@ type Props = {
* A {@link View} which captures the 'onLayout' event and calls a prop with the
* component size.
*/
export default class DimensionsDetector extends Component<Props> {
export default class DimensionsDetector extends PureComponent<Props> {
/**
* Initializes a new DimensionsDetector instance.
*

View File

@@ -1,74 +0,0 @@
// @flow
import React, { Component, type Node } from 'react';
import { type Dispatch } from 'redux';
import { connect } from '../../redux';
import { setReducedUI } from '../actions';
import DimensionsDetector from './DimensionsDetector';
/**
* ReducedUIDetector component's property types.
*/
type Props = {
/**
* The "onDimensionsHandler" handler.
*/
_onDimensionsChanged: Function,
/**
* Any nested components.
*/
children: Node
};
/**
* A root {@link View} which captures the 'onLayout' event and figures out
* if the UI is reduced.
*/
class ReducedUIDetector extends Component<Props> {
/**
* Renders the root view and it's children.
*
* @returns {Component}
*/
render() {
return (
<DimensionsDetector
onDimensionsChanged = { this.props._onDimensionsChanged } >
{ this.props.children }
</DimensionsDetector>
);
}
}
/**
* Maps dispatching of the reduced UI actions to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @private
* @returns {{
* _onDimensionsChanged: Function
* }}
*/
function _mapDispatchToProps(dispatch: Dispatch<any>) {
return {
/**
* Handles the "on dimensions changed" event and dispatches the
* reduced UI action.
*
* @param {number} width - The new width for the component.
* @param {number} height - The new height for the component.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
dispatch(setReducedUI(width, height));
}
};
}
export default connect(undefined, _mapDispatchToProps)(ReducedUIDetector);

View File

@@ -1,4 +1,2 @@
export * from './AspectRatioAware';
export { default as AspectRatioDetector } from './AspectRatioDetector';
export { default as DimensionsDetector } from './DimensionsDetector';
export { default as ReducedUIDetector } from './ReducedUIDetector';

View File

@@ -3,4 +3,5 @@ export * from './actionTypes';
export * from './components';
export * from './constants';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,83 @@
// @flow
import { Dimensions } from 'react-native';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import { setAspectRatio, setReducedUI } from './actions';
/**
* Dimensions change handler.
*/
let handler;
/**
* Middleware that handles widnow dimension changes and updates the aspect ratio and
* reduced UI modes accordingly.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_UNMOUNT: {
_appWillUnmount();
break;
}
case APP_WILL_MOUNT:
_appWillMount(store);
break;
}
return result;
});
/**
* Notifies this feature that the action {@link APP_WILL_MOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @private
* @returns {void}
*/
function _appWillMount(store) {
handler = dim => {
_onDimensionsChange(dim, store);
};
Dimensions.addEventListener('change', handler);
}
/**
* Notifies this feature that the action {@link APP_WILL_UNMOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @private
* @returns {void}
*/
function _appWillUnmount() {
Dimensions.removeEventListener('change', handler);
handler = undefined;
}
/**
* Handles window dimension changes.
*
* @param {Object} dimensions - The new dimensions.
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _onDimensionsChange(dimensions, store) {
const { width, height } = dimensions.window;
const { dispatch } = store;
dispatch(setAspectRatio(width, height));
dispatch(setReducedUI(width, height));
}

View File

@@ -13,16 +13,14 @@ const DEFAULT_STATE = {
reducedUI: false
};
ReducerRegistry.register(
'features/base/responsive-ui',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_ASPECT_RATIO:
return set(state, 'aspectRatio', action.aspectRatio);
ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_ASPECT_RATIO:
return set(state, 'aspectRatio', action.aspectRatio);
case SET_REDUCED_UI:
return set(state, 'reducedUI', action.reducedUI);
}
case SET_REDUCED_UI:
return set(state, 'reducedUI', action.reducedUI);
}
return state;
});
return state;
});

View File

@@ -1,6 +1,10 @@
// @flow
import _ from 'lodash';
import { APP_WILL_MOUNT } from '../app';
import { setAudioOnly } from '../audio-only';
import parseURLParams from '../config/parseURLParams'; // minimize imports to avoid circular imports
import { SET_LOCATION_URL } from '../connection/actionTypes'; // minimize imports to avoid circular imports
import { getLocalParticipant, participantUpdated } from '../participants';
import { MiddlewareRegistry } from '../redux';
@@ -19,15 +23,37 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_MOUNT:
_initializeCallIntegration(store);
break;
case SETTINGS_UPDATED:
_maybeHandleCallIntegrationChange(action);
_maybeSetAudioOnly(store, action);
_updateLocalParticipant(store, action);
break;
case SET_LOCATION_URL:
_updateLocalParticipantFromUrl(store);
break;
}
return result;
});
/**
* Initializes the audio device handler based on the `disableCallIntegration` setting.
*
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _initializeCallIntegration({ getState }) {
const { disableCallIntegration } = getState()['features/base/settings'];
if (typeof disableCallIntegration === 'boolean') {
handleCallIntegrationChange(disableCallIntegration);
}
}
/**
* Maps the settings field names to participant names where they don't match.
* Currently there is only one such field, but may be extended in the future.
@@ -46,7 +72,7 @@ function _mapSettingsFieldToParticipant(settingsField) {
}
/**
* Updates {@code startAudioOnly} flag if it's updated in the settings.
* Handles a change in the `disableCallIntegration` setting.
*
* @param {Object} action - The redux action.
* @private
@@ -98,3 +124,30 @@ function _updateLocalParticipant({ dispatch, getState }, action) {
dispatch(participantUpdated(newLocalParticipant));
}
/**
* Returns the userInfo set in the URL.
*
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _updateLocalParticipantFromUrl({ dispatch, getState }) {
const urlParams
= parseURLParams(getState()['features/base/connection'].locationURL);
const urlEmail = urlParams['userInfo.email'];
if (!urlEmail) {
return;
}
const localParticipant = getLocalParticipant(getState());
if (localParticipant) {
dispatch(participantUpdated({
...localParticipant,
email: _.escape(urlEmail)
}));
}
}

View File

@@ -4,6 +4,8 @@ import md5 from 'js-md5';
import logger from './logger';
declare var __DEV__;
/**
* The name of the {@code localStorage} store where the app persists its values.
*/
@@ -87,7 +89,9 @@ class PersistenceRegistry {
// Initialize the checksum.
this._checksum = this._calculateChecksum(filteredPersistedState);
logger.info('redux state rehydrated as', filteredPersistedState);
if (typeof __DEV__ !== 'undefined' && __DEV__) {
logger.info('redux state rehydrated as', filteredPersistedState);
}
return filteredPersistedState;
}
@@ -113,7 +117,6 @@ class PersistenceRegistry {
logger.error(
'Error persisting redux subtree',
subtreeName,
filteredState[subtreeName],
error);
}
}
@@ -153,7 +156,7 @@ class PersistenceRegistry {
try {
return md5.hex(JSON.stringify(state) || '');
} catch (error) {
logger.error('Error calculating checksum for state', state, error);
logger.error('Error calculating checksum for state', error);
return '';
}

View File

@@ -117,8 +117,8 @@ class TestConnectionInfo extends Component<Props, State> {
this.setState({
stats: {
bitrate: {
download: stats.bitrate.download,
upload: stats.bitrate.upload
download: stats.bitrate?.download || 0,
upload: stats.bitrate?.upload || 0
}
}
});

View File

@@ -373,6 +373,23 @@ function _standardURIToString(thiz: ?Object) {
return str;
}
/**
* Sometimes we receive strings that we don't know if already percent-encoded, or not, due to the
* various sources we get URLs or room names. This function encapsulates the decoding in a safe way.
*
* @param {string} text - The text to decode.
* @returns {string}
*/
export function safeDecodeURIComponent(text: string) {
try {
return decodeURIComponent(text);
} catch (e) {
// The text wasn't encoded.
}
return text;
}
/**
* Attempts to return a {@code String} representation of a specific
* {@code Object} which is supposed to represent a URL. Obviously, if a
@@ -510,7 +527,7 @@ export function urlObjectToString(o: Object): ?string {
let { hash } = url;
for (const urlPrefix of [ 'config', 'interfaceConfig', 'devices' ]) {
for (const urlPrefix of [ 'config', 'interfaceConfig', 'devices', 'userInfo' ]) {
const urlParamsArray
= _objectToURLParamsArray(
o[`${urlPrefix}Overwrite`]

View File

@@ -15,7 +15,7 @@ export type Props = {
*
* @extends PureComponent
*/
export default class AbstractMessageContainer extends PureComponent<Props> {
export default class AbstractMessageContainer<P: Props> extends PureComponent<P> {
static defaultProps = {
messages: []
};
@@ -46,7 +46,7 @@ export default class AbstractMessageContainer extends PureComponent<Props> {
}
}
groups.push(currentGrouping);
currentGrouping.length && groups.push(currentGrouping);
return groups;
}

View File

@@ -3,6 +3,7 @@
import React, { Component } from 'react';
import { TextInput, TouchableOpacity, View } from 'react-native';
import { translate } from '../../../base/i18n';
import { Icon, IconChatSend } from '../../../base/icons';
import { Platform } from '../../../base/react';
@@ -13,7 +14,12 @@ type Props = {
/**
* Callback to invoke on message send.
*/
onSend: Function
onSend: Function,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
type State = {
@@ -37,7 +43,7 @@ type State = {
/**
* Implements the chat input bar with text field and action(s).
*/
export default class ChatInputBar extends Component<Props, State> {
class ChatInputBar extends Component<Props, State> {
/**
* Instantiates a new instance of the component.
*
@@ -53,6 +59,7 @@ export default class ChatInputBar extends Component<Props, State> {
};
this._onChangeText = this._onChangeText.bind(this);
this._onFieldReferenceAvailable = this._onFieldReferenceAvailable.bind(this);
this._onFocused = this._onFocused.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
@@ -76,6 +83,8 @@ export default class ChatInputBar extends Component<Props, State> {
onChangeText = { this._onChangeText }
onFocus = { this._onFocused(true) }
onSubmitEditing = { this._onSubmit }
placeholder = { this.props.t('chat.fieldPlaceHolder') }
ref = { this._onFieldReferenceAvailable }
returnKeyType = 'send'
style = { styles.inputField }
value = { this.state.message } />
@@ -105,6 +114,18 @@ export default class ChatInputBar extends Component<Props, State> {
});
}
_onFieldReferenceAvailable: Object => void;
/**
* Callback to be invoked when the field reference is available.
*
* @param {Object} field - The reference to the field.
* @returns {void}
*/
_onFieldReferenceAvailable(field) {
field && field.focus();
}
_onFocused: boolean => Function;
/**
@@ -138,3 +159,5 @@ export default class ChatInputBar extends Component<Props, State> {
});
}
}
export default translate(ChatInputBar);

View File

@@ -1,18 +1,36 @@
// @flow
import React from 'react';
import { FlatList } from 'react-native';
import { FlatList, Text, View } from 'react-native';
import AbstractMessageContainer, { type Props }
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractMessageContainer, { type Props as AbstractProps }
from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType,
/**
* Function to be used to translate i18n labels.
*/
t: Function
};
/**
* Implements a container to render all the chat messages in a conference.
*/
export default class MessageContainer extends AbstractMessageContainer {
class MessageContainer extends AbstractMessageContainer<Props> {
/**
* Instantiates a new instance of the component.
*
@@ -22,6 +40,7 @@ export default class MessageContainer extends AbstractMessageContainer {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
this._renderListEmptyComponent = this._renderListEmptyComponent.bind(this);
this._renderMessageGroup = this._renderMessageGroup.bind(this);
}
@@ -31,10 +50,16 @@ export default class MessageContainer extends AbstractMessageContainer {
* @inheritdoc
*/
render() {
const data = this._getMessagesGroupedBySender();
return (
<FlatList
data = { this._getMessagesGroupedBySender() }
inverted = { true }
ListEmptyComponent = { this._renderListEmptyComponent }
data = { data }
// Workaround for RN bug:
// https://github.com/facebook/react-native/issues/21196
inverted = { Boolean(data.length) }
keyExtractor = { this._keyExtractor }
keyboardShouldPersistTaps = 'always'
renderItem = { this._renderMessageGroup }
@@ -58,7 +83,26 @@ export default class MessageContainer extends AbstractMessageContainer {
return `key_${index}`;
}
_renderMessageGroup: Object => React$Element<*>;
_renderListEmptyComponent: () => React$Element<any>;
/**
* Renders a message when there are no messages in the chat yet.
*
* @returns {React$Element<any>}
*/
_renderListEmptyComponent() {
const { _styles, t } = this.props;
return (
<View style = { styles.emptyComponentWrapper }>
<Text style = { _styles.emptyComponentText }>
{ t('chat.noMessagesMessage') }
</Text>
</View>
);
}
_renderMessageGroup: Object => React$Element<any>;
/**
* Renders a single chat message.
@@ -70,3 +114,17 @@ export default class MessageContainer extends AbstractMessageContainer {
return <ChatMessageGroup messages = { messages } />;
}
}
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps)(MessageContainer));

View File

@@ -42,6 +42,13 @@ export default {
flexDirection: 'column'
},
emptyComponentWrapper: {
alignSelf: 'center',
flex: 1,
padding: BoxModel.padding,
paddingTop: '10%'
},
/**
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/
@@ -143,6 +150,11 @@ ColorSchemeRegistry.register('Chat', {
fontSize: 13
},
emptyComponentText: {
color: schemeColor('displayName'),
textAlign: 'center'
},
localMessageBubble: {
backgroundColor: schemeColor('localMsgBackground'),
borderTopRightRadius: 0

View File

@@ -14,7 +14,7 @@ import ChatMessageGroup from './ChatMessageGroup';
*
* @extends AbstractMessageContainer
*/
export default class MessageContainer extends AbstractMessageContainer {
export default class MessageContainer extends AbstractMessageContainer<Props> {
/**
* Whether or not chat has been scrolled to the bottom of the screen. Used
* to determine if chat should be scrolled automatically to the bottom when

View File

@@ -20,7 +20,7 @@ export function notifyKickedOut(participant: Object, _: ?Function) { // eslint-d
return (dispatch: Dispatch<any>, getState: Function) => {
const args = {
participantDisplayName:
getParticipantDisplayName(getState, participant.getDisplayName())
getParticipantDisplayName(getState, participant.getId())
};
dispatch(showNotification({

View File

@@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { NativeModules, SafeAreaView, StatusBar } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { appNavigate } from '../../../app';
@@ -279,7 +279,7 @@ class Conference extends AbstractConference<Props, *> {
</TintedView>
}
<View
<SafeAreaView
pointerEvents = 'box-none'
style = { styles.toolboxAndFilmstripContainer }>
@@ -320,7 +320,7 @@ class Conference extends AbstractConference<Props, *> {
*/
_shouldDisplayTileView ? undefined : <Filmstrip />
}
</View>
</SafeAreaView>
<SafeAreaView
pointerEvents = 'box-none'

View File

@@ -3,6 +3,7 @@
import React, { Component } from 'react';
import { getConferenceName } from '../../../base/conference/functions';
import { getParticipantCount } from '../../../base/participants/functions';
import { connect } from '../../../base/redux';
import { isToolboxVisible } from '../../../toolbox';
@@ -13,6 +14,11 @@ import ParticipantsCount from './ParticipantsCount';
*/
type Props = {
/**
* Whether then participant count should be shown or not.
*/
_showParticipantCount: boolean,
/**
* The subject or the of the conference.
* Falls back to conference name.
@@ -39,12 +45,12 @@ class Subject extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _subject, _visible } = this.props;
const { _showParticipantCount, _subject, _visible } = this.props;
return (
<div className = { `subject ${_visible ? 'visible' : ''}` }>
<span className = 'subject-text'>{ _subject }</span>
<ParticipantsCount />
{ _showParticipantCount && <ParticipantsCount /> }
</div>
);
}
@@ -62,8 +68,10 @@ class Subject extends Component<Props> {
* }}
*/
function _mapStateToProps(state) {
const participantCount = getParticipantCount(state);
return {
_showParticipantCount: participantCount > 2,
_subject: getConferenceName(state),
_visible: isToolboxVisible(state)
};

View File

@@ -94,6 +94,23 @@ class DeepLinkingMobilePage extends Component<Props> {
const downloadButtonClassName
= `${_SNS}__button ${_SNS}__button_primary`;
const onOpenLinkProperties = _URLS[Platform.OS]
? {
// When opening a link to the download page, we want to let the
// OS itself handle intercepting and opening the appropriate
// app store. This avoids potential issues with browsers, such
// as iOS Chrome, not opening the store properly.
}
: {
// When falling back to another URL (Firebase) let the page be
// opened in a new window. This helps prevent the user getting
// trapped in an app-open-cycle where going back to the mobile
// browser re-triggers the app-open behavior.
target: '_blank',
rel: 'noopener noreferrer'
};
return (
<div className = { _SNS }>
<div className = 'header'>
@@ -113,20 +130,18 @@ class DeepLinkingMobilePage extends Component<Props> {
{ t(`${_TNS}.appNotInstalled`, { app: NATIVE_APP_NAME }) }
</p>
<a
{ ...onOpenLinkProperties }
href = { this._generateDownloadURL() }
onClick = { this._onDownloadApp }
rel = 'noopener noreferrer'
target = '_blank'>
onClick = { this._onDownloadApp }>
<button className = { downloadButtonClassName }>
{ t(`${_TNS}.downloadApp`) }
</button>
</a>
<a
{ ...onOpenLinkProperties }
className = { `${_SNS}__href` }
href = { generateDeepLinkingURL() }
onClick = { this._onOpenApp }
rel = 'noopener noreferrer'
target = '_blank'>
onClick = { this._onOpenApp }>
{/* <button className = { `${_SNS}__button` }> */}
{ t(`${_TNS}.openApp`) }
{/* </button> */}

View File

@@ -15,10 +15,10 @@ import {
CONNECTION_DISCONNECTED,
CONNECTION_FAILED,
JITSI_CONNECTION_CONFERENCE_KEY,
JITSI_CONNECTION_URL_KEY
JITSI_CONNECTION_URL_KEY,
getURLWithoutParams
} from '../../base/connection';
import { MiddlewareRegistry } from '../../base/redux';
import { toURLString } from '../../base/util';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
import { sendEvent } from './functions';
@@ -82,7 +82,7 @@ MiddlewareRegistry.register(store => next => action => {
store,
CONFERENCE_TERMINATED,
/* data */ {
url: toURLString(locationURL)
url: _normalizeUrl(locationURL)
});
}
@@ -106,7 +106,7 @@ MiddlewareRegistry.register(store => next => action => {
CONFERENCE_TERMINATED,
/* data */ {
error: _toErrorString(error),
url: toURLString(locationURL)
url: _normalizeUrl(locationURL)
});
break;
}
@@ -161,10 +161,20 @@ function _maybeTriggerEarlyConferenceWillJoin(store, action) {
store,
CONFERENCE_WILL_JOIN,
/* data */ {
url: toURLString(locationURL)
url: _normalizeUrl(locationURL)
});
}
/**
* Normalizes the given URL for presentation over the external API.
*
* @param {URL} url -The URL to normalize.
* @returns {string} - The normalized URL as a string.
*/
function _normalizeUrl(url: URL) {
return getURLWithoutParams(url).href;
}
/**
* Sends an event to the native counterpart of the External API for a specific
* conference-related redux action.
@@ -186,7 +196,7 @@ function _sendConferenceEvent(
// instance. The external API cannot transport such an object so we have to
// transport an "equivalent".
if (conference) {
data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
data.url = _normalizeUrl(conference[JITSI_CONFERENCE_URL_KEY]);
}
if (_swallowEvent(store, action, data)) {
@@ -233,7 +243,7 @@ function _sendConferenceFailedOnConnectionError(store, action) {
store,
CONFERENCE_TERMINATED,
/* data */ {
url: toURLString(locationURL),
url: _normalizeUrl(locationURL),
error: action.error.name
});
}

View File

@@ -3,7 +3,7 @@ import {
getLocalizedDurationFormatter
} from '../base/i18n';
import { NavigateSectionList } from '../base/react';
import { parseURIString } from '../base/util';
import { parseURIString, safeDecodeURIComponent } from '../base/util';
/**
* Creates a displayable list item of a recent list entry.
@@ -31,7 +31,7 @@ function toDisplayableItem(item, defaultServerURL, t) {
_toDurationString(item.duration),
serverName
],
title: decodeURIComponent(location.room),
title: safeDecodeURIComponent(location.room),
url: item.conference
};
}

View File

@@ -5,10 +5,25 @@ import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import { IconMessage } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { _mapDispatchToProps, _mapStateToProps, type Props } from '../../../chat/components/PrivateMessageButton';
import {
_mapDispatchToProps,
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../../../chat/components/PrivateMessageButton';
import { isButtonEnabled } from '../../../toolbox';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
declare var interfaceConfig: Object;
type Props = AbstractProps & {
/**
* True if the private chat functionality is disabled, hence the button is not visible.
*/
_hidden: boolean
};
/**
* A custom implementation of the PrivateMessageButton specialized for
* the web version of the remote video menu. When the web platform starts to use
@@ -34,7 +49,11 @@ class PrivateMessageMenuButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
const { participantID, t, _hidden } = this.props;
if (_hidden) {
return null;
}
return (
<RemoteVideoMenuButton
@@ -59,4 +78,19 @@ class PrivateMessageMenuButton extends Component<Props> {
}
}
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
return {
..._abstractMapStateToProps(state, ownProps),
_hidden: typeof interfaceConfig !== 'undefined'
&& (interfaceConfig.DISABLE_PRIVATE_MESSAGES || !isButtonEnabled('chat'))
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageMenuButton));

View File

@@ -5,7 +5,7 @@ import { Text, View } from 'react-native';
import { translate } from '../../../base/i18n';
import styles, { ANDROID_UNDERLINE_COLOR } from './styles';
import styles, { ANDROID_UNDERLINE_COLOR, PLACEHOLDER_COLOR } from './styles';
/**
* The type of the React {@code Component} props of {@link FormRow}
@@ -107,6 +107,7 @@ class FormRow extends Component<Props> {
switch (field.type.displayName) {
case 'TextInput':
return {
placeholderTextColor: PLACEHOLDER_COLOR,
style: styles.textInputField,
underlineColorAndroid: ANDROID_UNDERLINE_COLOR
};

View File

@@ -4,6 +4,8 @@ import {
} from '../../../base/styles';
export const ANDROID_UNDERLINE_COLOR = 'transparent';
export const PLACEHOLDER_COLOR = ColorPalette.lightGrey;
const TEXT_SIZE = 17;
/**
@@ -79,6 +81,7 @@ export default createStyleSheet({
* Standard text input field style.
*/
textInputField: {
color: ColorPalette.black,
flex: 1,
fontSize: TEXT_SIZE,
textAlign: 'right'

View File

@@ -1,6 +1,6 @@
// @flow
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import { View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
@@ -46,37 +46,10 @@ type Props = {
dispatch: Function
};
/**
* The type of {@link Toolbox}'s React {@code Component} state.
*/
type State = {
/**
* The detected width for this component.
*/
width: number
};
/**
* Implements the conference toolbox on React Native.
*/
class Toolbox extends Component<Props, State> {
state = {
width: 0
};
/**
* Initializes a new {@code Toolbox} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onLayout = this._onLayout.bind(this);
}
class Toolbox extends PureComponent<Props> {
/**
* Implements React's {@link Component#render()}.
*
@@ -86,7 +59,6 @@ class Toolbox extends Component<Props, State> {
render() {
return (
<Container
onLayout = { this._onLayout }
style = { styles.toolbox }
visible = { this.props._visible }>
{ this._renderToolbar() }
@@ -125,20 +97,6 @@ class Toolbox extends Component<Props, State> {
};
}
_onLayout: (Object) => void;
/**
* Handles the "on layout" View's event and stores the width as state.
*
* @param {Object} event - The "on layout" event object/structure passed
* by react-native.
* @private
* @returns {void}
*/
_onLayout({ nativeEvent: { layout: { width } } }) {
this.setState({ width });
}
/**
* Renders the toolbar. In order to avoid a weird visual effect in which the
* toolbar is (visually) rendered and then visibly changes its size, it is

View File

@@ -57,7 +57,10 @@ import {
} from '../../../settings';
import { toggleSharedVideo } from '../../../shared-video';
import { SpeakerStats } from '../../../speaker-stats';
import { TileViewButton } from '../../../video-layout';
import {
TileViewButton,
toggleTileView
} from '../../../video-layout';
import {
OverflowMenuVideoQualityItem,
VideoQualityDialog
@@ -122,6 +125,11 @@ type Props = {
*/
_fullScreen: boolean,
/**
* Whether or not the tile view is enabled.
*/
_tileViewEnabled: boolean,
/**
* Whether or not invite should be hidden, regardless of feature
* availability.
@@ -236,6 +244,7 @@ class Toolbox extends Component<Props, State> {
this._onToolbarToggleScreenshare = this._onToolbarToggleScreenshare.bind(this);
this._onToolbarToggleSharedVideo = this._onToolbarToggleSharedVideo.bind(this);
this._onToolbarOpenLocalRecordingInfoDialog = this._onToolbarOpenLocalRecordingInfoDialog.bind(this);
this._onShortcutToggleTileView = this._onShortcutToggleTileView.bind(this);
this.state = {
windowWidth: window.innerWidth
@@ -274,6 +283,11 @@ class Toolbox extends Component<Props, State> {
character: 'S',
exec: this._onShortcutToggleFullScreen,
helpDescription: 'keyboardShortcuts.fullScreen'
},
this._shouldShowButton('tileview') && {
character: 'W',
exec: this._onShortcutToggleTileView,
helpDescription: 'toolbar.tileViewToggle'
}
];
@@ -475,6 +489,16 @@ class Toolbox extends Component<Props, State> {
this.props.dispatch(toggleDialog(VideoQualityDialog));
}
/**
* Dispaches an action to toggle tile view.
*
* @private
* @returns {void}
*/
_doToggleTileView() {
this.props.dispatch(toggleTileView());
}
_onMouseOut: () => void;
/**
@@ -565,6 +589,24 @@ class Toolbox extends Component<Props, State> {
this._doToggleVideoQuality();
}
_onShortcutToggleTileView: () => void;
/**
* Dispatches an action for toggling the tile view.
*
* @private
* @returns {void}
*/
_onShortcutToggleTileView() {
sendAnalytics(createShortcutEvent(
'toggle.tileview',
{
enable: !this.props._tileViewEnabled
}));
this._doToggleTileView();
}
_onShortcutToggleFullScreen: () => void;
/**
@@ -1295,6 +1337,7 @@ function _mapStateToProps(state) {
iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
_isGuest: state['features/base/jwt'].isGuest,
_fullScreen: fullScreen,
_tileViewEnabled: state['features/video-layout'].tileViewEnabled,
_localParticipantID: localParticipant.id,
_localRecState: localRecordingStates,
_overflowMenuVisible: overflowMenuVisible,

View File

@@ -1,4 +1,5 @@
local get_room_from_jid = module:require "util".get_room_from_jid;
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local jid_resource = require "util.jid".resource;
local ext_events = module:require "ext_events"
local st = require "util.stanza";
@@ -25,7 +26,7 @@ function on_message(event)
= event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet');
if speakerStats then
local roomAddress = speakerStats.attr.room;
local room = get_room_from_jid(roomAddress);
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
if not room then
log("warn", "No room found %s", roomAddress);

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="../../libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/load-test-participant.min.js" ></script>
</head>
<body>
<div>Number of participants: <span id="participants">1</span></div>
</body>
</html>

View File

@@ -0,0 +1,261 @@
/* global $, JitsiMeetJS */
import 'jquery';
import parseURLParams from '../../react/features/base/config/parseURLParams';
const params = parseURLParams(window.location, false, 'hash');
const { isHuman = false } = params;
const {
roomName = 'loadtest0',
localAudio = isHuman,
localVideo = isHuman,
remoteVideo = isHuman,
remoteAudio = isHuman
} = params;
const options = {
hosts: {
domain: 'george-perf.jitsi.net',
muc: 'conference.george-perf.jitsi.net'
},
bosh: '//george-perf.jitsi.net/http-bind',
// The name of client node advertised in XEP-0115 'c' stanza
clientNode: 'http://jitsi.org/jitsimeet'
};
const confOptions = {
openBridgeChannel: 'websocket',
testing: {
testMode: true,
noAutoPlayVideo: true
},
disableNS: true,
disableAEC: true,
gatherStats: true,
callStatsID: false
};
let connection = null;
let isJoined = false;
let room = null;
let numParticipants = 1;
let localTracks = [];
const remoteTracks = {};
window.APP = {
get room() {
return room;
},
get connection() {
return connection;
},
get numParticipants() {
return numParticipants;
},
get localTracks() {
return localTracks;
},
get remoteTracks() {
return remoteTracks;
},
get params() {
return {
roomName,
localAudio,
localVideo,
remoteVideo,
remoteAudio
};
}
};
/**
*
*/
function setNumberOfParticipants() {
$('#participants').text(numParticipants);
}
/**
* Handles local tracks.
* @param tracks Array with JitsiTrack objects
*/
function onLocalTracks(tracks = []) {
localTracks = tracks;
for (let i = 0; i < localTracks.length; i++) {
if (localTracks[i].getType() === 'video') {
$('body').append(`<video autoplay='1' id='localVideo${i}' />`);
localTracks[i].attach($(`#localVideo${i}`)[0]);
} else {
$('body').append(
`<audio autoplay='1' muted='true' id='localAudio${i}' />`);
localTracks[i].attach($(`#localAudio${i}`)[0]);
}
if (isJoined) {
room.addTrack(localTracks[i]);
}
}
}
/**
* Handles remote tracks
* @param track JitsiTrack object
*/
function onRemoteTrack(track) {
if (track.isLocal()
|| (track.getType() === 'video' && !remoteVideo) || (track.getType() === 'audio' && !remoteAudio)) {
return;
}
const participant = track.getParticipantId();
if (!remoteTracks[participant]) {
remoteTracks[participant] = [];
}
const idx = remoteTracks[participant].push(track);
const id = participant + track.getType() + idx;
if (track.getType() === 'video') {
$('body').append(`<video autoplay='1' id='${id}' />`);
} else {
$('body').append(`<audio autoplay='1' id='${id}' />`);
}
track.attach($(`#${id}`)[0]);
}
/**
* That function is executed when the conference is joined
*/
function onConferenceJoined() {
isJoined = true;
for (let i = 0; i < localTracks.length; i++) {
room.addTrack(localTracks[i]);
}
}
/**
*
* @param id
*/
function onUserLeft(id) {
numParticipants--;
setNumberOfParticipants();
if (!remoteTracks[id]) {
return;
}
const tracks = remoteTracks[id];
for (let i = 0; i < tracks.length; i++) {
const container = $(`#${id}${tracks[i].getType()}${i + 1}`)[0];
if (container) {
tracks[i].detach(container);
container.parentElement.removeChild(container);
}
}
}
/**
* That function is called when connection is established successfully
*/
function onConnectionSuccess() {
room = connection.initJitsiConference(roomName, confOptions);
room.on(JitsiMeetJS.events.conference.TRACK_ADDED, onRemoteTrack);
room.on(JitsiMeetJS.events.conference.CONFERENCE_JOINED, onConferenceJoined);
room.on(JitsiMeetJS.events.conference.USER_JOINED, id => {
numParticipants++;
setNumberOfParticipants();
remoteTracks[id] = [];
});
room.on(JitsiMeetJS.events.conference.USER_LEFT, onUserLeft);
room.join();
}
/**
* This function is called when the connection fail.
*/
function onConnectionFailed() {
console.error('Connection Failed!');
}
/**
* This function is called when we disconnect.
*/
function disconnect() {
console.log('disconnect!');
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED,
onConnectionSuccess);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_FAILED,
onConnectionFailed);
connection.removeEventListener(
JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED,
disconnect);
}
/**
*
*/
function unload() {
for (let i = 0; i < localTracks.length; i++) {
localTracks[i].dispose();
}
room.leave();
connection.disconnect();
}
$(window).bind('beforeunload', unload);
$(window).bind('unload', unload);
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.ERROR);
const initOptions = {
disableAudioLevels: true,
// The ID of the jidesha extension for Chrome.
desktopSharingChromeExtId: 'mbocklcggfhnbahlnepmldehdhpjfcjp',
// Whether desktop sharing should be disabled on Chrome.
desktopSharingChromeDisabled: true,
// The media sources to use when using screen sharing with the Chrome
// extension.
desktopSharingChromeSources: [ 'screen', 'window' ],
// Required version of Chrome extension
desktopSharingChromeMinExtVersion: '0.1',
// Whether desktop sharing should be disabled on Firefox.
desktopSharingFirefoxDisabled: true
};
JitsiMeetJS.init(initOptions);
connection = new JitsiMeetJS.JitsiConnection(null, null, options);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_ESTABLISHED, onConnectionSuccess);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_FAILED, onConnectionFailed);
connection.addEventListener(JitsiMeetJS.events.connection.CONNECTION_DISCONNECTED, disconnect);
connection.connect();
const devices = [];
if (localVideo) {
devices.push('video');
}
if (localAudio) {
devices.push('audio');
}
if (devices.length > 0) {
JitsiMeetJS.createLocalTracks({ devices })
.then(onLocalTracks)
.catch(error => {
throw error;
});
}

6983
static/load-test/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
{
"name": "jitsi-meet-load-test",
"version": "0.0.0",
"description": "A load test participant",
"repository": {
"type": "git",
"url": "git://github.com/jitsi/jitsi-meet"
},
"keywords": [
"jingle",
"webrtc",
"xmpp",
"browser"
],
"author": "",
"readmeFilename": "../README.md",
"dependencies": {
"jquery": "3.4.0"
},
"devDependencies": {
"@babel/core": "7.5.5",
"@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.4.4",
"@babel/plugin-proposal-optional-chaining": "7.2.0",
"@babel/plugin-transform-flow-strip-types": "7.0.0",
"@babel/preset-env": "7.1.0",
"@babel/preset-flow": "7.0.0",
"@babel/runtime": "7.5.5",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"eslint": "5.6.1",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#1.0.1",
"eslint-plugin-flowtype": "2.50.3",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsdoc": "3.8.0",
"expose-loader": "0.7.5",
"flow-bin": "0.104.0",
"imports-loader": "0.7.1",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"webpack": "4.27.1",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.1.2"
},
"engines": {
"node": ">=8.0.0",
"npm": ">=6.0.0"
},
"license": "Apache-2.0",
"scripts": {
"build": "webpack -p"
}
}

View File

@@ -0,0 +1,131 @@
/* global __dirname */
const process = require('process');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const analyzeBundle = process.argv.indexOf('--analyze-bundle') !== -1;
const minimize
= process.argv.indexOf('-p') !== -1
|| process.argv.indexOf('--optimize-minimize') !== -1;
/**
* Build a Performance configuration object for the given size.
* See: https://webpack.js.org/configuration/performance/
*/
function getPerformanceHints(size) {
return {
hints: minimize ? 'error' : false,
maxAssetSize: size,
maxEntrypointSize: size
};
}
// The base Webpack configuration to bundle the JavaScript artifacts of
// jitsi-meet such as app.bundle.js and external_api.js.
const config = {
devtool: 'source-map',
mode: minimize ? 'production' : 'development',
module: {
rules: [ {
// Transpile ES2015 (aka ES6) to ES5. Accept the JSX syntax by React
// as well.
exclude: [
new RegExp(`${__dirname}/node_modules/(?!js-utils)`)
],
loader: 'babel-loader',
options: {
// XXX The require.resolve bellow solves failures to locate the
// presets when lib-jitsi-meet, for example, is npm linked in
// jitsi-meet.
plugins: [
require.resolve('@babel/plugin-transform-flow-strip-types'),
require.resolve('@babel/plugin-proposal-class-properties'),
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-proposal-export-namespace-from'),
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'),
require.resolve('@babel/plugin-proposal-optional-chaining')
],
presets: [
[
require.resolve('@babel/preset-env'),
// Tell babel to avoid compiling imports into CommonJS
// so that webpack may do tree shaking.
{
modules: false,
// Specify our target browsers so no transpiling is
// done unnecessarily. For browsers not specified
// here, the ES2015+ profile will be used.
targets: {
chrome: 58,
electron: 2,
firefox: 54,
safari: 11
}
}
],
require.resolve('@babel/preset-flow'),
require.resolve('@babel/preset-react')
]
},
test: /\.jsx?$/
}, {
// Expose jquery as the globals $ and jQuery because it is expected
// to be available in such a form by multiple jitsi-meet
// dependencies including lib-jitsi-meet.
loader: 'expose-loader?$!expose-loader?jQuery',
test: /\/node_modules\/jquery\/.*\.js$/
}]
},
node: {
// Allow the use of the real filename of the module being executed. By
// default Webpack does not leak path-related information and provides a
// value that is a mock (/index.js).
__filename: true
},
optimization: {
concatenateModules: minimize,
minimize
},
output: {
filename: `[name]${minimize ? '.min' : ''}.js`,
path: `${__dirname}/libs`,
publicPath: 'load-test/libs/',
sourceMapFilename: `[name].${minimize ? 'min' : 'js'}.map`
},
plugins: [
analyzeBundle
&& new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true
})
].filter(Boolean),
resolve: {
alias: {
jquery: `jquery/dist/jquery${minimize ? '.min' : ''}.js`
},
aliasFields: [
'browser'
],
extensions: [
'.web.js',
// Webpack defaults:
'.js',
'.json'
]
}
};
module.exports = [
Object.assign({}, config, {
entry: {
'load-test-participant': './load-test-participant.js'
},
performance: getPerformanceHints(3 * 1024 * 1024)
})
];