Compare commits

...

56 Commits

Author SHA1 Message Date
titus.moldovan
d5916cdb3a e2ee stuff 2022-06-23 15:18:13 +03:00
tmoldovan8x8
9a99c517ab fix(rn, pip) enables PiP on conference mounted 2022-06-23 14:42:41 +03:00
Robert Pintilii
40a6240444 feat(local-recording) Update config (#11731)
Enable feature by default
2022-06-23 14:03:21 +03:00
tmoldovan8x8
4d6ca4383f fix(android) calls startForeground in onCreate
Call startForeground in onCreate to avoid android.app.RemoteServiceException thrown by the system.
2022-06-23 09:41:32 +02:00
Robert Pintilii
ddce2e6bec feat(breakout-rooms) add context menu to participants in other rooms 2022-06-23 09:40:11 +02:00
Robert Pintilii
7dca91a50a fix(local-recording) Add notification config and style fixes (#11728)
Add analytics
2022-06-22 12:52:22 +03:00
Saúl Ibarra Corretgé
31348179d4 fix(auth) simplify auth-and-upgrade procedure
It's not necessary to perform a full join, sending a conference IQ is
enough.
2022-06-21 19:20:09 +02:00
Calinteodor
e77679d025 feat(dynamic-branding): SVG branding image needs to cover the entire screen (#11724)
* feat(dynamic-branding) scale SVG branding image to cover entire screen
2022-06-21 17:51:25 +03:00
Titus Moldovan
44a9363f5b feat(mobile, external_api) exposes setClosedCaptionsEnabled 2022-06-21 16:18:31 +02:00
Calinteodor
bb76090bce feat(lobby/prejoin): updates
* feat(base/modal) added keyboard dismiss functionality

* feat(lobby) updated ui and start knocking if name is set

* feat(prejoin) updated ui and hide input if name is not required

* feat(prejoin) updated join button styles

* feat(prejoin) removed extra empty space

* feat(prejoin) updated disable join condition

* feat(base/modal) moved keaboard dismiss functionality

* feat(conference) updated auto knock condition

* feat(prejoin) updated button styles and disabling condition

* feat(lobby) updated styles

* feat(lobby/prejoin) updated styles for buttons and inputs

* feat(lobby/prejoin) updated contentContainer styles

* feat(lobby/prejoin) created shouldEnableAutoKnock helper
2022-06-21 16:16:38 +02:00
Robert Pintilii
d0790736db feat(external-api) Add participants pane toggled event (#11718) 2022-06-21 16:23:33 +03:00
Jaya Allamsetty
0308ba71b1 fix(audio-only) Do not unmute camera when SS is in progress.
If the audio-only mode is automatically disabled when user starts a screenshare while in audio-only mode, do not unmute the camera track.
2022-06-21 07:48:02 -04:00
Jaya Allamsetty
f7d1a5ec80 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1455.0.0+f3a2f61e...v1457.0.0+ad75454f
2022-06-21 07:24:38 -04:00
Saúl Ibarra Corretgé
d61fe58fcf fix(rn,styles) cleanup unused styles 2022-06-21 11:48:07 +02:00
Titus Moldovan
b428ce2dcd fix(pip) make PiP disabled by default
This reverts commit c84c3c61e2c24014b43023316627f7747bbca7a6.
2022-06-21 10:36:21 +03:00
Titus Moldovan
d1c9720033 fix(rn) add backhandler on Prejoin screen 2022-06-21 10:36:21 +03:00
Andrei Gavrilescu
c29e8bbdd1 feat(analytics): obfuscate room name (#11587)
* obfuscate room name

* fixed js-sha version

* add comma

* check for room change
2022-06-21 09:53:07 +03:00
Horatiu Muresan
3fb3be9727 feat(invite) Consider SHARING_FEATURES on the email invites (#11711) 2022-06-20 23:18:06 +03:00
Saúl Ibarra Corretgé
517ec29d85 fix(rn,dialogs) fix displaying dialogs on top of modal screens
Render them together with bottom sheets in a FullWindowOverlay.
2022-06-20 18:37:28 +03:00
Saúl Ibarra Corretgé
6ad279f029 fix(rn, bottomsheet) fix not rendering above presentation sheets
Move all sheets to render in a new container which uses FullWindowOverlay, which allows rendering above presentation controllers on iOS.
2022-06-20 16:53:19 +02:00
José Luís Andrade
0e98f90205 fix(lang) update Portuguese translation 2022-06-18 23:26:06 +02:00
Saúl Ibarra Corretgé
2c5b132483 fix(util) fix parsing strings in parseURLParams
After https://github.com/jitsi/jitsi-meet/pull/11607 we might call it
with a string. Be nice and accept that in addition to URL objects.
2022-06-18 23:17:55 +02:00
Calinteodor
4d8f29d4fe feat(rn,dynamic-brandind) added background image url to prejoin and lobby 2022-06-18 21:59:10 +02:00
Ali Alhaidary
22be96d838 fix(lang) update Arabic translation 2022-06-18 20:54:51 +02:00
Дамян Минков
ccc1157df5 fix: Fixes navigating back to welcome page after clicking cancel on login window.
It was handling just conference_failed with password required, but not connection failed with password required.
2022-06-17 15:19:06 +03:00
Дамян Минков
f613126776 fix: Hides pre join screen in few login window cases.
In Firefox pre-join was grabbing the focus and yuo cannot type username and password after clicking I'm the host button.
2022-06-17 15:19:06 +03:00
Robert Pintilii
38b21e986d fix(pinning) Fix pinning (#11693)
Hide Pin to Stage button while screensharing
Fix pin indicator while screensharing
2022-06-17 15:15:14 +03:00
Horatiu Muresan
38abca8a65 fix(carmode) Force potrait mode, add connection indicator 2022-06-17 12:54:51 +02:00
Дамян Минков
f3c6b54ffa fix: When adding a room param to urls check for previous params. (#11607)
* fix: When adding a room param to urls check for previous params.

* squash: Uses URL object to modify the url.

* squash: Use common connection options from base/connection.

Normalizes bosh url and for web.

* squash: Adds release param to external api and handles it.

* feat: Adds release handling for mobile(links in welcome page).

* squash: Fixes comments.
2022-06-16 15:27:41 +03:00
Gabriel Borlea
7dd85bb6ad fix(face-landmarks): work only when one face is detected (#11661)
* fix(face-landmarks): work only when one face is detected

* fix: remove redundant check for detection

* fix(face-landmarks): re-center and stop when more faces detected

* fix: remove faceCount checking when sending message from worker

* fix: add again the faceCount

* fix: add comment

* code review
2022-06-16 14:50:31 +03:00
Gabriel Borlea
624f88e069 add(face-landmarks): flag for rtc stats (#11682)
* add(face-landmarks): flag for rtc stats

* fix: check is faceLandmarks is defined
2022-06-16 14:13:36 +03:00
Calinteodor
fbf693b2dc feat(mobile/navigation) updated screens that have footer
* feat(mobile/navigation) updated screens that have footer

* feat(chat/native) reverted style change

* feat(chat/native) reverted changes and added input vertical padding

* feat(base/modal) replaced headerHeight with top safe area inset

* feat(carmode/native) removed unused import and fixed linter

* feat(chat/polls/native) reverted style changes

* feat(base/modal) added isModalPresentation default prop

* feat(base/modal) made isModalPresentation optional

* feat(base/modal) headerHeight based on top notch devices

* feat(polls) updated styles

* feat(base/modal) updated comment
2022-06-16 11:49:53 +02:00
Calinteodor
dbf7bf4750 feat(prejoin) native prejoin screen and other navigation updates
* feat(prejoin) created native Prejoin screen

* feat(prejoin) fixed useState callback and updates warnings

* feat(prejoin) created styles file

* feat(prejoin) moved nav from middleware to appNavigate, created native DeviceStatus

* feat(prejoin) updated styles

* feat(prejoin) review remarks pt. 1

* feat(prejoin) removed unused styles

* feat(prejoin) review remarks pt. 2

* feat(prejoin) comment fix

* feat(prejoin) added header title

* feat(prejoin) review remarks

* feat(lobby) updated styles

* feat(prejoin) updated lobby screen header button functionality

* feat(prejoin) review remarks pt 3

* feat(welcome) removed VideoSwitch component

* feat(mobile/navigation) fixed linter

* feat(welcome) moved isWelcomePageEnabled to functions.ts

* feat(mobile/navigation) screen options and order updates

* feat(app) review remark

* feat(welcome) added translation for screen header title and fixed build

* feat(mobile/navigation) added screen title translation and created screen option

* feat(mobile/navigation) fixed screenOptions import

* feat(mobile/navigation) added DialInSummary title translation, fixed animation and close button

* feat(welcome) fixed build

* feat(welcome) removed extra check

* feat(prejoin) review remarks pt 4

* feat(prejoin) added Join in low bandwidth mode btn

* feat(welcome) changed welcome screen header title

* fixup lobby close
2022-06-16 11:49:07 +02:00
Titus Moldovan
d31eb3b248 fix(android) parse initial isAudioMuted when starting JitsiMeetOngoingConferenceService 2022-06-16 11:54:25 +03:00
Titus Moldovan
9b75fc98c1 feat(rn) send isAudioMuted on conferenceEvent 2022-06-16 11:54:25 +03:00
hmuresan
1ee9f6a7e5 feat(extension-banner) Show edge extension when edge browser detected 2022-06-16 11:43:03 +03:00
Robert Pintilii
06d0cbd418 fix(local-recording) Don't use setCaptureHandle when in iframe (#11687) 2022-06-16 10:43:58 +03:00
Nils Ohlmeier
066dd71afb feat(RTC): report conference start timestamp through rtcstats (#11646) 2022-06-15 15:22:15 -07:00
Hristo Terezov
d573bd41b4 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1454.0.0+fd668c9d...v1455.0.0+f3a2f61e
2022-06-15 16:25:02 -05:00
Robert Pintilii
d06d190229 fix(keyboard-shortcut) Fix error on number keypress (#11680)
Fix error when pressing a number bigger than the number of participants
Fix local recording issue
2022-06-15 14:04:06 +03:00
Shahab
d3b650c741 refactor(prejoin) use jss instead of sass in DialOutDialog (#11361)
* refactor(premeeting): use jss instead of sass in DialOutDialog

* refactor(prejoin): move remaining prejoin-dialog styles to commonStyless
2022-06-15 10:34:09 +03:00
Saúl Ibarra Corretgé
4a04b8b5ee fix(rn,dynamic-branding) filter out gradients 2022-06-14 15:39:14 +02:00
Saúl Ibarra Corretgé
6718ba7423 feat(rn,dynamic-branding) add support for didPageUrl and inviteDomain 2022-06-14 15:39:14 +02:00
robertpin
e662433c2a Add audio constraints 2022-06-14 15:11:00 +02:00
robertpin
08bb957672 fix(local-recording) Add framerate 2022-06-14 15:11:00 +02:00
Saúl Ibarra Corretgé
78d8176cc8 fix(rn,bottom-sheet) fix styling after refactor
I somehow missed all other usages of the ColorSchemeRegistry.
2022-06-14 13:38:45 +02:00
robertpin
59ee984e09 fix(tile-view, rn) Fix landscape mode tile view 2022-06-14 13:38:24 +02:00
Robert Pintilii
f6fab051ce chore(deps) lib-jitsi-meet@latest (#11671)
https://github.com/jitsi/lib-jitsi-meet/compare/v1450.0.0+462996fc...v1454.0.0+fd668c9d
2022-06-14 11:01:04 +03:00
Saúl Ibarra Corretgé
f0ff6a9f1c fix(video-layout) fix usage of disableTileView
The documented behavior is that it would disable auto-switching to it,
but users would still be available to toggle it.

This change restores that behavior. If the user has selected a layout
that will be preferred before cheching for this setting.

Ref: https://community.jitsi.org/t/how-to-disable-titleview/115093
2022-06-13 14:35:30 +02:00
Calin Chitu
dfa761b963 feat(mobile/navigation): added screen orientation based on Platform 2022-06-10 17:55:56 +02:00
Saúl Ibarra Corretgé
ad8cdcd81b fix(rn,bottom-sheet) fix scroll
In the past we used a PanResponder to detect user gestures in the sheet
to show a reduced version or a full-height version of it, and also to
close it.

There is an obvious conflic between the gestures and scrolling, which
didn't work all that great, but we could live with it.

After reactions were introduced we no longer rendered the 2 different
heights, so that functionaligy stopped being used but the PanResponder
still remained there.

This commit removes it completely and sets a max height of 75% on any
BottomSheet, so any tap outside will close it.
2022-06-10 17:54:58 +02:00
Calin Chitu
98ef0e74d6 feat(welcome/native): updated settings name placeholder example text 2022-06-10 17:59:50 +03:00
Saúl Ibarra Corretgé
746fde7c10 fix(local-recordings) fix for browsers not supporting MediaRecorder 2022-06-10 16:15:03 +02:00
Calin Chitu
bedddd4760 fix(lobby/native) removed nav button overwrite 2022-06-10 16:07:56 +03:00
Calin Chitu
7ea78e9845 fix(lobby/native) style updates and local video fix 2022-06-10 16:07:56 +03:00
Gabriel Borlea
9383942cb9 fix(face-landmarks): filter face detections based on detection score (#11658)
* fix(face-landmarks): filter face detections based on detection score

* fix: add blank line and semi column
2022-06-10 15:19:18 +03:00
159 changed files with 2538 additions and 2098 deletions

View File

@@ -76,7 +76,8 @@ public class BroadcastAction {
OPEN_CHAT("org.jitsi.meet.OPEN_CHAT"),
CLOSE_CHAT("org.jitsi.meet.CLOSE_CHAT"),
SEND_CHAT_MESSAGE("org.jitsi.meet.SEND_CHAT_MESSAGE"),
SET_VIDEO_MUTED("org.jitsi.meet.SET_VIDEO_MUTED");
SET_VIDEO_MUTED("org.jitsi.meet.SET_VIDEO_MUTED"),
SET_CLOSED_CAPTIONS_ENABLED("org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED");
private final String action;

View File

@@ -48,4 +48,10 @@ public class BroadcastIntentHelper {
intent.putExtra("muted", muted);
return intent;
}
public static Intent buildSetClosedCaptionsEnabledIntent(boolean enabled) {
Intent intent = new Intent(BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
intent.putExtra("enabled", enabled);
return intent;
}
}

View File

@@ -95,6 +95,7 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
constants.put("CLOSE_CHAT", BroadcastAction.Type.CLOSE_CHAT.getAction());
constants.put("SEND_CHAT_MESSAGE", BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
constants.put("SET_VIDEO_MUTED", BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
constants.put("SET_CLOSED_CAPTIONS_ENABLED", BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
return constants;
}

View File

@@ -213,7 +213,7 @@ public class JitsiMeetActivity extends AppCompatActivity
protected void onConferenceJoined(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference joined: " + extraData);
// Launch the service for the ongoing notification.
JitsiMeetOngoingConferenceService.launch(this);
JitsiMeetOngoingConferenceService.launch(this, extraData);
}
protected void onConferenceTerminated(HashMap<String, Object> extraData) {

View File

@@ -17,18 +17,22 @@
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
* responsible for presenting an ongoing notification when a conference is in progress.
@@ -39,16 +43,22 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
public class JitsiMeetOngoingConferenceService extends Service
implements OngoingConferenceTracker.OngoingConferenceListener {
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
private static final String EXTRA_DATA_KEY = "extraDataKey";
private static final String EXTRA_DATA_BUNDLE_KEY = "extraDataBundleKey";
private static final String IS_AUDIO_MUTED_KEY = "isAudioMuted";
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver();
private boolean isAudioMuted;
static void launch(Context context) {
static void launch(Context context, HashMap<String, Object> extraData) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(Action.START.getName());
Bundle extraDataBundle = new Bundle();
extraDataBundle.putSerializable(EXTRA_DATA_KEY, extraData);
intent.putExtra(EXTRA_DATA_BUNDLE_KEY, extraDataBundle);
ComponentName componentName;
@@ -79,6 +89,15 @@ public class JitsiMeetOngoingConferenceService extends Service
public void onCreate() {
super.onCreate();
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
OngoingConferenceTracker.getInstance().addListener(this);
IntentFilter intentFilter = new IntentFilter();
@@ -101,37 +120,45 @@ public class JitsiMeetOngoingConferenceService extends Service
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Boolean isAudioMuted = tryParseIsAudioMuted(intent);
if (isAudioMuted != null) {
this.isAudioMuted = Boolean.parseBoolean(intent.getStringExtra("muted"));
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(OngoingNotification.NOTIFICATION_ID, notification);
}
}
final String actionName = intent.getAction();
final Action action = Action.fromName(actionName);
switch (action) {
case UNMUTE:
case MUTE:
Intent muteBroadcastIntent = BroadcastIntentHelper.buildSetAudioMutedIntent(action == Action.MUTE);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(muteBroadcastIntent);
break;
case START:
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
// When starting the service, there is no action passed in the intent
if (action != null) {
switch (action) {
case UNMUTE:
case MUTE:
Intent muteBroadcastIntent = BroadcastIntentHelper.buildSetAudioMutedIntent(action == Action.MUTE);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(muteBroadcastIntent);
break;
case HANGUP:
JitsiMeetLogger.i(TAG + " Hangup requested");
Intent hangupBroadcastIntent = BroadcastIntentHelper.buildHangUpIntent();
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(hangupBroadcastIntent);
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
break;
case HANGUP:
JitsiMeetLogger.i(TAG + " Hangup requested");
Intent hangupBroadcastIntent = BroadcastIntentHelper.buildHangUpIntent();
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(hangupBroadcastIntent);
stopSelf();
break;
default:
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
stopSelf();
break;
break;
default:
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
break;
}
}
return START_NOT_STICKY;
@@ -147,7 +174,6 @@ public class JitsiMeetOngoingConferenceService extends Service
}
public enum Action {
START(TAG + ":START"),
HANGUP(TAG + ":HANGUP"),
MUTE(TAG + ":MUTE"),
UNMUTE(TAG + ":UNMUTE");
@@ -172,6 +198,15 @@ public class JitsiMeetOngoingConferenceService extends Service
}
}
private Boolean tryParseIsAudioMuted(Intent intent) {
try {
HashMap<String, Object> extraData = (HashMap<String, Object>) intent.getBundleExtra(EXTRA_DATA_BUNDLE_KEY).getSerializable(EXTRA_DATA_KEY);
return Boolean.parseBoolean((String) extraData.get(IS_AUDIO_MUTED_KEY));
} catch (Exception ignored) {
}
return null;
}
private class BroadcastReceiver extends android.content.BroadcastReceiver {
@Override
@@ -180,10 +215,12 @@ public class JitsiMeetOngoingConferenceService extends Service
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
JitsiMeetLogger.w(TAG + " Couldn't update service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " audio muted changed");
}
}
}

View File

@@ -24,6 +24,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
import com.oney.WebRTCModule.WebRTCModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
@@ -125,18 +126,14 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
* page.
*/
public void enterPictureInPicture() {
PictureInPictureModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
PictureInPictureModule.class);
if (pipModule != null
&& pipModule.isPictureInPictureSupported()
&& !JitsiMeetActivityDelegate.arePermissionsBeingRequested()
&& this.url != null) {
try {
pipModule.enterPictureInPicture();
} catch (RuntimeException re) {
JitsiMeetLogger.e(re, "Failed to enter PiP mode");
}
try {
WebRTCModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
WebRTCModule.class);
pipModule.addDecryptors();
}
catch (Exception e) {
int a = 1;
}
}

View File

@@ -28,6 +28,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import com.oney.WebRTCModule.WebRTCModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
@@ -43,7 +44,7 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
private static final String TAG = NAME;
private static boolean isSupported;
private boolean isDisabled;
private boolean isEnabled;
public PictureInPictureModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -84,34 +85,10 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
*/
@TargetApi(Build.VERSION_CODES.O)
public void enterPictureInPicture() {
if (isDisabled) {
return;
}
if (!isSupported) {
throw new IllegalStateException("Picture-in-Picture not supported");
}
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
throw new IllegalStateException("No current Activity!");
}
JitsiMeetLogger.i(TAG + " Entering Picture-in-Picture");
PictureInPictureParams.Builder builder
= new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(1, 1));
// https://developer.android.com/reference/android/app/Activity.html#enterPictureInPictureMode(android.app.PictureInPictureParams)
//
// The system may disallow entering picture-in-picture in various cases,
// including when the activity is not visible, if the screen is locked
// or if the user has an activity pinned.
if (!currentActivity.enterPictureInPictureMode(builder.build())) {
throw new RuntimeException("Failed to enter Picture-in-Picture");
}
WebRTCModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
WebRTCModule.class);
pipModule.addDecryptors();
}
/**
@@ -123,17 +100,15 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
*/
@ReactMethod
public void enterPictureInPicture(Promise promise) {
try {
enterPictureInPicture();
promise.resolve(null);
} catch (RuntimeException re) {
promise.reject(re);
}
WebRTCModule pipModule
= ReactInstanceManagerHolder.getNativeModule(
WebRTCModule.class);
pipModule.addDecryptors();
}
@ReactMethod
public void setPictureInPictureDisabled(Boolean disabled) {
this.isDisabled = disabled;
public void setPictureInPictureEnabled(Boolean enabled) {
this.isEnabled = enabled;
}
public boolean isPictureInPictureSupported() {

View File

@@ -35,6 +35,8 @@ import com.oney.WebRTCModule.RTCVideoViewManager;
import com.oney.WebRTCModule.WebRTCModule;
import org.devio.rn.splashscreen.SplashScreenModule;
import org.webrtc.Loggable;
import org.webrtc.Logging;
import org.webrtc.SoftwareVideoDecoderFactory;
import org.webrtc.SoftwareVideoEncoderFactory;
import org.webrtc.audio.AudioDeviceModule;
@@ -57,6 +59,13 @@ class ReactInstanceManagerHolder {
*/
private static ReactInstanceManager reactInstanceManager;
private static Loggable webrtcLogger = new Loggable() {
@Override
public void onLogMessage(String message, Logging.Severity severity, String tag) {
Log.d(tag,message);
}
};
private static List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> nativeModules
= new ArrayList<>(Arrays.<NativeModule>asList(
@@ -88,6 +97,8 @@ class ReactInstanceManagerHolder {
options.setVideoDecoderFactory(new SoftwareVideoDecoderFactory());
options.setVideoEncoderFactory(new SoftwareVideoEncoderFactory());
options.setInjectableLogger(webrtcLogger);
options.setLoggingSeverity(Logging.Severity.LS_VERBOSE);
nativeModules.add(new WebRTCModule(reactContext, options));

View File

@@ -1759,12 +1759,12 @@ export default {
return Promise.reject('Cannot toggle screen sharing: not supported.');
}
if (this.isAudioOnly()) {
APP.store.dispatch(setAudioOnly(false));
}
if (toggle) {
try {
await this._switchToScreenSharing(options);
if (this.isAudioOnly()) {
APP.store.dispatch(setAudioOnly(false));
}
return;
} catch (err) {
@@ -2653,13 +2653,9 @@ export default {
// muteVideo logic in such case.
const tracks = APP.store.getState()['features/base/tracks'];
const isTrackInRedux
= Boolean(
tracks.find(
track => track.jitsiTrack
&& track.jitsiTrack.getType() === 'video'));
= Boolean(tracks.find(track => track.jitsiTrack && track.jitsiTrack.getType() === MEDIA_TYPE.VIDEO));
if (isTrackInRedux) {
if (isTrackInRedux && !this.isSharingScreen) {
this.muteVideo(audioOnly);
}

View File

@@ -295,8 +295,13 @@ var config = {
// Whether to enable live streaming or not.
// liveStreamingEnabled: false,
// Whether to enable local recording or not.
// enableLocalRecording: false,
// Local recording configuration.
// localRecording: {
// // Whether to disable local recording or not.
// disable: false,
// // Whether to notify all participants when a participant is recording locally.
// notifyAllParticipants: false
// },
// Transcription (in interface_config,
// subtitles and buttons can be configured)
@@ -788,14 +793,14 @@ var config = {
// // Enables displaying face expressions in speaker stats
// enableDisplayFaceExpressions: false,
// // Enable rtc stats for face landmarks
// enableRTCStats: false,
// // Minimum required face movement percentage threshold for sending new face centering coordinates data.
// faceCenteringThreshold: 10,
// // Milliseconds for processing a new image capture in order to detect face coordinates if they exist.
// captureInterval: 1000,
// // Maximum number of faces that can be detected from a video track.
// maxFacesDetected: 4
// captureInterval: 1000
// },
// Controls the percentage of automatic feedback shown to participants when callstats is enabled.
@@ -875,6 +880,10 @@ var config = {
// The Amplitude APP Key:
// amplitudeAPPKey: '<APP_KEY>'
// Obfuscates room name sent to analytics (amplitude, rtcstats)
// Default value is false.
// obfuscateRoomName: false,
// Configuration for the rtcstats server:
// By enabling rtcstats server every time a conference is joined the rtcstats
// module connects to the provided rtcstatsEndpoint and sends statistics regarding
@@ -949,12 +958,18 @@ var config = {
// chromeExtensionBanner: {
// // The chrome extension to be installed address
// url: 'https://chrome.google.com/webstore/detail/jitsi-meetings/kglhbbefdnlheedjiejgomgmfplipfeb',
// edgeUrl: 'https://microsoftedge.microsoft.com/addons/detail/jitsi-meetings/eeecajlpbgjppibfledfihobcabccihn',
// // Extensions info which allows checking if they are installed or not
// chromeExtensionsInfo: [
// {
// id: 'kglhbbefdnlheedjiejgomgmfplipfeb',
// path: 'jitsi-logo-48x48.png'
// },
// // Edge extension info
// {
// id: 'eeecajlpbgjppibfledfihobcabccihn',
// path: 'jitsi-logo-48x48.png'
// }
// ]
// },

View File

@@ -8,7 +8,8 @@ import { LoginDialog } from './react/features/authentication/components';
import { isTokenAuthEnabled } from './react/features/authentication/functions';
import {
connectionEstablished,
connectionFailed
connectionFailed,
constructOptions
} from './react/features/base/connection/actions';
import { openDialog } from './react/features/base/dialog/actions';
import { setJWT } from './react/features/base/jwt';
@@ -19,7 +20,10 @@ import {
import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-meet/functions';
import { getCustomerDetails } from './react/features/jaas/actions.any';
import { isVpaasMeeting, getJaasJWT } from './react/features/jaas/functions';
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
import {
setPrejoinDisplayNameRequired,
setPrejoinPageVisibility
} from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
/**
@@ -81,12 +85,10 @@ function checkForAttachParametersAndConnect(id, password, connection) {
* Try to open connection using provided credentials.
* @param {string} [id]
* @param {string} [password]
* @param {string} [roomName]
* @returns {Promise<JitsiConnection>} connection if
* everything is ok, else error.
*/
export async function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
export async function connect(id, password) {
const state = APP.store.getState();
let { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
@@ -100,19 +102,7 @@ export async function connect(id, password, roomName) {
}
}
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption
// that this code executes only on web browsers/electron. This needs to be changed when mobile and web are unified.
let serviceUrl = connectionConfig.websocket || connectionConfig.bosh;
serviceUrl += `?room=${roomName}`;
connectionConfig.serviceUrl = serviceUrl;
if (connectionConfig.websocketKeepAliveUrl) {
connectionConfig.websocketKeepAliveUrl += `?room=${roomName}`;
}
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, connectionConfig);
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, constructOptions(state));
if (config.iAmRecorder) {
connection.addFeature(DISCO_JIBRI_FEATURE);
@@ -257,6 +247,7 @@ function requestAuth(roomName) {
resolve(connection);
};
APP.store.dispatch(setPrejoinPageVisibility(false));
APP.store.dispatch(
openDialog(LoginDialog, { onSuccess,
roomName })

View File

@@ -28,8 +28,21 @@
}
.local-recording-warning {
margin-top: 4px;
margin-top: 8px;
display: block;
font-size: 14px;
line-height: 20px;
padding: 8px 16px;
&.text {
color: #fff;
background-color: #3D3D3D;
}
&.notification {
color: #040404;
background-color: #F8AE1A;
}
}
.recording-switch-disabled {
@@ -46,7 +59,7 @@
border-radius: 4px;
height: 40px;
justify-content: center;
width: 56px;
width: 42px;
}
.cloud-content-recording-icon-container {
@@ -58,7 +71,7 @@
}
.jitsi-recording-header {
margin-bottom: 32px;
margin-bottom: 16px;
}
.jitsi-content-recording-icon-container-with-switch {

View File

@@ -1,5 +1,4 @@
@import 'lobby';
@import 'premeeting-screens';
@import 'prejoin';
@import 'prejoin-dialog';
@import 'prejoin-third-party';

View File

@@ -1,118 +0,0 @@
.prejoin-dialog {
background: #1C2025;
box-shadow: 0px 2px 20px rgba(0, 0, 0, 0.5);
border-radius: 5px;
color: #fff;
height: 400px;
width: 375px;
&--small {
height: 300;
width: 400;
}
&-label {
font-size: 15px;
line-height: 24px;
&-num {
background: #2b3b4b;
border: 1px solid #A4B8D1;
border-radius: 50%;
color: #fff;
display: inline-block;
height: 24px;
margin-right: 8px;
width: 24px;
}
}
&-container {
align-items: center;
background: rgba(0,0,0,0.6);
display: flex;
height: 100vh;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100vw;
z-index: 3;
}
&-flag {
display: inline-block;
margin-right: 8px;
transform: scale(1.2);
}
&-title {
display: inline-block;
font-size: 24px;
line-height: 32px;
}
&-icon {
cursor: pointer;
> svg {
fill: #A4B8D1;
}
}
&-btn {
width: 309px;
}
&-dialin-container {
text-align: center;
}
&-delimiter {
background: #5f6266;
border: 0;
height: 1px;
margin: 0;
padding: 0;
width: 100%;
&-container {
margin: 16px 0 24px 0;
position: relative;
}
&-txt-container {
position: absolute;
text-align: center;
top: -8px;
width: 100%;
}
&-txt {
background: #1C2025;
color: #5f6266;
font-size: 11px;
text-transform: uppercase;
padding: 0 8px;
}
}
.prejoin-dialog-btn.primary,
.action-btn.prejoin-dialog-btn.text {
width: 310px;
}
}
.prejoin-dialog-callout {
padding: 16px;
&-header {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
}
&-picker {
margin: 8px 0 16px 0;
}
}

View File

@@ -749,4 +749,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: bef1335067eaa4e8c558b1248f8ab3948de855bc
COCOAPODS: 1.11.2
COCOAPODS: 1.11.3

View File

@@ -27,5 +27,6 @@
- (void)closeChat;
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
- (void)sendSetVideoMuted:(BOOL)muted;
- (void)sendSetClosedCaptionsEnabled:(BOOL)enabled;
@end

View File

@@ -27,6 +27,7 @@ static NSString * const openChatAction = @"org.jitsi.meet.OPEN_CHAT";
static NSString * const closeChatAction = @"org.jitsi.meet.CLOSE_CHAT";
static NSString * const sendChatMessageAction = @"org.jitsi.meet.SEND_CHAT_MESSAGE";
static NSString * const setVideoMutedAction = @"org.jitsi.meet.SET_VIDEO_MUTED";
static NSString * const setClosedCaptionsEnabledAction = @"org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED";
@implementation ExternalAPI
@@ -49,7 +50,8 @@ RCT_EXPORT_MODULE();
@"OPEN_CHAT": openChatAction,
@"CLOSE_CHAT": closeChatAction,
@"SEND_CHAT_MESSAGE": sendChatMessageAction,
@"SET_VIDEO_MUTED" : setVideoMutedAction
@"SET_VIDEO_MUTED" : setVideoMutedAction,
@"SET_CLOSED_CAPTIONS_ENABLED": setClosedCaptionsEnabledAction
};
};
@@ -73,7 +75,8 @@ RCT_EXPORT_MODULE();
openChatAction,
closeChatAction,
sendChatMessageAction,
setVideoMutedAction
setVideoMutedAction,
setClosedCaptionsEnabledAction
];
}
@@ -205,5 +208,10 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:setVideoMutedAction body:data];
}
- (void)sendSetClosedCaptionsEnabled:(BOOL)enabled {
NSDictionary *data = @{ @"enabled": [NSNumber numberWithBool:enabled]};
[self sendEventWithName:setClosedCaptionsEnabledAction body:data];
}
@end

View File

@@ -45,5 +45,6 @@
- (void)closeChat;
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)setVideoMuted:(BOOL)muted;
- (void)setClosedCaptionsEnabled:(BOOL)enabled;
@end

View File

@@ -160,6 +160,11 @@ static void initializeViewsMap() {
[externalAPI sendSetVideoMuted:muted];
}
- (void)setClosedCaptionsEnabled:(BOOL)enabled {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetClosedCaptionsEnabled:enabled];
}
#pragma mark Private methods
/**

View File

@@ -116,6 +116,7 @@
},
"chromeExtensionBanner": {
"buttonText": "نزِّل إضافة متصفح كروم",
"buttonTextEdge": "قم بتثبيت ملحق Edge",
"close": "إغلق",
"dontShowAgain": "لا ترني هذه مرة أخرى",
"installExtensionText": "نزِّل الإضافة للدمج مع رزنامة غوغل ورزنامة أوفيس 365"
@@ -207,6 +208,9 @@
"selectADevice": "اختر جهازًا",
"testAudio": "اختبر الصوت"
},
"dialIn": {
"screenTitle": "ملخص الطلب"
},
"dialOut": {
"statusMessage": "{{status}} الآن"
},
@@ -814,6 +818,7 @@
"initiated": "بدأ الاتصال",
"joinAudioByPhone": "انضم مع صوت من الجوال",
"joinMeeting": "انضم للمُلتقى",
"joinMeetingInLowBandwidthMode": "الانضمام في وضع النطاق الترددي المنخفض",
"joinWithoutAudio": "انضم دون صوت",
"keyboardShortcuts": "تفعيل اختصارات لوحة المفاتيح",
"linkCopied": "نُسِخ الرابط",
@@ -948,6 +953,7 @@
"playSounds": "تشغيل الصوت عند:",
"reactions": "ردود فعل المُلتقى",
"sameAsSystem": "مثل النظام ({{label}})",
"screenTitle": "إعدادات",
"selectAudioOutput": "خرج الصوت",
"selectCamera": "الكاميرا",
"selectMic": "المجهار (المايكروفون)",
@@ -973,6 +979,7 @@
"disableCrashReportingWarning": "أمتأكد من تعطيل تقارير الأعطال التقنية؟ ستسري الإعدادات الجديدة بعد إعادة تشغيل التطبيق",
"disableP2P": "تعطيل وضع واحد شخص-لشخص",
"displayName": "عرض الاسم",
"displayNamePlaceholderText": "على سبيل المثال: علي الحيدري",
"email": "البريد الإلكتروني",
"header": "الإعدادات",
"profileSection": "الملف الشخصي",

View File

@@ -116,6 +116,7 @@
},
"chromeExtensionBanner": {
"buttonText": "Instalar extensão do Chrome",
"buttonTextEdge": "Instalar extensão do Edge",
"close": "Fechar",
"dontShowAgain": "Não me mostre isto outra vez",
"installExtensionText": "Instalar a extensão para a integração Google Calendar e Office 365"
@@ -194,7 +195,7 @@
"unsupportedBrowser": "Parece que está a usar um browser que não suportamos."
},
"defaultLink": "ex.: {{url}}",
"defaultNickname": "ex.: João Pedro",
"defaultNickname": "ex.: João Dias",
"deviceError": {
"cameraError": "Falha ao aceder à sua câmara",
"cameraPermission": "Erro ao obter permissão para a câmara",
@@ -207,6 +208,9 @@
"selectADevice": "Selecione um dispositivo",
"testAudio": "Tocar um som de teste"
},
"dialIn": {
"screenTitle": "Resumo da marcação"
},
"dialOut": {
"statusMessage": "está agora {{status}}"
},
@@ -288,7 +292,7 @@
"lockRoom": "Adicionar reunião $t(lockRoomPassword)",
"lockTitle": "Bloqueio falhado",
"login": "Entrar",
"logoutQuestion": "Tem a certeza de que quer terminar a sessão e interromper a conferência?",
"logoutQuestion": "Tem a certeza de que quer terminar a sessão e sair da conferência?",
"logoutTitle": "Sair",
"maxUsersLimitReached": "O limite para o número máximo de participantes foi atingido. A conferência está cheia. Por favor contacte o proprietário da reunião ou tente novamente mais tarde!",
"maxUsersLimitReachedTitle": "Limite máximo de participantes atingido",
@@ -657,6 +661,8 @@
"linkToSalesforceKey": "Ligar esta reunião",
"linkToSalesforceProgress": "A ligar a reunião à Salesforce...",
"linkToSalesforceSuccess": "A reunião foi ligada à Salesforce",
"localRecordingStarted": "{{name}} iniciou uma gravação local.",
"localRecordingStopped": "{{name}} parou uma gravação local.",
"me": "Eu",
"moderationInEffectCSDescription": "Por favor, levantem a mão se quiserem partilhar o vosso ecrã.",
"moderationInEffectCSTitle": "A partilha de ecrã é bloqueada pelo moderador",
@@ -812,6 +818,7 @@
"initiated": "Chamada iniciada",
"joinAudioByPhone": "Entrar com o áudio do telefone",
"joinMeeting": "Entrar na reunião",
"joinMeetingInLowBandwidthMode": "Entrar em modo de baixa largura de banda",
"joinWithoutAudio": "Entrar sem áudio",
"keyboardShortcuts": "Ativar os atalhos de teclado",
"linkCopied": "Link copiado para a área de transferência",
@@ -887,6 +894,7 @@
"limitNotificationDescriptionWeb": "Devido à grande procura, a sua gravação será limitada a {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "Gerámos um link para a sua gravação.",
"live": "DIRETO",
"localRecordingWarning": "Certifique-se de seleccionar o separador actual a fim de utilizar o vídeo e áudio correctos. A gravação está actualmente limitada a 1 GB, o que é cerca de 100 minutos.",
"loggedIn": "Conectado como {{userName}}",
"off": "Gravação parada",
"offBy": "{{name}} parou a gravação",
@@ -894,6 +902,7 @@
"onBy": "{{name}} iniciou a gravação",
"pending": "Preparando para gravar a reunião...",
"rec": "REC",
"saveLocalRecording": "Guardar ficheiro de gravação localmente",
"serviceDescription": "Sua gravação será salva pelo serviço de gravação",
"serviceDescriptionCloud": "Gravação na nuvem",
"serviceDescriptionCloudInfo": "As reuniões gravadas são automaticamente apagadas 24h após a hora de gravação.",
@@ -901,11 +910,12 @@
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
"signIn": "Entrar",
"signOut": "Sair",
"surfaceError": "Por favor, seleccione o separador actual.",
"unavailable": "Oops! O {{serviceName}} está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"unavailableTitle": "Gravação indisponível",
"uploadToCloud": "Enviar para a nuvem"
},
"screenshareDisplayName": "Ecrã do {{name}}",
"screenshareDisplayName": "Ecrã de {{name}}",
"sectionList": {
"pullToRefresh": "Puxe para atualizar"
},
@@ -943,6 +953,7 @@
"playSounds": "Reproduzir som quando",
"reactions": "Expressarem uma reação",
"sameAsSystem": "O mesmo que o sistema ({{label}})",
"screenTitle": "Definições",
"selectAudioOutput": "Saída de áudio",
"selectCamera": "Câmara",
"selectMic": "Microfone",
@@ -968,6 +979,7 @@
"disableCrashReportingWarning": "Tem a certeza de que quer desativar o relatório de falhas? A configuração será aplicada depois de reiniciar a aplicação.",
"disableP2P": "Desactivar o modo Peer-To-Peer",
"displayName": "Nome de exibição",
"displayNamePlaceholderText": "Ex: João Dias",
"email": "Email",
"header": "Configurações",
"profileSection": "Perfil",
@@ -1077,7 +1089,7 @@
"tileView": "Mudar a vista em quadrícula",
"toggleCamera": "Mudar a câmara",
"toggleFilmstrip": "Mudar a película de filme",
"undock": "Desencaixar numa janela separada",
"undock": "Soltar numa janela separada",
"videoblur": "Mudar o desfoque de vídeo",
"videomute": "Iniciar / Parar câmara"
},
@@ -1163,7 +1175,7 @@
"talkWhileMutedPopup": "Está a tentar falar? Está com o microfone desativado.",
"tileViewToggle": "Mudar para vista em quadrícula",
"toggleCamera": "Mudar a câmara",
"undock": "Desencaixar numa janela separada",
"undock": "Soltar numa janela separada",
"videoSettings": "Definições de vídeo",
"videomute": "Iniciar / Parar câmara"
},

View File

@@ -116,6 +116,7 @@
},
"chromeExtensionBanner": {
"buttonText": "Install Chrome Extension",
"buttonTextEdge": "Install Edge Extension",
"close": "Close",
"dontShowAgain": "Dont show me this again",
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration"
@@ -207,6 +208,9 @@
"selectADevice": "Select a device",
"testAudio": "Play a test sound"
},
"dialIn": {
"screenTitle": "Dial-in summary"
},
"dialOut": {
"statusMessage": "is now {{status}}"
},
@@ -814,6 +818,7 @@
"initiated": "Call initiated",
"joinAudioByPhone": "Join with phone audio",
"joinMeeting": "Join meeting",
"joinMeetingInLowBandwidthMode": "Join in low bandwidth mode",
"joinWithoutAudio": "Join without audio",
"keyboardShortcuts": "Enable Keyboard shortcuts",
"linkCopied": "Link copied to clipboard",
@@ -889,6 +894,7 @@
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "We have generated a link to your recording.",
"live": "LIVE",
"localRecordingNoNotificationWarning": "The recording will not be announced to other participants. You will need to let them know that the meeting is recorded.",
"localRecordingWarning": "Make sure you select the current tab in order to use the right video and audio. The recording is currently limited to 1GB, which is around 100 minutes.",
"loggedIn": "Logged in as {{userName}}",
"off": "Recording stopped",
@@ -948,6 +954,7 @@
"playSounds": "Play sound on",
"reactions": "Meeting reactions",
"sameAsSystem": "Same as system ({{label}})",
"screenTitle": "Settings",
"selectAudioOutput": "Audio output",
"selectCamera": "Camera",
"selectMic": "Microphone",
@@ -973,6 +980,7 @@
"disableCrashReportingWarning": "Are you sure you want to disable crash reporting? The setting will be applied after you restart the app.",
"disableP2P": "Disable Peer-To-Peer mode",
"displayName": "Display name",
"displayNamePlaceholderText": "Eg: John Doe",
"email": "Email",
"header": "Settings",
"profileSection": "Profile",

View File

@@ -1695,6 +1695,19 @@ class API {
});
}
/**
* Notify the external application that the state of the participants pane changed.
*
* @param {boolean} open - Wether the panel is open or not.
* @returns {void}
*/
notifyParticipantsPaneToggled(open) {
this._sendEvent({
name: 'participants-pane-toggled',
open
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -126,6 +126,7 @@ const events = {
'participant-kicked-out': 'participantKickedOut',
'participant-left': 'participantLeft',
'participant-role-changed': 'participantRoleChanged',
'participants-pane-toggled': 'participantsPaneToggled',
'password-required': 'passwordRequired',
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
@@ -301,6 +302,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* the participant opening the meeting.
* @param {string} [options.e2eeKey] - The key used for End-to-End encryption.
* THIS IS EXPERIMENTAL.
* @param {string} [options.release] - The key used for specifying release if enabled on the backend.
*/
constructor(domain, ...args) {
super();
@@ -317,7 +319,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
invitees,
devices,
userInfo,
e2eeKey
e2eeKey,
release
} = parseArguments(args);
const localStorageContent = jitsiLocalStorage.getItem('jitsiLocalStorage');
@@ -332,7 +335,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
userInfo,
appData: {
localStorageContent
}
},
release
});
this._createIFrame(height, width, onload);
this._transport = new Transport({

View File

@@ -17,6 +17,7 @@ import {
import { getReplaceParticipant } from '../../../react/features/base/config/functions';
import { isDialogOpen } from '../../../react/features/base/dialog';
import { setJWT } from '../../../react/features/base/jwt';
import { setPrejoinPageVisibility } from '../../../react/features/prejoin';
import UIUtil from '../util/UIUtil';
import ExternalLoginDialog from './LoginDialog';
@@ -180,6 +181,7 @@ function authenticate(room: Object, lockPassword: string) {
if (isTokenAuthEnabled(config) || room.isExternalAuthEnabled()) {
doExternalAuth(room, lockPassword);
} else {
APP.store.dispatch(setPrejoinPageVisibility(false));
APP.store.dispatch(openLoginDialog());
}
}

93
package-lock.json generated
View File

@@ -72,8 +72,9 @@
"jquery": "3.5.1",
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1450.0.0+462996fc/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1457.0.0+ad75454f/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.2",
"moment-duration-format": "2.2.2",
@@ -93,7 +94,7 @@
"react-native-collapsible": "1.6.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "9.2.1",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.1.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
@@ -141,7 +142,9 @@
"@babel/preset-react": "7.16.0",
"@babel/runtime": "7.16.0",
"@jitsi/eslint-config": "4.0.0",
"@types/react": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-redux": "7.1.24",
"@types/uuid": "8.3.4",
"babel-loader": "8.2.3",
"babel-plugin-optional-require": "0.3.1",
@@ -5382,6 +5385,16 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/http-proxy": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz",
@@ -5478,9 +5491,9 @@
"dev": true
},
"node_modules/@types/react": {
"version": "17.0.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz",
"integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==",
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -5496,6 +5509,18 @@
"@types/react": "*"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.24",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
"dev": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
@@ -11625,6 +11650,11 @@
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.6.1.tgz",
"integrity": "sha512-lyUTXOqMEaA9mm38mHxbTo83WsYAvMJm850kxJcRno3T2qL+e40B2G89E0/4r9TdAeB3jN0TdSVp/VHNI6/WyQ=="
},
"node_modules/js-sha512": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
"integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -12138,8 +12168,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1450.0.0+462996fc/lib-jitsi-meet.tgz",
"integrity": "sha512-CY2qdbD7U0kZBLQyMVrqbtb2hf9fkkBMyLvSsks35wQvYK2mqZS9bkBjU2VUhJxcw6GxdIIWJGZluf2KQukG3g==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1457.0.0+ad75454f/lib-jitsi-meet.tgz",
"integrity": "sha512-K+dJWt6nlAXtKE/WhR8pkf3vga+52tJSpTWX/fxOTKn8IJKTlj46gSC2CosAfwyG4P6ISzeFnTvVC3E+qbxbUg==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -15392,9 +15422,10 @@
}
},
"node_modules/react-native-dialog": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-9.2.1.tgz",
"integrity": "sha512-UNnGFTpH0cX16cJZLFq9/TAZH1+B2vzJrQL1mUaSqZjV+sFTpUB1WvghJZxPwi52v587kJpfKN7oPfWaXAu+YQ==",
"version": "9.2.2",
"resolved": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"integrity": "sha512-MKbuBbovO8eGiAM9i6o0nrdBXivhRpzPQ+aVBXGJEPMH7RrCSNUKaCoEpkjfGHlTxjZimi6WjDCjjzCRSHlV1A==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.63.0"
}
@@ -24015,6 +24046,16 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
"integrity": "sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
"dev": true,
"requires": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"@types/http-proxy": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz",
@@ -24111,9 +24152,9 @@
"dev": true
},
"@types/react": {
"version": "17.0.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz",
"integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==",
"version": "17.0.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -24129,6 +24170,18 @@
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.24",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.24.tgz",
"integrity": "sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.4.tgz",
@@ -28897,6 +28950,11 @@
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.6.1.tgz",
"integrity": "sha512-lyUTXOqMEaA9mm38mHxbTo83WsYAvMJm850kxJcRno3T2qL+e40B2G89E0/4r9TdAeB3jN0TdSVp/VHNI6/WyQ=="
},
"js-sha512": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz",
"integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -29311,8 +29369,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1450.0.0+462996fc/lib-jitsi-meet.tgz",
"integrity": "sha512-CY2qdbD7U0kZBLQyMVrqbtb2hf9fkkBMyLvSsks35wQvYK2mqZS9bkBjU2VUhJxcw6GxdIIWJGZluf2KQukG3g==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1457.0.0+ad75454f/lib-jitsi-meet.tgz",
"integrity": "sha512-K+dJWt6nlAXtKE/WhR8pkf3vga+52tJSpTWX/fxOTKn8IJKTlj46gSC2CosAfwyG4P6ISzeFnTvVC3E+qbxbUg==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -31883,9 +31941,8 @@
"integrity": "sha512-92676ZWHZHsPM/EW1ulgb2MuVfjYfMWRTWMbLcrCsipkcMaZ9Traz5mpsnCS7KZpsOksnvUinzDIjsct2XGc6Q=="
},
"react-native-dialog": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/react-native-dialog/-/react-native-dialog-9.2.1.tgz",
"integrity": "sha512-UNnGFTpH0cX16cJZLFq9/TAZH1+B2vzJrQL1mUaSqZjV+sFTpUB1WvghJZxPwi52v587kJpfKN7oPfWaXAu+YQ=="
"version": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"integrity": "sha512-MKbuBbovO8eGiAM9i6o0nrdBXivhRpzPQ+aVBXGJEPMH7RrCSNUKaCoEpkjfGHlTxjZimi6WjDCjjzCRSHlV1A=="
},
"react-native-gesture-handler": {
"version": "2.1.0",

View File

@@ -77,8 +77,9 @@
"jquery": "3.5.1",
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1450.0.0+462996fc/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1457.0.0+ad75454f/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.2",
"moment-duration-format": "2.2.2",
@@ -98,7 +99,7 @@
"react-native-collapsible": "1.6.0",
"react-native-default-preference": "1.4.4",
"react-native-device-info": "8.4.8",
"react-native-dialog": "9.2.1",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.1.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
@@ -146,7 +147,9 @@
"@babel/preset-react": "7.16.0",
"@babel/runtime": "7.16.0",
"@jitsi/eslint-config": "4.0.0",
"@types/react": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-redux": "7.1.24",
"@types/uuid": "8.3.4",
"babel-loader": "8.2.3",
"babel-plugin-optional-require": "0.3.1",
@@ -189,5 +192,9 @@
"postinstall": "patch-package --error-on-fail && jetify",
"validate": "npm ls",
"start": "make dev"
},
"resolutions": {
"@types/react": "17.0.14",
"@types/react-dom": "17.0.14"
}
}

View File

@@ -1,118 +0,0 @@
diff --git a/node_modules/react-native-dialog/lib/Button.js b/node_modules/react-native-dialog/lib/Button.js
index 19eeb22..b8a66f4 100644
--- a/node_modules/react-native-dialog/lib/Button.js
+++ b/node_modules/react-native-dialog/lib/Button.js
@@ -50,7 +50,7 @@ const buildStyles = (isDark) => StyleSheet.create({
backgroundColor: "transparent",
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "link_text_dark" : "link_text_dark_light"}`),
+ color: isDark ? '#BFC7C7C7' : '#BF727272',
textAlign: "center",
backgroundColor: "transparent",
padding: 8,
diff --git a/node_modules/react-native-dialog/lib/CodeInput.js b/node_modules/react-native-dialog/lib/CodeInput.js
index eceae56..cc4339d 100644
--- a/node_modules/react-native-dialog/lib/CodeInput.js
+++ b/node_modules/react-native-dialog/lib/CodeInput.js
@@ -97,7 +97,7 @@ const buildStyles = (isDark) => StyleSheet.create({
color: PlatformColor("label"),
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
+ color: isDark ? '#FAFAFA' : '#212121',
fontSize: 20,
},
default: {},
@@ -107,7 +107,7 @@ const buildStyles = (isDark) => StyleSheet.create({
color: PlatformColor("label"),
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
+ color: isDark ? '#FAFAFA' : '#212121',
fontSize: 14,
},
default: {},
diff --git a/node_modules/react-native-dialog/lib/Container.js b/node_modules/react-native-dialog/lib/Container.js
index 69e3764..d7569fb 100644
--- a/node_modules/react-native-dialog/lib/Container.js
+++ b/node_modules/react-native-dialog/lib/Container.js
@@ -82,7 +82,7 @@ DialogContainer.propTypes = {
useNativeDriver: PropTypes.bool,
children: PropTypes.node.isRequired,
};
-const buildStyles = () => StyleSheet.create({
+const buildStyles = (isDark) => StyleSheet.create({
centeredView: {
marginTop: 22,
},
@@ -103,7 +103,7 @@ const buildStyles = () => StyleSheet.create({
overflow: "hidden",
},
android: {
- backgroundColor: PlatformColor("?attr/colorBackgroundFloating"),
+ backgroundColor: isDark ? '#212121' : '#FFFFFF',
flexDirection: "column",
borderRadius: 3,
padding: 16,
diff --git a/node_modules/react-native-dialog/lib/Description.js b/node_modules/react-native-dialog/lib/Description.js
index 2da9ed3..248ac2f 100644
--- a/node_modules/react-native-dialog/lib/Description.js
+++ b/node_modules/react-native-dialog/lib/Description.js
@@ -28,7 +28,7 @@ const buildStyles = (isDark) => StyleSheet.create({
marginTop: 4,
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "secondary_text_dark" : "secondary_text_light"}`),
+ color: isDark ? '#C7C7C7' : '#727272',
fontSize: 16,
marginTop: 10,
},
diff --git a/node_modules/react-native-dialog/lib/Input.js b/node_modules/react-native-dialog/lib/Input.js
index b33a1a0..063d7f8 100644
--- a/node_modules/react-native-dialog/lib/Input.js
+++ b/node_modules/react-native-dialog/lib/Input.js
@@ -48,7 +48,7 @@ const buildStyles = (isDark) => StyleSheet.create({
color: PlatformColor("label"),
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
+ color: isDark ? '#FAFAFA' : '#212121',
fontSize: 14,
},
default: {},
@@ -58,7 +58,7 @@ const buildStyles = (isDark) => StyleSheet.create({
color: PlatformColor("label"),
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
+ color: isDark ? '#FAFAFA' : '#212121',
marginLeft: -4,
paddingLeft: 4,
},
diff --git a/node_modules/react-native-dialog/lib/Switch.js b/node_modules/react-native-dialog/lib/Switch.js
index 26a05ca..05114fa 100644
--- a/node_modules/react-native-dialog/lib/Switch.js
+++ b/node_modules/react-native-dialog/lib/Switch.js
@@ -52,7 +52,7 @@ const buildStyles = (isDark) => StyleSheet.create({
flex: 1,
paddingRight: 8,
fontSize: 16,
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
+ color: isDark ? '#FAFAFA' : '#212121',
},
default: {},
}),
diff --git a/node_modules/react-native-dialog/lib/Title.js b/node_modules/react-native-dialog/lib/Title.js
index 1c6fd87..b5511cc 100644
--- a/node_modules/react-native-dialog/lib/Title.js
+++ b/node_modules/react-native-dialog/lib/Title.js
@@ -28,7 +28,7 @@ const buildStyles = (isDark) => StyleSheet.create({
fontWeight: "600",
},
android: {
- color: PlatformColor(`@android:color/${isDark ? "primary_text_dark" : "primary_text_light"}`),
+ color: isDark ? '#FAFAFA' : '#212121',
fontWeight: "500",
fontSize: 18,
},

View File

@@ -2,6 +2,7 @@
import { API_ID } from '../../../modules/API/constants';
import { getName as getAppName } from '../app/functions';
import { getAnalyticsRoomName } from '../base/conference';
import {
checkChromeExtensionsInstalled,
isMobileBrowser
@@ -155,7 +156,9 @@ export async function createHandlers({ getState }: { getState: Function }) {
* @param {Array<Object>} handlers - The analytics handlers.
* @returns {void}
*/
export function initAnalytics({ getState }: { getState: Function }, handlers: Array<Object>) {
export function initAnalytics(store: Store, handlers: Array<Object>) {
const { getState, dispatch } = store;
if (!isAnalyticsEnabled(getState) || handlers.length === 0) {
return;
}
@@ -166,7 +169,6 @@ export function initAnalytics({ getState }: { getState: Function }, handlers: Ar
deploymentInfo
} = config;
const { group, server } = state['features/base/jwt'];
const roomName = state['features/base/conference'].room;
const { locationURL = {} } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
const permanentProperties = {};
@@ -204,7 +206,7 @@ export function initAnalytics({ getState }: { getState: Function }, handlers: Ar
}
analytics.addPermanentProperties(permanentProperties);
analytics.setConferenceName(roomName);
analytics.setConferenceName(getAnalyticsRoomName(state, dispatch));
// Set the handlers last, since this triggers emptying of the cache
analytics.setAnalyticsHandlers(handlers);

View File

@@ -15,11 +15,17 @@ import { connect, disconnect, setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet/functions.native';
import { createDesiredLocalTracks } from '../base/tracks';
import {
appendURLParam,
getBackendSafeRoomName,
parseURIString,
parseURLParams,
toURLString
} from '../base/util';
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
import { isPrejoinPageEnabled } from '../mobile/navigation/functions';
import {
goBackToRoot,
navigateRoot
} from '../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { setFatalError } from '../overlay';
@@ -86,7 +92,11 @@ export function appNavigate(uri: ?string) {
let url = `${baseURL}config.js`;
// XXX In order to support multiple shards, tell the room to the deployment.
room && (url += `?room=${getBackendSafeRoomName(room)}`);
room && (url = appendURLParam(url, 'room', getBackendSafeRoomName(room)));
const { release } = parseURLParams(location, true, 'search');
release && (url = appendURLParam(url, 'release', release));
let config;
@@ -128,7 +138,15 @@ export function appNavigate(uri: ?string) {
if (room) {
dispatch(createDesiredLocalTracks());
dispatch(connect());
if (isPrejoinPageEnabled(getState())) {
navigateRoot(screen.preJoin);
} else {
dispatch(connect());
navigateRoot(screen.conference.root);
}
} else {
goBackToRoot(getState(), dispatch);
}
};
}

View File

@@ -15,8 +15,10 @@ import {
import { setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet/functions.web';
import {
appendURLParam,
getBackendSafeRoomName,
parseURIString
parseURIString,
parseURLParams
} from '../base/util';
import { isVpaasMeeting } from '../jaas/functions';
import {
@@ -93,7 +95,11 @@ export function appNavigate(uri: ?string) {
let url = `${baseURL}config.js`;
// XXX In order to support multiple shards, tell the room to the deployment.
room && (url += `?room=${getBackendSafeRoomName(room)}`);
room && (url = appendURLParam(url, 'room', getBackendSafeRoomName(room)));
const { release } = parseURLParams(location, true, 'search');
release && (url = appendURLParam(url, 'release', release));
let config;

View File

@@ -1,14 +1,14 @@
// @flow
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { FullWindowOverlay } from 'react-native-screens';
import SplashScreen from 'react-native-splash-screen';
import { DialogContainer } from '../../base/dialog';
import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
import { updateFlags } from '../../base/flags/actions';
import { CALL_INTEGRATION_ENABLED, SERVER_URL_CHANGE_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { Platform } from '../../base/react';
import { DimensionsDetector, clientResized, setSafeAreaInsets } from '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import { _getRouteToRender } from '../getRouteToRender.native';
@@ -23,6 +23,11 @@ import '../reducers';
declare var __DEV__;
const DialogContainerWrapper = Platform.select({
ios: FullWindowOverlay,
default: View
});
/**
* The type of React {@code Component} props of {@link App}.
*/
@@ -240,7 +245,12 @@ export class App extends AbstractApp {
*/
_renderDialogContainer() {
return (
<DialogContainer />
<DialogContainerWrapper
pointerEvents = 'box-none'
style = { StyleSheet.absoluteFill }>
<BottomSheetContainer />
<DialogContainer />
</DialogContainerWrapper>
);
}
}

View File

@@ -7,7 +7,8 @@ import { toState } from '../base/redux';
import { Conference } from '../conference';
import { getDeepLinkingPage } from '../deep-linking';
import { UnsupportedDesktopBrowser } from '../unsupported-browser';
import { BlankPage, isWelcomePageUserEnabled, WelcomePage } from '../welcome';
import { BlankPage, WelcomePage } from '../welcome';
import { isWelcomePageEnabled } from '../welcome/functions';
/**
* Determines which route is to be rendered in order to depict a specific Redux
@@ -72,7 +73,7 @@ function _getWebConferenceRoute(state) {
function _getWebWelcomePageRoute(state) {
const route = _getEmptyRoute();
if (isWelcomePageUserEnabled(state)) {
if (isWelcomePageEnabled(state)) {
if (isSupportedBrowser()) {
route.component = WelcomePage;
} else {

View File

@@ -36,6 +36,7 @@ import '../large-video/middleware';
import '../lobby/middleware';
import '../notifications/middleware';
import '../overlay/middleware';
import '../participants-pane/middleware';
import '../polls/middleware';
import '../reactions/middleware';
import '../recent-list/middleware';

View File

@@ -29,14 +29,11 @@ export function authenticateAndUpgradeRole(
id: string,
password: string,
conference: Object) {
return (dispatch: Dispatch<any>, getState: Function) => {
const { password: roomPassword }
= getState()['features/base/conference'];
return (dispatch: Dispatch<any>) => {
const process
= conference.authenticateAndUpgradeRole({
id,
password,
roomPassword,
onLoginSuccessful() {
// When the login succeeds, the process has completed half

View File

@@ -1,23 +1,15 @@
/* @flow */
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import Dialog from 'react-native-dialog';
import { connect as reduxConnect } from 'react-redux';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { toJid } from '../../../base/connection';
import { connect } from '../../../base/connection/actions.native';
import { _abstractMapStateToProps } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
import type { StyleType } from '../../../base/styles';
import { authenticateAndUpgradeRole, cancelLogin } from '../../actions.native';
// Register styles.
import './styles';
/**
* The type of the React {@link Component} props of {@link LoginDialog}.
*/
@@ -44,22 +36,12 @@ type Props = {
*/
_error: Object,
/**
* Extra handler for cancel functionality.
*/
_onCancel: Function,
/**
* The progress in the floating range between 0 and 1 of the authenticating
* and upgrading the role of the local participant/user.
*/
_progress: number,
/**
* The color-schemed stylesheet of this feature.
*/
_styles: StyleType,
/**
* Redux store dispatch method.
*/
@@ -68,12 +50,7 @@ type Props = {
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Override the default visibility.
*/
visible: boolean
t: Function
};
/**
@@ -120,10 +97,6 @@ type State = {
* of the configuration parameters.
*/
class LoginDialog extends Component<Props, State> {
static defaultProps = {
visible: true
};
/**
* Initializes a new LoginDialog instance.
*
@@ -154,42 +127,40 @@ class LoginDialog extends Component<Props, State> {
render() {
const {
_connecting: connecting,
t,
visible
t
} = this.props;
return (
<View>
<Dialog.Container
visible = { visible }>
<Dialog.Title>
{ t('dialog.login') }
</Dialog.Title>
<Dialog.Input
autoCapitalize = { 'none' }
autoCorrect = { false }
onChangeText = { this._onUsernameChange }
placeholder = { 'user@domain.com' }
spellCheck = { false }
value = { this.state.username } />
<Dialog.Input
autoCapitalize = { 'none' }
onChangeText = { this._onPasswordChange }
placeholder = { t('dialog.userPassword') }
secureTextEntry = { true }
value = { this.state.password } />
<Dialog.Description>
{ this._renderMessage() }
</Dialog.Description>
<Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
<Dialog.Button
disabled = { connecting }
label = { t('dialog.Ok') }
onPress = { this._onLogin } />
</Dialog.Container>
</View>
<Dialog.Container
coverScreen = { false }
visible = { true }>
<Dialog.Title>
{ t('dialog.login') }
</Dialog.Title>
<Dialog.Input
autoCapitalize = { 'none' }
autoCorrect = { false }
onChangeText = { this._onUsernameChange }
placeholder = { 'user@domain.com' }
spellCheck = { false }
value = { this.state.username } />
<Dialog.Input
autoCapitalize = { 'none' }
onChangeText = { this._onPasswordChange }
placeholder = { t('dialog.userPassword') }
secureTextEntry = { true }
value = { this.state.password } />
<Dialog.Description>
{ this._renderMessage() }
</Dialog.Description>
<Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
<Dialog.Button
disabled = { connecting }
label = { t('dialog.Ok') }
onPress = { this._onLogin } />
</Dialog.Container>
);
}
@@ -204,12 +175,10 @@ class LoginDialog extends Component<Props, State> {
_connecting: connecting,
_error: error,
_progress: progress,
_styles: styles,
t
} = this.props;
let messageKey;
let messageIsError = false;
const messageOptions = {};
if (progress && progress < 1) {
@@ -230,34 +199,22 @@ class LoginDialog extends Component<Props, State> {
this.props._configHosts)
&& credentials.password === this.state.password) {
messageKey = 'dialog.incorrectPassword';
messageIsError = true;
}
} else if (name) {
messageKey = 'dialog.connectErrorWithMsg';
messageOptions.msg = `${name} ${error.message}`;
messageIsError = true;
}
} else if (connecting) {
messageKey = 'connection.CONNECTING';
}
if (messageKey) {
const message = t(messageKey, messageOptions);
const messageStyles
= messageIsError ? styles.errorMessage : styles.progressMessage;
return (
<Text style = { messageStyles }>
{ message }
</Text>
);
return t(messageKey, messageOptions);
}
return null;
}
_onUsernameChange: (string) => void;
/**
* Called when user edits the username.
*
@@ -271,8 +228,6 @@ class LoginDialog extends Component<Props, State> {
});
}
_onPasswordChange: (string) => void;
/**
* Called when user edits the password.
*
@@ -286,8 +241,6 @@ class LoginDialog extends Component<Props, State> {
});
}
_onCancel: () => void;
/**
* Notifies this LoginDialog that it has been dismissed by cancel.
*
@@ -295,14 +248,9 @@ class LoginDialog extends Component<Props, State> {
* @returns {void}
*/
_onCancel() {
const { _onCancel, dispatch } = this.props;
_onCancel && _onCancel();
dispatch(cancelLogin());
this.props.dispatch(cancelLogin());
}
_onLogin: () => void;
/**
* Notifies this LoginDialog that the login button (OK) has been pressed by
* the user.
@@ -355,8 +303,7 @@ function _mapStateToProps(state) {
_configHosts: configHosts,
_connecting: Boolean(connecting) || Boolean(thenableWithCancel),
_error: connectionError || authenticateAndUpgradeRoleError,
_progress: progress,
_styles: ColorSchemeRegistry.get(state, 'LoginDialog')
_progress: progress
};
}

View File

@@ -1,14 +1,10 @@
// @flow
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { cancelWaitForOwner } from '../../actions.native';
import LoginDialog from './LoginDialog';
import { openLoginDialog, cancelWaitForOwner } from '../../actions.native';
/**
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
@@ -42,14 +38,9 @@ class WaitForOwnerDialog extends Component<Props> {
constructor(props) {
super(props);
this.state = {
showLoginDialog: false
};
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._onLogin = this._onLogin.bind(this);
this._onLoginDialogCancel = this._onLoginDialogCancel.bind(this);
}
/**
@@ -65,17 +56,10 @@ class WaitForOwnerDialog extends Component<Props> {
confirmLabel = 'dialog.IamHost'
descriptionKey = 'dialog.WaitForHostMsg'
onCancel = { this._onCancel }
onSubmit = { this._onLogin }>
<LoginDialog
// eslint-disable-next-line react/jsx-handler-names
_onCancel = { this._onLoginDialogCancel }
visible = { this.state.showLoginDialog } />
</ConfirmDialog>
onSubmit = { this._onLogin } />
);
}
_onCancel: () => void;
/**
* Called when the cancel button is clicked.
*
@@ -86,8 +70,6 @@ class WaitForOwnerDialog extends Component<Props> {
this.props.dispatch(cancelWaitForOwner());
}
_onLogin: () => void;
/**
* Called when the OK button is clicked.
*
@@ -95,17 +77,7 @@ class WaitForOwnerDialog extends Component<Props> {
* @returns {void}
*/
_onLogin() {
this.setState({ showLoginDialog: true });
}
/**
* Called when the nested login dialog is cancelled.
*
* @private
* @returns {void}
*/
_onLoginDialogCancel() {
this.setState({ showLoginDialog: false });
this.props.dispatch(openLoginDialog());
}
}

View File

@@ -1,41 +0,0 @@
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { BoxModel } from '../../../base/styles';
/**
* The styles of the authentication feature.
*/
ColorSchemeRegistry.register('LoginDialog', {
/**
* The style of {@code Text} rendered by the {@code Dialog}s of the
* feature authentication.
*/
dialogText: {
margin: BoxModel.margin,
marginTop: BoxModel.margin * 2
},
/**
* The style used when an error message is rendered.
*/
errorMessage: {
color: schemeColor('errorText')
},
/**
* The style of {@code LoginDialog}.
*/
loginDialog: {
flex: 0,
flexDirection: 'column',
marginBottom: BoxModel.margin,
marginTop: BoxModel.margin
},
/**
* The style used then a progress message is rendered.
*/
progressMessage: {
color: schemeColor('text')
}
});

View File

@@ -15,6 +15,7 @@ import {
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux';
import { setPrejoinPageVisibility } from '../prejoin';
import {
CANCEL_LOGIN,
@@ -120,6 +121,7 @@ MiddlewareRegistry.register(store => next => action => {
&& error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
&& typeof error.recoverable === 'undefined') {
error.recoverable = true;
store.dispatch(setPrejoinPageVisibility(false));
store.dispatch(openLoginDialog());
}
break;

View File

@@ -51,9 +51,10 @@ MiddlewareRegistry.register(store => next => action => {
dispatch(hideLoginDialog());
const { authRequired, conference } = getState()['features/base/conference'];
const { passwordRequired } = getState()['features/base/connection'];
// Only end the meeting if we are not already inside and trying to upgrade.
if (authRequired && !conference) {
if ((authRequired && !conference) || passwordRequired) {
dispatch(maybeRedirectToWelcomePage());
}
}

View File

@@ -29,11 +29,7 @@ export default {
inviteButtonBackground: 'rgb(0, 119, 225)',
onVideoText: 'white'
},
'Dialog': {
border: 'rgba(0, 3, 6, 0.6)',
buttonBackground: ColorPalette.blue,
buttonLabel: ColorPalette.white
},
'Dialog': {},
'Header': {
background: ColorPalette.blue,
icon: ColorPalette.white,
@@ -41,10 +37,6 @@ export default {
statusBarContent: ColorPalette.white,
text: ColorPalette.white
},
'Modal': {},
'LargeVideo': {
background: '#040404'
},
'Toolbox': {
button: 'rgb(255, 255, 255)',
buttonToggled: 'rgb(38, 58, 76)',

View File

@@ -206,6 +206,16 @@ export const SEND_TONES = 'SEND_TONES';
*/
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
/**
* The type of (redux) action which sets the obfuscated room name.
*
* {
* type: SET_OBFUSCATED_ROOM,
* obfuscatedRoom: string
* }
*/
export const SET_OBFUSCATED_ROOM = 'SET_OBFUSCATED_ROOM';
/**
* The type of (redux) action which updates the current known status of the
* Mute Reactions Sound feature.

View File

@@ -56,6 +56,7 @@ import {
P2P_STATUS_CHANGED,
SEND_TONES,
SET_FOLLOW_ME,
SET_OBFUSCATED_ROOM,
SET_PASSWORD,
SET_PASSWORD_FAILED,
SET_ROOM,
@@ -804,6 +805,24 @@ export function setPassword(
};
}
/**
* Sets the obfuscated room name of the conference to be joined.
*
* @param {(string)} obfuscatedRoom - Obfuscated room name.
* @param {(string)} obfuscatedRoomSource - The room name that was obfuscated.
* @returns {{
* type: SET_OBFUSCATED_ROOM,
* room: string
* }}
*/
export function setObfuscatedRoom(obfuscatedRoom: string, obfuscatedRoomSource: string) {
return {
type: SET_OBFUSCATED_ROOM,
obfuscatedRoom,
obfuscatedRoomSource
};
}
/**
* Sets (the name of) the room of the conference to be joined.
*

View File

@@ -1,5 +1,6 @@
// @flow
import { sha512_256 as sha512 } from 'js-sha512';
import _ from 'lodash';
import { getName } from '../../app/functions';
@@ -19,6 +20,7 @@ import {
safeDecodeURIComponent
} from '../util';
import { setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -298,6 +300,47 @@ export function getRoomName(state: Object): string {
return getConferenceState(state).room;
}
/**
* Get an obfuscated room name or create and persist it if it doesn't exists.
*
* @param {Object} state - The current state of the app.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {string} - Obfuscated room name.
*/
export function getOrCreateObfuscatedRoomName(state: Object, dispatch: Function) {
let { obfuscatedRoom } = getConferenceState(state);
const { obfuscatedRoomSource } = getConferenceState(state);
const room = getRoomName(state);
// On native mobile the store doesn't clear when joining a new conference so we might have the obfuscatedRoom
// stored even though a different room was joined.
// Check if the obfuscatedRoom was already computed for the current room.
if (!obfuscatedRoom || (obfuscatedRoomSource !== room)) {
obfuscatedRoom = sha512(room);
dispatch(setObfuscatedRoom(obfuscatedRoom, room));
}
return obfuscatedRoom;
}
/**
* Analytics may require an obfuscated room name, this functions decides based on a config if the normal or
* obfuscated room name should be returned.
*
* @param {Object} state - The current state of the app.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {string} - Analytics room name.
*/
export function getAnalyticsRoomName(state: Object, dispatch: Function) {
const { analysis: { obfuscateRoomName = false } = {} } = state['features/base/config'];
if (obfuscateRoomName) {
return getOrCreateObfuscatedRoomName(state, dispatch);
}
return getRoomName(state);
}
/**
* Returns the result of getWiFiStats from the global NS or does nothing
* (returns empty result).

View File

@@ -1,6 +1,6 @@
// @flow
import { setPictureInPictureDisabled } from '../../mobile/picture-in-picture/functions';
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
import { setAudioOnly } from '../audio-only';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
@@ -41,7 +41,7 @@ function _toggleScreenSharing(enabled, store) {
}
} else {
dispatch(destroyLocalDesktopTrackIfExists());
setPictureInPictureDisabled(false);
setPictureInPictureEnabled(true);
}
}
@@ -54,7 +54,7 @@ function _toggleScreenSharing(enabled, store) {
* @returns {void}
*/
function _startScreenSharing(dispatch, state) {
setPictureInPictureDisabled(true);
setPictureInPictureEnabled(false);
JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] })
.then(tracks => {
@@ -73,6 +73,6 @@ function _startScreenSharing(dispatch, state) {
.catch(error => {
console.log('ERROR creating ScreeSharing stream ', error);
setPictureInPictureDisabled(false);
setPictureInPictureEnabled(true);
});
}

View File

@@ -18,6 +18,7 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_FOLLOW_ME,
SET_OBFUSCATED_ROOM,
SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE,
SET_ROOM,
@@ -88,6 +89,12 @@ ReducerRegistry.register(
case SET_LOCATION_URL:
return set(state, 'room', undefined);
case SET_OBFUSCATED_ROOM:
return { ...state,
obfuscatedRoom: action.obfuscatedRoom,
obfuscatedRoomSource: action.obfuscatedRoomSource
};
case SET_PASSWORD:
return _setPassword(state, action);

View File

@@ -143,7 +143,6 @@ export default [
'enableLayerSuspension',
'enableLipSync',
'enableLobbyChat',
'enableLocalRecording',
'enableOpusRed',
'enableRemb',
'enableSaveLogs',
@@ -185,6 +184,7 @@ export default [
'ignoreStartMuted',
'inviteAppName',
'liveStreamingEnabled',
'localRecording',
'localSubject',
'maxFullResolutionParticipants',
'mouseMoveCallbackInterval',

View File

@@ -0,0 +1,71 @@
import _ from 'lodash';
import {
appendURLParam,
getBackendSafeRoomName,
parseURIString
} from '../util';
import logger from './logger';
/**
* Constructs options to be passed to the constructor of {@code JitsiConnection}
* based on the redux state.
*
* @param {Object} state - The redux state.
* @returns {Object} The options to be passed to the constructor of
* {@code JitsiConnection}.
*/
export function constructOptions(state) {
// Deep clone the options to make sure we don't modify the object in the
// redux store.
const options = _.cloneDeep(state['features/base/config']);
let { bosh, websocket } = options;
// TESTING: Only enable WebSocket for some percentage of users.
if (websocket && navigator.product === 'ReactNative') {
if ((Math.random() * 100) >= (options?.testing?.mobileXmppWsThreshold ?? 0)) {
websocket = undefined;
}
}
// Normalize the BOSH URL.
if (bosh && !websocket) {
const { locationURL } = state['features/base/connection'];
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL.href);
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
}
// WebSocket is preferred over BOSH.
const serviceUrl = websocket || bosh;
logger.log(`Using service URL ${serviceUrl}`);
// Append room to the URL's search.
const { room } = state['features/base/conference'];
if (serviceUrl && room) {
const roomName = getBackendSafeRoomName(room);
options.serviceUrl = appendURLParam(serviceUrl, 'room', roomName);
if (options.websocketKeepAliveUrl) {
options.websocketKeepAliveUrl = appendURLParam(options.websocketKeepAliveUrl, 'room', roomName);
}
}
return options;
}

View File

@@ -1,15 +1,10 @@
// @flow
import _ from 'lodash';
import type { Dispatch } from 'redux';
import { conferenceLeft, conferenceWillLeave } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import {
getBackendSafeRoomName,
parseURIString
} from '../util';
import {
CONNECTION_DISCONNECTED,
@@ -18,9 +13,12 @@ import {
CONNECTION_WILL_CONNECT,
SET_LOCATION_URL
} from './actionTypes';
import { constructOptions } from './actions.any';
import { JITSI_CONNECTION_URL_KEY } from './constants';
import logger from './logger';
export * from './actions.any';
/**
* The error structure passed to the {@link connectionFailed} action.
*
@@ -78,7 +76,7 @@ export type ConnectionFailedError = {
export function connect(id: ?string, password: ?string) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const options = _constructOptions(state);
const options = constructOptions(state);
const { locationURL } = state['features/base/connection'];
const { jwt } = state['features/base/jwt'];
const connection = new JitsiMeetJS.JitsiConnection(options.appId, jwt, options);
@@ -262,69 +260,6 @@ function _connectionWillConnect(connection) {
};
}
/**
* Constructs options to be passed to the constructor of {@code JitsiConnection}
* based on the redux state.
*
* @param {Object} state - The redux state.
* @returns {Object} The options to be passed to the constructor of
* {@code JitsiConnection}.
*/
function _constructOptions(state) {
// Deep clone the options to make sure we don't modify the object in the
// redux store.
const options = _.cloneDeep(state['features/base/config']);
let { bosh, websocket } = options;
// TESTING: Only enable WebSocket for some percentage of users.
if (websocket) {
if ((Math.random() * 100) >= (options?.testing?.mobileXmppWsThreshold ?? 0)) {
websocket = undefined;
}
}
// Normalize the BOSH URL.
if (bosh && !websocket) {
const { locationURL } = state['features/base/connection'];
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL.href);
// eslint-disable-next-line max-len
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
}
// WebSocket is preferred over BOSH.
const serviceUrl = websocket || bosh;
logger.log(`Using service URL ${serviceUrl}`);
// Append room to the URL's search.
const { room } = state['features/base/conference'];
if (serviceUrl && room) {
const roomName = getBackendSafeRoomName(room);
options.serviceUrl = `${serviceUrl}?room=${roomName}`;
if (options.websocketKeepAliveUrl) {
options.websocketKeepAliveUrl += `?room=${roomName}`;
}
}
return options;
}
/**
* Closes connection.
*

View File

@@ -16,6 +16,8 @@ export {
} from './actions.native';
import logger from './logger';
export * from './actions.any';
/**
* Opens new connection.
*

View File

@@ -7,6 +7,15 @@
*/
export const HIDE_DIALOG = 'HIDE_DIALOG';
/**
* The type of Redux action which closes a sheet.
*
* {
* type: HIDE_SHEET
* }
*/
export const HIDE_SHEET = 'HIDE_SHEET';
/**
* The type of Redux action which begins a request to open a dialog.
*
@@ -18,3 +27,15 @@ export const HIDE_DIALOG = 'HIDE_DIALOG';
*
*/
export const OPEN_DIALOG = 'OPEN_DIALOG';
/**
* The type of Redux action which begins a request to open a sheet.
*
* {
* type: OPEN_SHEET,
* component: React.Component,
* props: PropTypes
* }
*
*/
export const OPEN_SHEET = 'OPEN_SHEET';

View File

@@ -2,7 +2,12 @@
import type { Dispatch } from 'redux';
import { HIDE_DIALOG, OPEN_DIALOG } from './actionTypes';
import {
HIDE_DIALOG,
HIDE_SHEET,
OPEN_DIALOG,
OPEN_SHEET
} from './actionTypes';
import { isDialogOpen } from './functions';
/**
@@ -24,6 +29,19 @@ export function hideDialog(component: ?Object) {
};
}
/**
* Closes the active sheet.
*
* @returns {{
* type: HIDE_SHEET,
* }}
*/
export function hideSheet() {
return {
type: HIDE_SHEET
};
}
/**
* Signals Dialog to open dialog.
*
@@ -44,6 +62,26 @@ export function openDialog(component: Object, componentProps: ?Object) {
};
}
/**
* Opens the requested sheet.
*
* @param {Object} component - The component to display as a sheet.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @returns {{
* type: OPEN_SHEET,
* component: React.Component,
* componentProps: (Object | undefined)
* }}
*/
export function openSheet(component: Object, componentProps: ?Object) {
return {
type: OPEN_SHEET,
component,
componentProps
};
}
/**
* Signals Dialog to open a dialog with the specified component if the component
* is not already open. If it is open, then Dialog is signaled to close its

View File

@@ -1,7 +1,4 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import Dialog from 'react-native-dialog';
import { translate } from '../../../i18n';
@@ -46,16 +43,16 @@ class AlertDialog extends AbstractDialog<Props> {
: renderHTML(t(contentKey.key, contentKey.params));
return (
<View>
<Dialog.Container visible = { true }>
<Dialog.Description>
{ content }
</Dialog.Description>
<Dialog.Button
label = { t('dialog.Ok') }
onPress = { this._onSubmit } />
</Dialog.Container>
</View>
<Dialog.Container
coverScreen = { false }
visible = { true }>
<Dialog.Description>
{ content }
</Dialog.Description>
<Dialog.Button
label = { t('dialog.Ok') }
onPress = { this._onSubmit } />
</Dialog.Container>
);
}

View File

@@ -1,40 +1,17 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import { PanResponder, SafeAreaView, ScrollView, View } from 'react-native';
import { SafeAreaView, ScrollView, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { SlidingView } from '../../../react';
import { connect } from '../../../redux';
import { StyleType } from '../../../styles';
import { hideSheet } from '../../actions';
import { bottomSheetStyles as styles } from './styles';
/**
* Minimal distance that needs to be moved by the finger to consider it a swipe.
*/
const GESTURE_DISTANCE_THRESHOLD = 5;
/**
* The minimal speed needed to be achieved by the finger to consider it as a swipe.
*/
const GESTURE_SPEED_THRESHOLD = 0.2;
/**
* The type of {@code BottomSheet}'s React {@code Component} prop types.
*/
type Props = {
/**
* The height of the screen.
*/
_height: number,
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType,
/**
* Whether to add padding to scroll view.
*/
@@ -45,17 +22,17 @@ type Props = {
*/
children: Node,
/**
* Redux Dispatch function.
*/
dispatch: Function,
/**
* Handler for the cancel event, which happens when the user dismisses
* the sheet.
*/
onCancel: ?Function,
/**
* Callback to be attached to the custom swipe event of the BottomSheet.
*/
onSwipe?: Function,
/**
* Function to render a bottom sheet header element, if necessary.
*/
@@ -81,8 +58,6 @@ type Props = {
* A component emulating Android's BottomSheet.
*/
class BottomSheet extends PureComponent<Props> {
panResponder: Object;
/**
* Default values for {@code BottomSheet} component's properties.
*
@@ -94,18 +69,28 @@ class BottomSheet extends PureComponent<Props> {
};
/**
* Instantiates a new component.
* Initializes a new instance.
*
* @inheritdoc
* @param {Props} props - The React {@code Component} props to initialize
* the new instance with.
*/
constructor(props: Props) {
super(props);
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: this._onShouldSetResponder.bind(this),
onMoveShouldSetPanResponder: this._onShouldSetResponder.bind(this),
onPanResponderRelease: this._onGestureEnd.bind(this)
});
this._onCancel = this._onCancel.bind(this);
}
/**
* Handles the cancel event, when the user dismissed the sheet. By default we close it.
*
* @returns {void}
*/
_onCancel() {
if (this.props.onCancel) {
this.props.onCancel();
} else {
this.props.dispatch(hideSheet());
}
}
/**
@@ -116,8 +101,6 @@ class BottomSheet extends PureComponent<Props> {
*/
render() {
const {
_height,
_styles,
addScrollViewPadding,
renderHeader,
renderFooter,
@@ -129,7 +112,7 @@ class BottomSheet extends PureComponent<Props> {
<SlidingView
accessibilityRole = 'menu'
accessibilityViewIsModal = { true }
onHide = { this.props.onCancel }
onHide = { this._onCancel }
position = 'bottom'
show = { showSlidingView }>
<View
@@ -143,20 +126,16 @@ class BottomSheet extends PureComponent<Props> {
style = { [
styles.sheetItemContainer,
renderHeader
? _styles.sheetHeader
: _styles.sheet,
renderFooter && _styles.sheetFooter,
style,
{
maxHeight: _height - 100
}
] }
{ ...this.panResponder.panHandlers }>
? styles.sheetHeader
: styles.sheet,
renderFooter && styles.sheetFooter,
style
] }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { [
renderFooter && _styles.sheet,
renderFooter && styles.sheet,
addScrollViewPadding && styles.scrollView
] } >
{ this.props.children }
@@ -167,63 +146,6 @@ class BottomSheet extends PureComponent<Props> {
</SlidingView>
);
}
/**
* Callback to handle a gesture end event.
*
* @param {Object} evt - The native gesture event.
* @param {Object} gestureState - The gesture state.
* @returns {void}
*/
_onGestureEnd(evt, gestureState) {
const verticalSwipe = Math.abs(gestureState.vy) > Math.abs(gestureState.vx)
&& Math.abs(gestureState.vy) > GESTURE_SPEED_THRESHOLD;
if (verticalSwipe) {
const direction = gestureState.vy > 0 ? 'down' : 'up';
const { onCancel, onSwipe } = this.props;
let isSwipeHandled = false;
if (onSwipe) {
isSwipeHandled = onSwipe(direction);
}
if (direction === 'down' && !isSwipeHandled) {
// Swipe down is a special gesture that can be used to close the
// BottomSheet, so if the swipe is not handled by the parent
// component, we consider it as a request to close.
onCancel && onCancel();
}
}
}
/**
* Returns true if the pan responder should activate, false otherwise.
*
* @param {Object} evt - The native gesture event.
* @param {Object} gestureState - The gesture state.
* @returns {boolean}
*/
_onShouldSetResponder({ nativeEvent }, gestureState) {
return nativeEvent.touches.length === 1
&& Math.abs(gestureState.dx) > GESTURE_DISTANCE_THRESHOLD
&& Math.abs(gestureState.dy) > GESTURE_DISTANCE_THRESHOLD;
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _styles: StyleType
* }}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'BottomSheet'),
_height: state['features/base/responsive-ui'].clientHeight
};
}
export default connect(_mapStateToProps)(BottomSheet);
export default connect()(BottomSheet);

View File

@@ -0,0 +1,20 @@
import React, { Fragment } from 'react';
import { useSelector } from 'react-redux';
const BottomSheetContainer: () => JSX.Element = () => {
const { sheet, sheetProps } = useSelector(state => state['features/base/dialog']);
const { reducedUI } = useSelector(state => state['features/base/responsive-ui']);
if (!sheet || reducedUI) {
return null;
}
return (
<Fragment>
{ React.createElement(sheet, sheetProps) }
</Fragment>
);
}
export default BottomSheetContainer;

View File

@@ -1,7 +1,4 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import Dialog from 'react-native-dialog';
import { translate } from '../../../i18n';
@@ -26,7 +23,7 @@ type Props = {
/**
* The React {@code Component} children.
*/
children?: React$Node,
children?: Node,
/**
* The i18n key of the text label for the confirm button.
@@ -111,25 +108,25 @@ class ConfirmDialog extends AbstractDialog<Props> {
? styles.destructiveDialogButton : styles.dialogButton;
return (
<View>
<Dialog.Container visible = { true }>
{
title && <Dialog.Title>
{ t(title) }
</Dialog.Title>
}
{ this._renderDescription() }
{ children }
<Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
<Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }
onPress = { this._onSubmit }
style = { dialogButtonStyle } />
</Dialog.Container>
</View>
<Dialog.Container
coverScreen = { false }
visible = { true }>
{
title && <Dialog.Title>
{ t(title) }
</Dialog.Title>
}
{ this._renderDescription() }
{ children }
<Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
<Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }
onPress = { this._onSubmit }
style = { dialogButtonStyle } />
</Dialog.Container>
);
}

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { Fragment } from 'react';
import { ReactionEmoji } from '../../../../reactions/components';
import { getReactionsQueue } from '../../../../reactions/functions.any';
@@ -38,10 +38,12 @@ class DialogContainer extends AbstractDialogContainer {
* @returns {ReactElement}
*/
render() {
return (<React.Fragment>
{this._renderReactions()}
{this._renderDialogContent()}
</React.Fragment>);
return (
<Fragment>
{this._renderReactions()}
{this._renderDialogContent()}
</Fragment>
);
}
}

View File

@@ -1,7 +1,4 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import Dialog from 'react-native-dialog';
import { translate } from '../../../i18n';
@@ -95,40 +92,39 @@ class InputDialog<P: Props, S: State> extends AbstractDialog<P, S> {
} = this.props;
return (
<View>
<Dialog.Container
visible = { true }>
<Dialog.Title>
{ t(titleKey) }
</Dialog.Title>
{
descriptionKey && (
<Dialog.Description>
{ t(descriptionKey) }
</Dialog.Description>
)
}
<Dialog.Input
autoFocus = { true }
onChangeText = { this._onChangeText }
value = { this.state.fieldValue }
{ ...this.props.textInputProps } />
{
messageKey && (
<Dialog.Description
style = { styles.formMessage }>
{ t(messageKey) }
</Dialog.Description>
)
}
<Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
<Dialog.Button
label = { t('dialog.Ok') }
onPress = { this._onSubmitValue } />
</Dialog.Container>
</View>
<Dialog.Container
coverScreen = { false }
visible = { true }>
<Dialog.Title>
{ t(titleKey) }
</Dialog.Title>
{
descriptionKey && (
<Dialog.Description>
{ t(descriptionKey) }
</Dialog.Description>
)
}
<Dialog.Input
autoFocus = { true }
onChangeText = { this._onChangeText }
value = { this.state.fieldValue }
{ ...this.props.textInputProps } />
{
messageKey && (
<Dialog.Description
style = { styles.formMessage }>
{ t(messageKey) }
</Dialog.Description>
)
}
<Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
<Dialog.Button
label = { t('dialog.Ok') }
onPress = { this._onSubmitValue } />
</Dialog.Container>
);
}

View File

@@ -1,5 +1,3 @@
// @flow
import { StyleSheet } from 'react-native';
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
@@ -20,85 +18,6 @@ export const MD_FONT_SIZE = 16;
export const MD_ITEM_HEIGHT = 48;
export const MD_ITEM_MARGIN_PADDING = 16;
/**
* The React {@code Component} styles of {@code BottomSheet}. These have
* been implemented as per the Material Design guidelines:
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
export const bottomSheetStyles = {
sheetAreaCover: {
backgroundColor: ColorPalette.transparent,
flex: 1
},
scrollView: {
paddingHorizontal: 0
},
/**
* Style for the container of the sheet.
*/
sheetContainer: {
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end',
maxWidth: 500,
marginLeft: 'auto',
marginRight: 'auto',
width: '100%'
},
sheetItemContainer: {
flex: -1
}
};
export default {
dialogButton: {
...BaseTheme.typography.labelButton
},
destructiveDialogButton: {
...BaseTheme.typography.labelButton,
color: BaseTheme.palette.actionDanger
}
};
export const brandedDialog = {
/**
* The style of bold {@code Text} rendered by the {@code Dialog}s of the
* feature authentication.
*/
boldDialogText: {
fontWeight: 'bold'
},
buttonFarRight: {
borderBottomRightRadius: BORDER_RADIUS
},
buttonWrapper: {
alignItems: 'stretch',
borderRadius: BORDER_RADIUS,
flexDirection: 'row'
},
mainWrapper: {
alignSelf: 'stretch',
padding: BoxModel.padding * 2,
// The added bottom padding is to compensate the empty space around the
// close icon.
paddingBottom: BoxModel.padding * 3
},
overlayTouchable: {
...StyleSheet.absoluteFillObject
}
};
/**
* Reusable (colored) style for text in any branded dialogs.
*/
@@ -136,12 +55,39 @@ export const inputDialog = {
};
/**
* Default styles for the items of a {@code BottomSheet}-based menu.
*
* These have been implemented as per the Material Design guidelines:
* The React {@code Component} styles of {@code BottomSheet}. These have
* been implemented as per the Material Design guidelines:
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
ColorSchemeRegistry.register('BottomSheet', {
export const bottomSheetStyles = {
sheetAreaCover: {
backgroundColor: ColorPalette.transparent,
flex: 1
},
scrollView: {
paddingHorizontal: 0
},
/**
* Style for the container of the sheet.
*/
sheetContainer: {
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end',
maxWidth: 500,
marginLeft: 'auto',
marginRight: 'auto',
width: '100%'
},
sheetItemContainer: {
flex: -1,
maxHeight: '75%'
},
buttons: {
/**
* Style for the {@code Icon} element in a generic item of the menu.
@@ -194,7 +140,53 @@ ColorSchemeRegistry.register('BottomSheet', {
sheetFooter: {
backgroundColor: BaseTheme.palette.bottomSheet
}
});
};
export default {
dialogButton: {
...BaseTheme.typography.labelButton
},
destructiveDialogButton: {
...BaseTheme.typography.labelButton,
color: BaseTheme.palette.actionDanger
}
};
export const brandedDialog = {
/**
* The style of bold {@code Text} rendered by the {@code Dialog}s of the
* feature authentication.
*/
boldDialogText: {
fontWeight: 'bold'
},
buttonFarRight: {
borderBottomRightRadius: BORDER_RADIUS
},
buttonWrapper: {
alignItems: 'stretch',
borderRadius: BORDER_RADIUS,
flexDirection: 'row'
},
mainWrapper: {
alignSelf: 'stretch',
padding: BoxModel.padding * 2,
// The added bottom padding is to compensate the empty space around the
// close icon.
paddingBottom: BoxModel.padding * 3
},
overlayTouchable: {
...StyleSheet.absoluteFillObject
}
};
/**
* Color schemed styles for all the component based on the abstract dialog.
@@ -272,28 +264,3 @@ ColorSchemeRegistry.register('Dialog', {
borderTopWidth: 1
}
});
ColorSchemeRegistry.register('SecurityDialog', {
/**
* Field on an input dialog.
*/
field: {
borderBottomWidth: 1,
borderColor: schemeColor('border'),
color: schemeColor('text'),
fontSize: 14,
paddingBottom: 8
},
text: {
color: schemeColor('text'),
fontSize: 14,
marginTop: 8
},
title: {
color: schemeColor('text'),
fontSize: 18,
fontWeight: 'bold'
}
});

View File

@@ -1,8 +1,11 @@
/* @flow */
import { assign, ReducerRegistry } from '../redux';
import { HIDE_DIALOG, OPEN_DIALOG } from './actionTypes';
import {
HIDE_DIALOG,
HIDE_SHEET,
OPEN_DIALOG,
OPEN_SHEET
} from './actionTypes';
/**
* Reduces redux actions which show or hide dialogs.
@@ -32,6 +35,18 @@ ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
component: action.component,
componentProps: action.componentProps
});
case HIDE_SHEET:
return assign(state, {
sheet: undefined,
sheetProps: undefined
});
case OPEN_SHEET:
return assign(state, {
sheet: action.component,
sheetProps: action.componentProps
});
}
return state;

View File

@@ -160,6 +160,12 @@ export const OVERFLOW_MENU_ENABLED = 'overflow-menu.enabled';
*/
export const PIP_ENABLED = 'pip.enabled';
/**
* Flag indicating if the prejoin page should be enabled.
* Default: enabled (true).
*/
export const PREJOIN_PAGE_ENABLED = 'prejoinpage.enabled';
/**
* Flag indicating if raise hand feature should be enabled.
* Default: enabled.

View File

@@ -1,13 +1,14 @@
// @flow
import { useHeaderHeight } from '@react-navigation/elements';
import React, { useEffect, useState } from 'react';
import { getDefaultHeaderHeight } from '@react-navigation/elements';
import React, { useCallback, useEffect, useState } from 'react';
import {
Keyboard,
KeyboardAvoidingView,
Platform,
StatusBar
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context';
import { StyleType } from '../../styles';
@@ -33,6 +34,11 @@ type Props = {
*/
hasTabNavigator: boolean,
/**
* Is the screen presented as a modal?
*/
isModalPresentation: boolean,
/**
* Additional style to be appended to the KeyboardAvoidingView.
*/
@@ -45,26 +51,37 @@ const JitsiKeyboardAvoidingView = (
contentContainerStyle,
hasTabNavigator,
hasBottomTextInput,
isModalPresentation,
style
}: Props) => {
const headerHeight = useHeaderHeight();
const frame = useSafeAreaFrame();
const insets = useSafeAreaInsets();
const [ bottomPadding, setBottomPadding ] = useState(insets.bottom);
const [ topPadding, setTopPadding ] = useState(insets.top);
useEffect(() => {
// This useEffect is needed because insets are undefined at first for some reason
// https://github.com/th3rdwave/react-native-safe-area-context/issues/54
setBottomPadding(insets.bottom);
setTopPadding(insets.top);
}, [ insets.bottom, insets.top ]);
}, [ insets.bottom ]);
const headerHeight = getDefaultHeaderHeight(frame, isModalPresentation, topPadding);
// Notch devices have in general a header height between 103 and 106px
const topNotchDevice = headerHeight > 100;
const deviceHeight = topNotchDevice ? headerHeight - 50 : headerHeight;
const tabNavigatorPadding
= hasTabNavigator ? headerHeight : 0;
= hasTabNavigator ? deviceHeight : 0;
const noNotchDevicePadding = bottomPadding || 10;
const iosVerticalOffset
= headerHeight + noNotchDevicePadding + tabNavigatorPadding;
= deviceHeight + noNotchDevicePadding + tabNavigatorPadding;
const androidVerticalOffset = hasBottomTextInput
? headerHeight + StatusBar.currentHeight : headerHeight;
? deviceHeight + StatusBar.currentHeight : deviceHeight;
// Tells the view what to do with taps
const shouldSetResponse = useCallback(() => true);
const onRelease = useCallback(() => Keyboard.dismiss());
return (
<KeyboardAvoidingView
@@ -76,6 +93,8 @@ const JitsiKeyboardAvoidingView = (
? iosVerticalOffset
: androidVerticalOffset
}
onResponderRelease = { onRelease }
onStartShouldSetResponder = { shouldSetResponse }
style = { style }>
{ children }
</KeyboardAvoidingView>

View File

@@ -37,6 +37,11 @@ type Props = {
*/
hasTabNavigator?: boolean,
/**
* Is the screen presented as a modal?
*/
isModalPresentation?: boolean,
/**
* Insets for the SafeAreaView.
*/
@@ -54,7 +59,8 @@ const JitsiScreen = ({
footerComponent,
hasTabNavigator = false,
hasBottomTextInput = false,
safeAreaInsets = [ 'bottom', 'left', 'right' ],
isModalPresentation = true,
safeAreaInsets = [ 'left', 'right' ],
style
}: Props) => (
<View
@@ -63,13 +69,14 @@ const JitsiScreen = ({
contentContainerStyle = { contentContainerStyle }
hasBottomTextInput = { hasBottomTextInput }
hasTabNavigator = { hasTabNavigator }
isModalPresentation = { isModalPresentation }
style = { style }>
<SafeAreaView
edges = { safeAreaInsets }
style = { styles.safeArea }>
{ children }
{children}
</SafeAreaView>
{ footerComponent && footerComponent() }
{footerComponent && footerComponent()}
</JitsiKeyboardAvoidingView>
</View>
);

View File

@@ -178,20 +178,24 @@ MiddlewareRegistry.register(store => next => action => {
}
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
const state = store.getState();
const { recording } = action;
const localId = getLocalParticipant(store.getState())?.id;
const localId = getLocalParticipant(state)?.id;
const { localRecording } = state['features/base/config'];
store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
if (localRecording.notifyAllParticipants) {
store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id: localId,
local: true,
localRecording: recording
}));
id: localId,
local: true,
localRecording: recording
}));
}
break;
}

View File

@@ -3,4 +3,6 @@
/**
* The default server URL to open if no other was specified.
*/
export const DEFAULT_SERVER_URL = 'https://meet.jit.si';
//export const DEFAULT_SERVER_URL = 'https://abora6.jitsi.net#config.replaceParticipant=true';
export const DEFAULT_SERVER_URL = 'https://alpha.jitsi.net';
//export const DEFAULT_SERVER_URL = 'https://meet.jit.si';

View File

@@ -11,6 +11,8 @@ export const commonClassName = {
overflowMenuItem: 'overflow-menu-item',
overflowMenuItemIcon: 'overflow-menu-item-icon',
participantAvatar: 'participant-avatar',
prejoinDialog: 'prejoin-dialog',
prejoinDialogButton: 'prejoin-dialog-btn',
toolboxIcon: 'toolbox-icon',
toolboxButton: 'toolbox-button',
toolboxContentItems: 'toolbox-content-items'
@@ -120,6 +122,112 @@ export const commonStyles = (theme: Object) => {
[commonClassName.participantAvatar]: {
margin: `${theme.spacing(2)}px ${theme.spacing(3)}px ${theme.spacing(2)}px 0`
},
[commonClassName.prejoinDialog]: {
background: '#1C2025',
boxShadow: '0px 2px 20px rgba(0, 0, 0, 0.5)',
borderRadius: '5px',
color: '#fff',
height: '400px',
width: '375px',
[`${commonClassName.prejoinDialog}--small`]: {
height: 300,
width: 400
},
[`${commonClassName.prejoinDialog}-label`]: {
fontSize: '15px',
lineHeight: '24px'
},
[`${commonClassName.prejoinDialog}-label-num`]: {
background: '#2b3b4b',
border: '1px solid #A4B8D1',
borderRadius: '50%',
color: '#fff',
display: 'inline-block',
height: '24px',
marginRight: `${theme.spacing(2)}px`,
width: '24px'
},
[`${commonClassName.prejoinDialog}-container`]: {
alignItems: 'center',
background: 'rgba(0,0,0,0.6)',
display: 'flex',
height: '100vh',
justifyContent: 'center',
left: 0,
position: 'absolute',
top: 0,
width: '100vw',
zIndex: 3
},
[`${commonClassName.prejoinDialog}-flag`]: {
display: 'inline-block',
marginRight: `${theme.spacing(2)}px}`,
transform: 'scale(1.2)'
},
[`${commonClassName.prejoinDialog}-title`]: {
display: 'inline-block',
fontSize: '24px',
lineHeight: '32px'
},
[`${commonClassName.prejoinDialog}-icon`]: {
cursor: 'pointer',
'& > svg': {
fill: '#A4B8D1'
}
},
[commonClassName.prejoinDialogButton]: {
width: '309px'
},
[`${commonClassName.prejoinDialog}-dialin-container`]: {
textAlign: 'center'
},
[`${commonClassName.prejoinDialog}-delimiter`]: {
background: '#5f6266',
border: '0',
height: '1px',
margin: '0',
padding: '0',
width: '100%'
},
[`${commonClassName.prejoinDialog}-delimiter-container`]: {
margin: `${theme.spacing(3)}px 0 ${theme.spacing(4)}px 0`,
position: 'relative'
},
[`${commonClassName.prejoinDialog}-delimiter-txt-container`]: {
position: 'absolute',
textAlign: 'center',
top: '-8px',
width: '100%'
},
[`${commonClassName.prejoinDialog}-delimiter-txt`]: {
background: '#1C2025',
color: '#5f6266',
fontSize: '11px',
textTransform: 'uppercase',
padding: `0 ${theme.spacing(2)}px`
}
},
[commonClassName.prejoinDialogButton]: {
[`&.primary, &${commonClassName.prejoinDialogButton}.text`]: {
width: '310px'
}
},
[commonClassName.toolboxIcon]: {
display: 'flex',
borderRadius: 3,

View File

@@ -23,9 +23,13 @@ const blacklist = [ '__proto__', 'constructor', 'prototype' ];
* @returns {Object}
*/
export function parseURLParams(
url: URL,
url: URL | string,
dontParse: boolean = false,
source: string = 'hash'): Object {
if (typeof url === 'string') {
// eslint-disable-next-line no-param-reassign
url = new URL(url);
}
const paramStr = source === 'search' ? url.search : url.hash;
const params = {};
const paramParts = (paramStr && paramStr.substr(1).split('&')) || [];

View File

@@ -524,7 +524,7 @@ export function urlObjectToString(o: Object): ?string {
// query/search
// Web's ExternalAPI jwt and lang
const { jwt, lang } = o;
const { jwt, lang, release } = o;
const search = new URLSearchParams(url.search);
@@ -538,6 +538,10 @@ export function urlObjectToString(o: Object): ?string {
search.set('lang', lang || defaultLanguage);
}
if (release) {
search.set('release', release);
}
const searchString = search.toString();
if (searchString) {
@@ -603,3 +607,20 @@ export function addHashParamsToURL(url: URL, hashParamsToAdd: Object = {}) {
export function getDecodedURI(uri: string) {
return decodeURI(uri.replace(/^https?:\/\//i, ''));
}
/**
* Adds new param to a url string. Checks whether to use '?' or '&' as a separator (checks for already existing params).
*
* @param {string} url - The url to modify.
* @param {string} name - The param name to add.
* @param {string} value - The value for the param.
*
* @returns {string} - The modified url.
*/
export function appendURLParam(url: string, name: string, value: string) {
const newUrl = new URL(url);
newUrl.searchParams.append(name, value);
return newUrl.toString();
}

View File

@@ -56,7 +56,8 @@ export default {
borderTopColor: 'rgb(209, 219, 231)',
borderTopWidth: 1,
flexDirection: 'row',
paddingHorizontal: BoxModel.padding
paddingBottom: '4%',
paddingHorizontal: BaseTheme.spacing[3]
},
inputField: {

View File

@@ -104,6 +104,7 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
shouldShow: false
};
this.isEdge = /Edg(e)?/.test(navigator.userAgent);
this._onClosePressed = this._onClosePressed.bind(this);
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
this._shouldNotRender = this._shouldNotRender.bind(this);
@@ -196,8 +197,10 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
* @returns {void}
*/
_onInstallExtensionClick() {
const { edgeUrl, url } = this.props.bannerCfg;
sendAnalytics(createChromeExtensionBannerEvent(true));
window.open(this.props.bannerCfg.url);
window.open(this.isEdge && edgeUrl ? edgeUrl : url);
this.setState({ closePressed: true });
}
@@ -264,7 +267,7 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
return null;
}
const { t } = this.props;
const { bannerCfg, t } = this.props;
const mainClassNames = this.props.conference
? 'chrome-extension-banner chrome-extension-banner__pos_in_meeting'
: 'chrome-extension-banner';
@@ -306,7 +309,10 @@ class ChromeExtensionBanner extends PureComponent<Props, State> {
<div
className = 'chrome-extension-banner__button-text'
id = 'chrome-extension-banner__button-text'>
{ t('chromeExtensionBanner.buttonText') }
{ t(this.isEdge && bannerCfg.edgeUrl
? 'chromeExtensionBanner.buttonTextEdge'
: 'chromeExtensionBanner.buttonText')
}
</div>
</div>
</div>

View File

@@ -22,11 +22,14 @@ import {
} from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { startKnocking } from '../../../lobby/actions.any';
import { KnockingParticipantList } from '../../../lobby/components/native';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { shouldEnableAutoKnock } from '../../../mobile/navigation/functions';
import { screen } from '../../../mobile/navigation/routes';
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture';
import { Captions } from '../../../subtitles';
import { setToolboxVisible } from '../../../toolbox/actions';
import { Toolbox } from '../../../toolbox/components/native';
@@ -61,11 +64,6 @@ type Props = AbstractProps & {
*/
_brandingStyles: Object,
/**
* Branding image background.
*/
_brandingImageBackgroundUrl: string,
/**
* Wherther the calendar feature is enabled or not.
*/
@@ -104,6 +102,11 @@ type Props = AbstractProps & {
*/
_largeVideoParticipantId: string,
/**
* Local participant's display name.
*/
_localParticipantDisplayName: string,
/**
* Whether Picture-in-Picture is enabled.
*/
@@ -120,6 +123,11 @@ type Props = AbstractProps & {
*/
_toolboxVisible: boolean,
/**
* Indicates if we should auto-knock.
*/
_shouldEnableAutoKnock: boolean,
/**
* Indicates whether the lobby screen should be visible.
*/
@@ -184,6 +192,7 @@ class Conference extends AbstractConference<Props, State> {
*/
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
setPictureInPictureEnabled(true);
}
/**
@@ -192,10 +201,18 @@ class Conference extends AbstractConference<Props, State> {
* @inheritdoc
*/
componentDidUpdate(prevProps) {
const { _showLobby } = this.props;
const {
_shouldEnableAutoKnock,
_showLobby,
dispatch
} = this.props;
if (!prevProps._showLobby && _showLobby) {
navigate(screen.lobby.root);
if (_shouldEnableAutoKnock) {
dispatch(startKnocking());
}
}
if (prevProps._showLobby && !_showLobby) {
@@ -216,6 +233,7 @@ class Conference extends AbstractConference<Props, State> {
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
clearTimeout(this._expandedLabelTimeout.current);
setPictureInPictureEnabled(false);
}
/**
@@ -226,7 +244,6 @@ class Conference extends AbstractConference<Props, State> {
*/
render() {
const {
_brandingImageBackgroundUrl,
_brandingStyles,
_fullscreenEnabled
} = this.props;
@@ -237,8 +254,7 @@ class Conference extends AbstractConference<Props, State> {
styles.conference,
_brandingStyles
] }>
<BrandingImageBackground
uri = { _brandingImageBackgroundUrl } />
<BrandingImageBackground />
<StatusBar
barStyle = 'light-content'
hidden = { _fullscreenEnabled }
@@ -520,7 +536,7 @@ class Conference extends AbstractConference<Props, State> {
function _mapStateToProps(state) {
const { isOpen } = state['features/participants-pane'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
const { backgroundColor } = state['features/dynamic-branding'];
const participantCount = getParticipantCount(state);
const brandingStyles = backgroundColor ? {
backgroundColor
@@ -530,7 +546,6 @@ function _mapStateToProps(state) {
...abstractMapStateToProps(state),
_aspectRatio: aspectRatio,
_brandingStyles: brandingStyles,
_brandingImageBackgroundUrl: backgroundImageUrl,
_calendarEnabled: isCalendarEnabled(state),
_connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state),
@@ -540,6 +555,7 @@ function _mapStateToProps(state) {
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
_reducedUI: reducedUI,
_shouldEnableAutoKnock: shouldEnableAutoKnock(state),
_showLobby: getIsLobbyVisible(state),
_toolboxVisible: isToolboxVisible(state)
};

View File

@@ -1,15 +1,11 @@
// @flow
import React, { PureComponent } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { getFeatureFlag, INVITE_ENABLED } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { Icon, IconAddPeople } from '../../../base/icons';
import { getParticipantCountWithFake } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { doInvitePeople } from '../../../invite/actions.native';
@@ -35,11 +31,6 @@ type Props = {
*/
_isLonelyMeeting: boolean,
/**
* Color schemed styles of the component.
*/
_styles: StyleType,
/**
* The Redux Dispatch function.
*/
@@ -76,7 +67,6 @@ class LonelyMeetingExperience extends PureComponent<Props> {
_isInBreakoutRoom,
_isInviteFunctionsDiabled,
_isLonelyMeeting,
_styles,
t
} = this.props;
@@ -86,29 +76,18 @@ class LonelyMeetingExperience extends PureComponent<Props> {
return (
<View style = { styles.lonelyMeetingContainer }>
<Text
style = { [
styles.lonelyMessage,
_styles.lonelyMessage
] }>
<Text style = { styles.lonelyMessage }>
{ t('lonelyMeetingExperience.youAreAlone') }
</Text>
{ !_isInviteFunctionsDiabled && !_isInBreakoutRoom && (
<TouchableOpacity
onPress = { this._onPress }
style = { [
styles.lonelyButton,
_styles.lonelyButton
] }>
style = { styles.lonelyButton }>
<Icon
size = { 24 }
src = { IconAddPeople }
style = { styles.lonelyButtonComponents } />
<Text
style = { [
styles.lonelyButtonComponents,
_styles.lonelyMessage
] }>
<Text style = { styles.lonelyButtonComponents }>
{ t('lonelyMeetingExperience.button') }
</Text>
</TouchableOpacity>
@@ -117,8 +96,6 @@ class LonelyMeetingExperience extends PureComponent<Props> {
);
}
_onPress: () => void;
/**
* Callback for the onPress function of the button.
*
@@ -136,7 +113,7 @@ class LonelyMeetingExperience extends PureComponent<Props> {
* @private
* @returns {Props}
*/
function _mapStateToProps(state): $Shape<Props> {
function _mapStateToProps(state) {
const { disableInviteFunctions } = state['features/base/config'];
const { conference } = state['features/base/conference'];
const flag = getFeatureFlag(state, INVITE_ENABLED, true);
@@ -145,8 +122,7 @@ function _mapStateToProps(state): $Shape<Props> {
return {
_isInBreakoutRoom,
_isInviteFunctionsDiabled: !flag || disableInviteFunctions,
_isLonelyMeeting: conference && getParticipantCountWithFake(state) === 1,
_styles: ColorSchemeRegistry.get(state, 'Conference')
_isLonelyMeeting: conference && getParticipantCountWithFake(state) === 1
};
}

View File

@@ -1,11 +1,13 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, SafeAreaView, View } from 'react-native';
import { Text, View } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import { LoadingIndicator, TintedView } from '../../../../base/react';
import { isLocalVideoTrackDesktop } from '../../../../base/tracks';
import { setPictureInPictureEnabled } from '../../../../mobile/picture-in-picture/functions';
import { setIsCarmode } from '../../../../video-layout/actions';
import ConferenceTimer from '../../ConferenceTimer';
import { isConnecting } from '../../functions';
@@ -15,8 +17,6 @@ import MicrophoneButton from './MicrophoneButton';
import SoundDeviceButton from './SoundDeviceButton';
import TitleBar from './TitleBar';
import styles from './styles';
import { isLocalVideoTrackDesktop } from '../../../../base/tracks';
import { setPictureInPictureDisabled } from '../../../../mobile/picture-in-picture/functions';
/**
* Implements the carmode tab.
@@ -31,14 +31,14 @@ const CarmodeTab = (): JSX.Element => {
useEffect(() => {
dispatch(setIsCarmode(true));
setPictureInPictureDisabled(true);
setPictureInPictureEnabled(false);
return () => {
dispatch(setIsCarmode(false));
if (!isSharing) {
setPictureInPictureDisabled(false);
setPictureInPictureEnabled(true);
}
}
};
}, []);
return (

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import { openDialog } from '../../../../base/dialog/actions';
import { openSheet } from '../../../../base/dialog/actions';
import AudioRoutePickerDialog from '../../../../mobile/audio-mode/components/AudioRoutePickerDialog';
import AudioIcon from './AudioIcon';
@@ -19,7 +19,7 @@ const SelectSoundDevice = () : JSX.Element => {
const dispatch = useDispatch();
const onSelect = useCallback(() =>
dispatch(openDialog(AudioRoutePickerDialog))
dispatch(openSheet(AudioRoutePickerDialog))
, [ dispatch ]);
return (

View File

@@ -1,13 +1,16 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Text, View } from 'react-native';
import { getConferenceName } from '../../../../base/conference/functions';
import { ConnectionIndicator } from '../../../../connection-indicator';
import { getFeatureFlag, MEETING_NAME_ENABLED } from '../../../../base/flags';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';;
import { RecordingLabel } from '../../../../recording';
import { VideoQualityLabel } from '../../../../video-quality';
import { getLocalParticipant } from '../../../../base/participants';
import styles from './styles';
@@ -32,35 +35,42 @@ type Props = {
* @param {Props} props - The React props passed to this component.
* @returns {JSX.Element}
*/
const TitleBar = (props: Props) : JSX.Element => (<>
<View
pointerEvents = 'box-none'
style = { styles.titleBarWrapper }>
const TitleBar = (props: Props) : JSX.Element => {
const localParticipant = useSelector(getLocalParticipant);
const localParticipantId = localParticipant?.id;
return (<>
<View
pointerEvents = 'box-none'
style = { styles.roomNameWrapper }>
<View style = { styles.qualityLabelContainer }>
<VideoQualityLabel />
</View>
<View style = { styles.headerLabels }>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
</View>
{
props._meetingNameEnabled
&& <View style = { styles.roomNameView }>
<Text
numberOfLines = { 1 }
style = { styles.roomName }>
{props._meetingName}
</Text>
style = { styles.titleBarWrapper }>
<View
pointerEvents = 'box-none'
style = { styles.roomNameWrapper }>
<View style = { styles.qualityLabelContainer }>
<VideoQualityLabel />
</View>
}
<ConnectionIndicator
participantId = { localParticipantId }
iconStyle = { styles.connectionIndicatorIcon } />
<View style = { styles.headerLabels }>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
</View>
{
props._meetingNameEnabled
&& <View style = { styles.roomNameView }>
<Text
numberOfLines = { 1 }
style = { styles.roomName }>
{props._meetingName}
</Text>
</View>
}
</View>
</View>
</View>
</>);
</>);
}
/**
* Maps part of the Redux store to the props of this component.

View File

@@ -203,5 +203,9 @@ export default {
color: BaseTheme.palette.text01,
marginBottom: 32,
...BaseTheme.typography.bodyShortRegularLarge
},
connectionIndicatorIcon: {
fontSize: 20
}
};

View File

@@ -1,4 +1,3 @@
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { fixAndroidViewClipping } from '../../../base/styles';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
@@ -81,6 +80,7 @@ export default {
lonelyButton: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.action01,
borderRadius: 24,
flexDirection: 'row',
height: BaseTheme.spacing[6],
@@ -89,6 +89,7 @@ export default {
},
lonelyButtonComponents: {
color: BaseTheme.palette.text01,
marginHorizontal: 6
},
@@ -99,6 +100,7 @@ export default {
},
lonelyMessage: {
color: BaseTheme.palette.text01,
paddingVertical: BaseTheme.spacing[2]
},
@@ -230,13 +232,3 @@ export default {
paddingLeft: BaseTheme.spacing[2]
}
};
ColorSchemeRegistry.register('Conference', {
lonelyButton: {
backgroundColor: schemeColor('inviteButtonBackground')
},
lonelyMessage: {
color: schemeColor('onVideoText')
}
});

View File

@@ -32,6 +32,11 @@ export type Props = {
*/
participantId: string,
/**
* Custom icon style.
*/
iconStyle?: Object,
/**
* The source name of the track.
*/

View File

@@ -58,7 +58,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
}}>
<BaseIndicator
icon = { IconConnectionActive }
iconStyle = { iconStyle } />
iconStyle = { this.props.iconStyle || iconStyle } />
</View>
);
}

View File

@@ -4,7 +4,7 @@ import { NativeModules } from 'react-native';
const { Dropbox } = NativeModules;
import { setPictureInPictureDisabled } from '../mobile/picture-in-picture/functions';
import { setPictureInPictureEnabled } from '../mobile/picture-in-picture/functions';
/**
* Action to authorize the Jitsi Recording app in dropbox.
@@ -13,12 +13,12 @@ import { setPictureInPictureDisabled } from '../mobile/picture-in-picture/functi
* access token or rejected with an error.
*/
export async function _authorizeDropbox(): Promise<Object> {
setPictureInPictureDisabled(true);
setPictureInPictureEnabled(false);
try {
return await Dropbox.authorize();
} finally {
setPictureInPictureDisabled(false);
setPictureInPictureEnabled(true);
}
}

View File

@@ -2,6 +2,9 @@ import React from 'react';
import { Image } from 'react-native';
import { SvgUri } from 'react-native-svg';
// @ts-ignore
import { connect } from '../../../base/redux';
import styles from './styles';
@@ -21,13 +24,25 @@ const BrandingImageBackground: React.FC<Props> = ({ uri }:Props) => {
let backgroundImage;
if (!uri) {
return null;
}
if (imageType?.includes('.svg')) {
backgroundImage
= (
<SvgUri
height = '100%'
// Force uniform scaling.
// Align the <min-x> of the element's viewBox
// with the smallest X value of the viewport.
// Align the <min-y> of the element's viewBox
// with the smallest Y value of the viewport.
preserveAspectRatio = 'xMinYMin'
style = { styles.brandingImageBackgroundSvg }
uri = { imgSrc }
viewBox = '0 0 400 650'
width = '100%' />
);
} else {
@@ -42,4 +57,20 @@ const BrandingImageBackground: React.FC<Props> = ({ uri }:Props) => {
return backgroundImage;
};
export default BrandingImageBackground;
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code DialInLink} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state: any) {
const { backgroundImageUrl } = state['features/dynamic-branding'];
return {
uri: backgroundImageUrl
};
}
export default connect(_mapStateToProps)(BrandingImageBackground);

View File

@@ -17,17 +17,26 @@ MiddlewareRegistry.register(store => next => action => {
case SET_DYNAMIC_BRANDING_DATA: {
const {
avatarBackgrounds,
avatarBackgrounds = [],
backgroundColor,
backgroundImageUrl
backgroundImageUrl,
didPageUrl,
inviteDomain
} = action.value;
action.value = {
...action.value,
avatarBackgrounds,
backgroundColor,
backgroundImageUrl
backgroundImageUrl,
didPageUrl,
inviteDomain
};
// TODO: implement support for gradients.
action.value.avatarBackgrounds = avatarBackgrounds.filter(
color => !color.includes('linear-gradient')
);
break;
}
}

View File

@@ -189,7 +189,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
}
case TOGGLE_E2EE: {
if (conference && conference.isE2EEEnabled() !== action.enabled) {
if (conference) {
logger.debug(`E2EE will be ${action.enabled ? 'enabled' : 'disabled'}`);
conference.toggleE2EE(action.enabled);

View File

@@ -1,10 +1,7 @@
// @flow
import React, { PureComponent } from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { IconArrowBack } from '../../../base/icons';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
@@ -27,11 +24,6 @@ type Props = {
*/
_documentUrl: string,
/**
* Color schemed style of the header component.
*/
_headerStyles: Object,
/**
* Default prop for navigation between screen components(React Navigation).
*/
@@ -97,8 +89,6 @@ class SharedDocument extends PureComponent<Props> {
);
}
_renderLoading: () => React$Component<any>;
/**
* Renders the loading indicator.
*
@@ -126,8 +116,7 @@ export function _mapStateToProps(state: Object) {
const documentUrl = getSharedDocumentUrl(state);
return {
_documentUrl: documentUrl,
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
_documentUrl: documentUrl
};
}

View File

@@ -1,12 +1,7 @@
import { setWasmPaths } from '@tensorflow/tfjs-backend-wasm';
import { Human, Config, FaceResult } from '@vladmandic/human';
import { DETECTION_TYPES, FACE_EXPRESSIONS_NAMING_MAPPING } from './constants';
type Detection = {
detections: Array<FaceResult>,
threshold?: number
};
import { DETECTION_TYPES, FACE_DETECTION_SCORE_THRESHOLD, FACE_EXPRESSIONS_NAMING_MAPPING } from './constants';
type DetectInput = {
image: ImageBitmap | ImageData,
@@ -21,20 +16,22 @@ type FaceBox = {
type InitInput = {
baseUrl: string,
detectionTypes: string[],
maxFacesDetected?: number
detectionTypes: string[]
}
type DetectOutput = {
faceExpression?: string,
faceBox?: FaceBox
faceBox?: FaceBox,
faceCount: number
};
export interface FaceLandmarksHelper {
getFaceBox({ detections, threshold }: Detection): FaceBox | undefined;
getFaceExpression({ detections }: Detection): string | undefined;
getFaceBox(detections: Array<FaceResult>, threshold: number): FaceBox | undefined;
getFaceExpression(detections: Array<FaceResult>): string | undefined;
getFaceCount(detections : Array<FaceResult>): number;
getDetections(image: ImageBitmap | ImageData): Promise<Array<FaceResult>>;
init(): Promise<void>;
detect({ image, threshold } : DetectInput): Promise<DetectOutput | undefined>;
detect({ image, threshold } : DetectInput): Promise<DetectOutput>;
getDetectionInProgress(): boolean;
}
@@ -45,7 +42,6 @@ export class HumanHelper implements FaceLandmarksHelper {
protected human: Human | undefined;
protected faceDetectionTypes: string[];
protected baseUrl: string;
protected maxFacesDetected?: number;
private detectionInProgress = false;
private lastValidFaceBox: FaceBox | undefined;
/**
@@ -66,7 +62,7 @@ export class HumanHelper implements FaceLandmarksHelper {
enabled: false,
rotation: false,
modelPath: 'blazeface-front.json',
maxDetected: 4
maxDetected: 20
},
mesh: { enabled: false },
iris: { enabled: false },
@@ -82,10 +78,9 @@ export class HumanHelper implements FaceLandmarksHelper {
segmentation: { enabled: false }
};
constructor({ baseUrl, detectionTypes, maxFacesDetected }: InitInput) {
constructor({ baseUrl, detectionTypes }: InitInput) {
this.faceDetectionTypes = detectionTypes;
this.baseUrl = baseUrl;
this.maxFacesDetected = maxFacesDetected;
this.init();
}
@@ -102,10 +97,6 @@ export class HumanHelper implements FaceLandmarksHelper {
if (this.faceDetectionTypes.length > 0 && this.config.face) {
this.config.face.enabled = true
}
if (this.maxFacesDetected && this.config.face?.detector) {
this.config.face.detector.maxDetected = this.maxFacesDetected;
}
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_BOX) && this.config.face?.detector) {
this.config.face.detector.enabled = true;
@@ -126,15 +117,15 @@ export class HumanHelper implements FaceLandmarksHelper {
}
}
getFaceBox({ detections, threshold }: Detection): FaceBox | undefined {
if (!detections.length) {
getFaceBox(detections: Array<FaceResult>, threshold: number): FaceBox | undefined {
if (this.getFaceCount(detections) !== 1) {
return;
}
const faceBox: FaceBox = {
// normalize to percentage based
left: Math.round(Math.min(...detections.map(d => d.boxRaw[0])) * 100),
right: Math.round(Math.max(...detections.map(d => d.boxRaw[0] + d.boxRaw[2])) * 100)
left: Math.round(detections[0].boxRaw[0] * 100),
right: Math.round((detections[0].boxRaw[0] + detections[0].boxRaw[2]) * 100)
};
faceBox.width = Math.round(faceBox.right - faceBox.left);
@@ -148,52 +139,75 @@ export class HumanHelper implements FaceLandmarksHelper {
return faceBox;
}
getFaceExpression({ detections }: Detection): string | undefined {
if (detections[0]?.emotion) {
return FACE_EXPRESSIONS_NAMING_MAPPING[detections[0]?.emotion[0].emotion];
getFaceExpression(detections: Array<FaceResult>): string | undefined {
if (this.getFaceCount(detections) !== 1) {
return;
}
if (detections[0].emotion) {
return FACE_EXPRESSIONS_NAMING_MAPPING[detections[0].emotion[0].emotion];
}
}
public async detect({ image, threshold } : DetectInput): Promise<DetectOutput | undefined> {
getFaceCount(detections: Array<FaceResult> | undefined): number {
if (detections) {
return detections.length;
}
return 0;
}
async getDetections(image: ImageBitmap | ImageData): Promise<Array<FaceResult>> {
if (!this.human || !this.faceDetectionTypes.length) {
return [];
}
this.human.tf.engine().startScope();
const imageTensor = this.human.tf.browser.fromPixels(image);
const { face: detections } = await this.human.detect(imageTensor, this.config);
this.human.tf.engine().endScope();
return detections.filter(detection => detection.score > FACE_DETECTION_SCORE_THRESHOLD);
}
public async detect({ image, threshold } : DetectInput): Promise<DetectOutput> {
let detections;
let faceExpression;
let faceBox;
if (!this.human){
return;
}
this.detectionInProgress = true;
this.human.tf.engine().startScope();
const imageTensor = this.human.tf.browser.fromPixels(image);
detections = await this.getDetections(image);
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_EXPRESSIONS)) {
const { face } = await this.human.detect(imageTensor, this.config);
detections = face;
faceExpression = this.getFaceExpression({ detections });
faceExpression = this.getFaceExpression(detections);
}
if (this.faceDetectionTypes.includes(DETECTION_TYPES.FACE_BOX)) {
if (!detections) {
const { face } = await this.human.detect(imageTensor, this.config);
//if more than one face is detected the face centering will be disabled.
if (this.getFaceCount(detections) > 1 ) {
this.faceDetectionTypes.splice(this.faceDetectionTypes.indexOf(DETECTION_TYPES.FACE_BOX), 1);
detections = face;
//face-box for re-centering
faceBox = {
left: 0,
right: 100,
width: 100,
};
} else {
faceBox = this.getFaceBox(detections, threshold);
}
faceBox = this.getFaceBox({
detections,
threshold
});
}
this.human.tf.engine().endScope();
this.detectionInProgress = false;
return {
faceExpression,
faceBox
faceBox,
faceCount: this.getFaceCount(detections)
}
}

View File

@@ -140,8 +140,7 @@ export function loadWorker() {
worker.postMessage({
type: INIT_WORKER,
baseUrl,
detectionTypes,
maxFacesDetected: faceLandmarks?.maxFacesDetected
detectionTypes
});
dispatch(startFaceLandmarksDetection());

View File

@@ -55,3 +55,8 @@ export const DETECTION_TYPES = {
FACE_BOX: 'face-box',
FACE_EXPRESSIONS: 'face-expressions'
};
/**
* Threshold for detection score of face.
*/
export const FACE_DETECTION_SCORE_THRESHOLD = 0.6;

View File

@@ -13,7 +13,7 @@ onmessage = async function(message: MessageEvent<any>) {
const detections = await helper.detect(message.data);
if (detections && (detections.faceBox || detections.faceExpression)) {
if (detections && (detections.faceBox || detections.faceExpression || detections.faceCount)) {
self.postMessage(detections);
}

View File

@@ -26,13 +26,13 @@ export function setTileViewDimensions() {
const columns = getColumnCount(state);
const rows = Math.ceil(participantCount / columns);
const conferenceBorder = conferenceStyles.conference.borderWidth || 0;
const heightToUse = height - top - (2 * conferenceBorder);
const heightToUse = height - top - bottom - (2 * conferenceBorder);
const widthToUse = width - (TILE_MARGIN * 2) - left - right - (2 * conferenceBorder);
let tileWidth;
// If there is going to be at least two rows, ensure that at least two
// rows display fully on screen.
if (rows / columns > 1) {
if (participantCount / columns > 1) {
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
} else {
tileWidth = Math.min(widthToUse / columns, heightToUse);

View File

@@ -50,6 +50,7 @@ import {
isFilmstripResizable,
showGridInVerticalView
} from './functions';
import { isStageFilmstripAvailable } from './functions.web';
export * from './actions.any';
@@ -334,9 +335,17 @@ export function clickOnVideo(n: number) {
// Use the list that correctly represents the current order of the participants as visible in the UI.
const { remoteParticipants } = state['features/filmstrip'];
const participants = [ localId, ...remoteParticipants ];
if (participants.length - 1 < n) {
return;
}
const { id, pinned } = getParticipantById(state, participants[n]);
dispatch(pinParticipant(pinned ? null : id));
if (isStageFilmstripAvailable(state)) {
dispatch(togglePinStageParticipant(id));
} else {
dispatch(pinParticipant(pinned ? null : id));
}
};
}

View File

@@ -7,7 +7,7 @@ import { useSelector } from 'react-redux';
import { IconPinParticipant } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { BaseIndicator } from '../../../base/react';
import { getPinnedActiveParticipants, isStageFilmstripEnabled } from '../../functions.web';
import { getPinnedActiveParticipants, isStageFilmstripAvailable } from '../../functions.web';
/**
* The type of the React {@code Component} props of {@link PinnedIndicator}.
@@ -54,7 +54,7 @@ const PinnedIndicator = ({
participantId,
tooltipPosition
}: Props) => {
const stageFilmstrip = useSelector(isStageFilmstripEnabled);
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
const pinned = useSelector(state => getParticipantById(state, participantId))?.pinned;
const isPinned = useSelector(getPinnedActiveParticipants).find(p => p.participantId === participantId);
const styles = useStyles();

View File

@@ -230,7 +230,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
data = { inviteItems }
horizontal = { true }
keyExtractor = { this._keyExtractor }
keyboardShouldPersistTaps = 'always'
renderItem = { this._renderInvitedItem } />
</View> }
<View style = { styles.resultList }>
@@ -239,7 +238,6 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
data = { selectableItems }
extraData = { inviteItems }
keyExtractor = { this._keyExtractor }
keyboardShouldPersistTaps = 'always'
renderItem = { this._renderItem } />
</View>
</JitsiScreen>

View File

@@ -1,23 +1,21 @@
// @flow
import React, { PureComponent } from 'react';
import { Linking, Platform, View } from 'react-native';
import { Linking, View } from 'react-native';
import { WebView } from 'react-native-webview';
import { type Dispatch } from 'redux';
import { openDialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { IconClose } from '../../../../base/icons';
import JitsiScreen from '../../../../base/modal/components/JitsiScreen';
import { LoadingIndicator } from '../../../../base/react';
import { connect } from '../../../../base/redux';
import HeaderNavigationButton
from '../../../../mobile/navigation/components/HeaderNavigationButton';
import { getDialInfoPageURLForURIString } from '../../../functions';
import DialInSummaryErrorDialog from './DialInSummaryErrorDialog';
import styles, { INDICATOR_COLOR } from './styles';
type Props = {
dispatch: Dispatch<any>,
@@ -65,28 +63,9 @@ class DialInSummary extends PureComponent<Props> {
*/
componentDidMount() {
const { navigation, t } = this.props;
const onNavigationClose = () => {
navigation.goBack();
};
navigation.setOptions({
headerLeft: () => {
if (Platform.OS === 'ios') {
return (
<HeaderNavigationButton
label = { t('dialog.close') }
// eslint-disable-next-line react/jsx-no-bind
onPress = { onNavigationClose } />
);
}
return (
<HeaderNavigationButton
// eslint-disable-next-line react/jsx-no-bind
onPress = { onNavigationClose }
src = { IconClose } />
);
}
headerTitle: t('dialIn.screenTitle')
});
}

View File

@@ -8,7 +8,11 @@ import { i18next } from '../base/i18n';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
import { toState } from '../base/redux';
import { parseURIString } from '../base/util';
import {
appendURLParam,
parseURIString,
parseURLParams
} from '../base/util';
import { isVpaasMeeting } from '../jaas/functions';
import { getDialInConferenceID, getDialInNumbers } from './_utils';
@@ -22,6 +26,13 @@ import logger from './logger';
declare var $: Function;
declare var interfaceConfig: Object;
export const sharingFeatures = {
email: 'email',
url: 'url',
dialIn: 'dial-in',
embed: 'embed'
};
/**
* Sends an ajax request to check if the phone number can be called.
*
@@ -242,7 +253,7 @@ export function getInviteTextiOS({
invite += t('info.inviteTextiOSInviteUrl', { inviteUrl });
invite += ' ';
if (shouldDisplayDialIn(dialIn)) {
if (shouldDisplayDialIn(dialIn) && isSharingEnabled(sharingFeatures.dialIn)) {
invite += t('info.inviteTextiOSPhone', {
number: phoneNumber,
conferenceID: dialIn.conferenceID,
@@ -291,7 +302,7 @@ export function getInviteText({
invite = `${invite}\n${liveStream}`;
}
if (shouldDisplayDialIn(dialIn)) {
if (shouldDisplayDialIn(dialIn) && isSharingEnabled(sharingFeatures.dialIn)) {
const dial = t('info.invitePhone', {
number: phoneNumber,
conferenceID: dialIn.conferenceID
@@ -596,7 +607,7 @@ export function getDialInfoPageURL(state: Object, roomName: ?string) {
const url = didPageUrl || `${href.substring(0, href.lastIndexOf('/'))}/${DIAL_IN_INFO_PAGE_PATH_NAME}`;
return `${url}?room=${room}`;
return appendURLParam(url, 'room', room);
}
/**
@@ -611,8 +622,15 @@ export function getDialInfoPageURLForURIString(
return undefined;
}
const { protocol, host, contextRoot, room } = parseURIString(uri);
let url = `${protocol}//${host}${contextRoot}${DIAL_IN_INFO_PAGE_PATH_NAME}`;
return `${protocol}//${host}${contextRoot}${DIAL_IN_INFO_PAGE_PATH_NAME}?room=${room}`;
url = appendURLParam(url, 'room', room);
const { release } = parseURLParams(uri, true, 'search');
release && (url = appendURLParam(url, 'release', release));
return url;
}
/**
@@ -790,13 +808,6 @@ export async function executeDialOutStatusRequest(url: string, reqId: string) {
return res.ok ? json : Promise.reject(json);
}
export const sharingFeatures = {
email: 'email',
url: 'url',
dialIn: 'dial-in',
embed: 'embed'
};
/**
* Returns true if a specific sharing feature is enabled in interface configuration.
*

View File

@@ -9,11 +9,8 @@ import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui';
import BaseTheme from '../../../base/ui/components/BaseTheme';
import InviteButton
from '../../../invite/components/add-people-dialog/native/InviteButton';
import { BrandingImageBackground } from '../../../dynamic-branding';
import { LargeVideo } from '../../../large-video/components';
import HeaderNavigationButton
from '../../../mobile/navigation/components/HeaderNavigationButton';
import { navigate }
from '../../../mobile/navigation/components/lobby/LobbyNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
@@ -38,25 +35,6 @@ type Props = AbstractProps & {
* Implements a waiting screen that represents the participant being in the lobby.
*/
class LobbyScreen extends AbstractLobbyScreen<Props> {
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
const { navigation, t } = this.props;
navigation.setOptions({
headerLeft: () => (
<HeaderNavigationButton
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
)
});
}
/**
* Implements {@code PureComponent#render}.
*
@@ -64,31 +42,31 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
*/
render() {
const { _aspectRatio } = this.props;
let contentStyles;
let largeVideoContainerStyles;
let contentWrapperStyles;
let contentContainerStyles;
let largeVideoContainerStyles;
if (_aspectRatio === ASPECT_RATIO_NARROW) {
contentWrapperStyles = styles.contentWrapper;
largeVideoContainerStyles = styles.largeVideoContainer;
contentContainerStyles = styles.contentContainer;
} else {
contentStyles = styles.contentWide;
contentWrapperStyles = styles.contentWrapperWide;
largeVideoContainerStyles = styles.largeVideoContainerWide;
contentContainerStyles = styles.contentContainerWide;
}
return (
<JitsiScreen
safeAreaInsets = { [ 'right' ] }
style = { styles.contentWrapper }>
<View style = { contentStyles }>
<View style = { largeVideoContainerStyles }>
<LargeVideo />
</View>
<View style = { contentContainerStyles }>
{ this._renderContent() }
{ this._renderToolbarButtons() }
</View>
safeAreaInsets = { [ 'left' ] }
style = { contentWrapperStyles }>
<BrandingImageBackground />
<View style = { largeVideoContainerStyles }>
<LargeVideo />
</View>
<View style = { contentContainerStyles }>
{ this._renderContent() }
{ this._renderToolbarButtons() }
</View>
</JitsiScreen>
);
@@ -137,7 +115,7 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
*/
_renderJoining() {
return (
<View style = { styles.formWrapper }>
<View>
<LoadingIndicator
color = { BaseTheme.palette.icon01 }
style = { styles.loadingIndicator } />
@@ -159,15 +137,11 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
const { displayName } = this.state;
return (
<View style = { styles.formWrapper }>
<Text style = { styles.fieldLabel }>
{ t('lobby.nameField') }
</Text>
<TextInput
onChangeText = { this._onChangeDisplayName }
style = { styles.field }
value = { displayName } />
</View>
<TextInput
onChangeText = { this._onChangeDisplayName }
placeholder = { t('lobby.nameField') }
style = { styles.field }
value = { displayName } />
);
}
@@ -190,13 +164,11 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
return (
<View style = { styles.formWrapper }>
<Text style = { styles.fieldLabel }>
{ this.props.t('lobby.passwordField') }
</Text>
<TextInput
autoCapitalize = 'none'
autoCompleteType = 'off'
onChangeText = { this._onChangePassword }
placeholder = { t('lobby.passwordField') }
secureTextEntry = { true }
style = { styles.field }
value = { this.state.password } />
@@ -263,8 +235,6 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
styles = { styles.buttonStylesBorderless } />
<VideoMuteButton
styles = { styles.buttonStylesBorderless } />
<InviteButton
styles = { styles.buttonStylesBorderless } />
</View>
);
}

View File

@@ -15,18 +15,17 @@ export default {
buttonStylesBorderless: {
iconStyle: {
backgroundColor: BaseTheme.palette.action02Active,
color: BaseTheme.palette.icon01,
fontSize: 24
},
style: {
backgroundColor: BaseTheme.palette.action02Active,
flexDirection: 'row',
justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[3],
height: 24,
width: 24
}
},
underlayColor: 'transparent'
},
lobbyChatWrapper: {
@@ -57,37 +56,41 @@ export default {
},
contentWrapper: {
backgroundColor: BaseTheme.palette.ui02,
flex: 1
},
contentWide: {
backgroundColor: BaseTheme.palette.ui02,
contentWrapperWide: {
flex: 1,
flexDirection: 'row'
},
largeVideoContainer: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
minHeight: '50%'
},
largeVideoContainerWide: {
height: '100%',
marginRight: 'auto',
position: 'absolute',
width: '50%'
},
contentContainer: {
alignSelf: 'center',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
justifyContent: 'center',
minHeight: '50%',
paddingHorizontal: BaseTheme.spacing[3],
width: 400
},
contentContainerWide: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
marginHorizontal: BaseTheme.spacing[2],
left: '50%',
paddingHorizontal: BaseTheme.spacing[3],
position: 'absolute',
width: '50%'
},
@@ -96,21 +99,13 @@ export default {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
marginTop: BaseTheme.spacing[4]
marginTop: BaseTheme.spacing[3]
},
toolboxContainerWide: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: BaseTheme.spacing[4]
},
dialogTitle: {
fontSize: 18,
fontWeight: 'bold',
margin: 'auto',
marginVertical: BaseTheme.spacing[3],
textAlign: 'center'
marginTop: BaseTheme.spacing[3]
},
displayNameText: {
@@ -128,14 +123,22 @@ export default {
fontSize: 16
},
formWrapper: {
alignSelf: 'stretch',
justifyContent: 'center'
},
field: {
alignSelf: 'stretch',
backgroundColor: BaseTheme.palette.field02,
borderColor: SECONDARY_COLOR,
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 2,
height: BaseTheme.spacing[7],
marginHorizontal: BaseTheme.spacing[3],
padding: BaseTheme.spacing[2]
marginTop: BaseTheme.spacing[3],
marginHorizontal: 12,
padding: BaseTheme.spacing[2],
textAlign: 'center'
},
fieldError: {
@@ -147,28 +150,17 @@ export default {
fieldLabel: {
...BaseTheme.typography.heading6,
color: BaseTheme.palette.text01,
marginVertical: BaseTheme.spacing[4],
textAlign: 'center'
},
formWrapper: {
alignSelf: 'stretch'
},
standardButtonWrapper: {
alignSelf: 'stretch',
marginHorizontal: BaseTheme.spacing[3]
},
joiningContainer: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center'
marginHorizontal: 12
},
joiningMessage: {
color: BaseTheme.palette.text01,
marginBottom: BaseTheme.spacing[2],
marginHorizontal: BaseTheme.spacing[3],
textAlign: 'center'
},
@@ -179,7 +171,7 @@ export default {
},
loadingIndicator: {
marginVertical: BaseTheme.spacing[4]
marginBottom: BaseTheme.spacing[4]
},
participantBox: {
@@ -194,12 +186,12 @@ export default {
primaryButton: {
backgroundColor: BaseTheme.palette.action01,
marginTop: BaseTheme.spacing[4]
marginTop: BaseTheme.spacing[3]
},
primaryButtonDisabled: {
backgroundColor: BaseTheme.palette.action03Disabled,
marginTop: BaseTheme.spacing[4]
marginTop: BaseTheme.spacing[3]
},
primaryButtonText: {

View File

@@ -1,7 +1,6 @@
// @flow
import type { Dispatch } from 'redux';
import { openDialog } from '../../../base/dialog';
import { openSheet } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconVolumeEmpty } from '../../../base/icons';
import { connect } from '../../../base/redux';
@@ -32,7 +31,7 @@ class AudioDeviceToggleButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.dispatch(openDialog(AudioRoutePickerDialog));
this.props.dispatch(openSheet(AudioRoutePickerDialog));
}
}

View File

@@ -1,11 +1,9 @@
// @flow
import _ from 'lodash';
import React, { Component } from 'react';
import { NativeModules, Text, TouchableHighlight, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { hideDialog, BottomSheet } from '../../../base/dialog';
import { hideSheet, BottomSheet } from '../../../base/dialog';
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
import { translate } from '../../../base/i18n';
import {
Icon,
@@ -16,7 +14,7 @@ import {
IconDeviceSpeaker
} from '../../../base/icons';
import { connect } from '../../../base/redux';
import { ColorPalette, type StyleType } from '../../../base/styles';
import { ColorPalette } from '../../../base/styles';
import styles from './styles';
@@ -85,11 +83,6 @@ type RawDevice = {
*/
type Props = {
/**
* Style of the bottom sheet feature.
*/
_bottomSheetStyles: StyleType,
/**
* Object describing available devices.
*/
@@ -148,11 +141,6 @@ const deviceInfoMap = {
}
};
/**
* The exported React {@code Component}.
*/
let AudioRoutePickerDialog_; // eslint-disable-line prefer-const
/**
* Implements a React {@code Component} which prompts the user when a password
* is required to join a conference.
@@ -223,36 +211,10 @@ class AudioRoutePickerDialog extends Component<Props, State> {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
// Trigger an initial update.
AudioMode.updateDeviceList && AudioMode.updateDeviceList();
}
/**
* Dispatches a redux action to hide this sheet.
*
* @returns {void}
*/
_hide() {
this.props.dispatch(hideDialog(AudioRoutePickerDialog_));
}
_onCancel: () => void;
/**
* Cancels the dialog by hiding it.
*
* @private
* @returns {void}
*/
_onCancel() {
this._hide();
}
_onSelectDeviceFn: (Device) => Function;
/**
* Builds and returns a function which handles the selection of a device
* on the sheet. The selected device will be used by {@code AudioMode}.
@@ -263,7 +225,7 @@ class AudioRoutePickerDialog extends Component<Props, State> {
*/
_onSelectDeviceFn(device: Device) {
return () => {
this._hide();
this.props.dispatch(hideSheet());
AudioMode.setAudioDevice(device.uid || device.type);
};
}
@@ -276,7 +238,6 @@ class AudioRoutePickerDialog extends Component<Props, State> {
* @returns {ReactElement}
*/
_renderDevice(device: Device) {
const { _bottomSheetStyles } = this.props;
const { icon, selected, text } = device;
const selectedStyle = selected ? styles.selectedText : {};
@@ -288,8 +249,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
<View style = { styles.deviceRow } >
<Icon
src = { icon }
style = { [ styles.deviceIcon, _bottomSheetStyles.buttons.iconStyle, selectedStyle ] } />
<Text style = { [ styles.deviceText, _bottomSheetStyles.buttons.labelStyle, selectedStyle ] } >
style = { [ styles.deviceIcon, bottomSheetStyles.buttons.iconStyle, selectedStyle ] } />
<Text style = { [ styles.deviceText, bottomSheetStyles.buttons.labelStyle, selectedStyle ] } >
{ text }
</Text>
</View>
@@ -304,14 +265,14 @@ class AudioRoutePickerDialog extends Component<Props, State> {
* @returns {ReactElement}
*/
_renderNoDevices() {
const { _bottomSheetStyles, t } = this.props;
const { t } = this.props;
return (
<View style = { styles.deviceRow } >
<Icon
src = { deviceInfoMap.SPEAKER.icon }
style = { [ styles.deviceIcon, _bottomSheetStyles.buttons.iconStyle ] } />
<Text style = { [ styles.deviceText, _bottomSheetStyles.buttons.labelStyle ] } >
style = { [ styles.deviceIcon, bottomSheetStyles.buttons.iconStyle ] } />
<Text style = { [ styles.deviceText, bottomSheetStyles.buttons.labelStyle ] } >
{ t('audioDevices.none') }
</Text>
</View>
@@ -335,7 +296,7 @@ class AudioRoutePickerDialog extends Component<Props, State> {
}
return (
<BottomSheet onCancel = { this._onCancel }>
<BottomSheet>
{ content }
</BottomSheet>
);
@@ -350,11 +311,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
*/
function _mapStateToProps(state) {
return {
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
_devices: state['features/mobile/audio-mode'].devices
};
}
AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
export default AudioRoutePickerDialog_;
export default translate(connect(_mapStateToProps)(AudioRoutePickerDialog));

View File

@@ -1,8 +1,11 @@
// @flow
import debounce from 'lodash/debounce';
import { NativeModules } from 'react-native';
import { getAppProp } from '../../base/app';
import { readyToClose } from '../external-api/actions';
/**
* Sends a specific event to the native counterpart of the External API. Native
@@ -24,3 +27,10 @@ export function sendEvent(store: Object, name: string, data: Object) {
externalAPIScope
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
}
/**
* Debounced sending of `readyToClose`.
*/
export const _sendReadyToClose = debounce(dispatch => {
dispatch(readyToClose());
}, 2500, { leading: true });

View File

@@ -35,11 +35,12 @@ import {
getLocalParticipant
} from '../../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
import { toggleScreensharing } from '../../base/tracks';
import { getLocalTracks, isLocalTrackMuted, toggleScreensharing } from '../../base/tracks';
import { OPEN_CHAT, CLOSE_CHAT } from '../../chat';
import { openChat } from '../../chat/actions';
import { sendMessage, setPrivateMessageRecipient, closeChat } from '../../chat/actions.any';
import { SET_PAGE_RELOAD_OVERLAY_CANCELED } from '../../overlay/actionTypes';
import { setRequestingSubtitles } from '../../subtitles';
import { muteLocal } from '../../video-menu/actions';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
@@ -372,6 +373,9 @@ function _registerForNativeEvents(store) {
dispatch(sendMessage(message));
});
eventEmitter.addListener(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED, ({ enabled }) => {
dispatch(setRequestingSubtitles(enabled));
});
}
/**
@@ -390,6 +394,7 @@ function _unregisterForNativeEvents() {
eventEmitter.removeAllListeners(ExternalAPI.OPEN_CHAT);
eventEmitter.removeAllListeners(ExternalAPI.CLOSE_CHAT);
eventEmitter.removeAllListeners(ExternalAPI.SEND_CHAT_MESSAGE);
eventEmitter.removeAllListeners(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED);
}
/**
@@ -529,6 +534,11 @@ function _sendConferenceEvent(
// transport an "equivalent".
if (conference) {
data.url = _normalizeUrl(conference[JITSI_CONFERENCE_URL_KEY]);
const localTracks = getLocalTracks(store.getState()['features/base/tracks']);
const isAudioMuted = isLocalTrackMuted(localTracks, MEDIA_TYPE.AUDIO);
data.isAudioMuted = isAudioMuted;
}
if (_swallowEvent(store, action, data)) {

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