mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-06 23:02:28 +00:00
Compare commits
39 Commits
7851
...
android-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a38ba6257 | ||
|
|
36dfb4c956 | ||
|
|
29c33ed38b | ||
|
|
c159a54fbf | ||
|
|
107a5b845c | ||
|
|
6953255375 | ||
|
|
d358dd8ec6 | ||
|
|
3d158fb2b4 | ||
|
|
b7785a9f91 | ||
|
|
86d869a107 | ||
|
|
1c81b93c1d | ||
|
|
052070a6c1 | ||
|
|
c531c0e65c | ||
|
|
e1055ebf9b | ||
|
|
467023f77a | ||
|
|
1249aa2dcb | ||
|
|
0c45d87d1a | ||
|
|
7140a90201 | ||
|
|
0a846606fc | ||
|
|
68dc111e3c | ||
|
|
c81184df69 | ||
|
|
9b0747a0d9 | ||
|
|
c8cd80a8df | ||
|
|
f1d4332668 | ||
|
|
55b3256dc4 | ||
|
|
aa8bb55f3e | ||
|
|
58b73e21de | ||
|
|
b1c955890a | ||
|
|
6ab945c2cb | ||
|
|
7291e1ef00 | ||
|
|
43e075d48e | ||
|
|
885e1afdaa | ||
|
|
e2ec4842a1 | ||
|
|
ea075d9bae | ||
|
|
68f7448624 | ||
|
|
954ef6df4f | ||
|
|
6a3c12b316 | ||
|
|
5be616a224 | ||
|
|
58d8f3be12 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -99,10 +99,7 @@ tsconfig.json
|
||||
#
|
||||
react-native-sdk/*.tgz
|
||||
react-native-sdk/android/src
|
||||
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
|
||||
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetReactNativePackage.java
|
||||
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JMOngoingConferenceModule.java
|
||||
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/RNOngoingNotification.java
|
||||
react-native-sdk/images
|
||||
react-native-sdk/ios
|
||||
react-native-sdk/lang
|
||||
|
||||
@@ -19,9 +19,9 @@ buildscript {
|
||||
ext {
|
||||
kotlinVersion = "1.7.0"
|
||||
buildToolsVersion = "33.0.2"
|
||||
compileSdkVersion = 33
|
||||
compileSdkVersion = 34
|
||||
minSdkVersion = 24
|
||||
targetSdkVersion = 33
|
||||
targetSdkVersion = 34
|
||||
supportLibVersion = "28.0.0"
|
||||
|
||||
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
|
||||
|
||||
@@ -26,5 +26,5 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
appVersion=99.0.0
|
||||
sdkVersion=99.0.0
|
||||
appVersion=24.0.1
|
||||
sdkVersion=9.0.1
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<uses-feature
|
||||
@@ -51,10 +50,6 @@
|
||||
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<service
|
||||
android:name="org.jitsi.meet.sdk.JitsiMeetMediaProjectionService"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
|
||||
<provider
|
||||
android:name="com.reactnativecommunity.webview.RNCWebViewFileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@ReactModule(name = JitsiMeetMediaProjectionModule.NAME)
|
||||
class JitsiMeetMediaProjectionModule
|
||||
extends ReactContextBaseJavaModule {
|
||||
|
||||
public static final String NAME = "JitsiMeetMediaProjectionModule";
|
||||
|
||||
public JitsiMeetMediaProjectionModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void launch() {
|
||||
Context context = getReactApplicationContext();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
JitsiMeetMediaProjectionService.launch(context, currentActivity);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void abort() {
|
||||
Context context = getReactApplicationContext();
|
||||
|
||||
JitsiMeetMediaProjectionService.abort(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* The service will help keep the app running while in the background.
|
||||
*
|
||||
* See: https://developer.android.com/guide/components/services
|
||||
*/
|
||||
public class JitsiMeetMediaProjectionService extends Service {
|
||||
private static final String TAG = JitsiMeetMediaProjectionService.class.getSimpleName();
|
||||
|
||||
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
|
||||
|
||||
public static void launch(Context context, Activity currentActivity) {
|
||||
|
||||
NotificationUtils.createNotificationChannel(currentActivity);
|
||||
|
||||
Intent intent = new Intent(context, JitsiMeetMediaProjectionService.class);
|
||||
|
||||
ComponentName componentName;
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
componentName = context.startForegroundService(intent);
|
||||
} else {
|
||||
componentName = context.startService(intent);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
// Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
|
||||
// See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
|
||||
JitsiMeetLogger.w(TAG + "Media projection service not started", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (componentName == null) {
|
||||
JitsiMeetLogger.w(TAG + "Media projection service not started");
|
||||
}
|
||||
}
|
||||
|
||||
public static void abort(Context context) {
|
||||
Intent intent = new Intent(context, JitsiMeetMediaProjectionService.class);
|
||||
context.stopService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
|
||||
Notification notification = MediaProjectionNotification.buildMediaProjectionNotification(this);
|
||||
|
||||
if (notification == null) {
|
||||
stopSelf();
|
||||
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
@@ -58,8 +58,8 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
|
||||
|
||||
public static void launch(Context context, HashMap<String, Object> extraData) {
|
||||
|
||||
NotificationUtils.createNotificationChannel((Activity) context);
|
||||
|
||||
OngoingNotification.createNotificationChannel((Activity) context);
|
||||
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import static org.jitsi.meet.sdk.NotificationUtils.ONGOING_CONFERENCE_CHANNEL_ID;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
/**
|
||||
* Helper class for creating the media projection notification which is used with
|
||||
* {@link JitsiMeetMediaProjectionService}.
|
||||
*/
|
||||
class MediaProjectionNotification {
|
||||
private static final String TAG = MediaProjectionNotification.class.getSimpleName();
|
||||
|
||||
static Notification buildMediaProjectionNotification(Context context) {
|
||||
|
||||
if (context == null) {
|
||||
JitsiMeetLogger.d(TAG, " Cannot create notification: no current context");
|
||||
return null;
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ONGOING_CONFERENCE_CHANNEL_ID);
|
||||
|
||||
builder
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setContentTitle(context.getString(R.string.media_projection_notification_title))
|
||||
.setContentText(context.getString(R.string.media_projection_notification_text))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(false)
|
||||
.setUsesChronometer(false)
|
||||
.setAutoCancel(true)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class NotificationUtils {
|
||||
|
||||
static final String ONGOING_CONFERENCE_CHANNEL_ID = "JitsiOngoingConferenceChannel";
|
||||
|
||||
public static List<String> allIds = new ArrayList<String>() {{ add(ONGOING_CONFERENCE_CHANNEL_ID); }};
|
||||
|
||||
private static final String TAG = NotificationUtils.class.getSimpleName();
|
||||
|
||||
static void createNotificationChannel(Activity context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager
|
||||
= (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
NotificationChannel channel
|
||||
= notificationManager.getNotificationChannel(ONGOING_CONFERENCE_CHANNEL_ID);
|
||||
|
||||
if (channel != null) {
|
||||
// The channel was already created, no need to do it again.
|
||||
return;
|
||||
}
|
||||
|
||||
channel = new NotificationChannel(ONGOING_CONFERENCE_CHANNEL_ID, context.getString(R.string.ongoing_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(false);
|
||||
channel.setShowBadge(false);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,11 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import static org.jitsi.meet.sdk.NotificationUtils.ONGOING_CONFERENCE_CHANNEL_ID;
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
@@ -26,7 +29,8 @@ import android.content.Intent;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
import android.os.Build;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for creating the ongoing notification which is used with
|
||||
@@ -38,6 +42,37 @@ class OngoingNotification {
|
||||
|
||||
private static long startingTime = 0;
|
||||
|
||||
static final String ONGOING_CONFERENCE_CHANNEL_ID = "JitsiOngoingConferenceChannel";
|
||||
|
||||
static void createNotificationChannel(Activity context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context == null) {
|
||||
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager
|
||||
= (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
NotificationChannel channel
|
||||
= notificationManager.getNotificationChannel(ONGOING_CONFERENCE_CHANNEL_ID);
|
||||
|
||||
if (channel != null) {
|
||||
// The channel was already created, no need to do it again.
|
||||
return;
|
||||
}
|
||||
|
||||
channel = new NotificationChannel(ONGOING_CONFERENCE_CHANNEL_ID, context.getString(R.string.ongoing_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(false);
|
||||
channel.setShowBadge(false);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
static Notification buildOngoingConferenceNotification(Boolean isMuted, Context context) {
|
||||
|
||||
if (context == null) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreenModule;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.Logging;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
@@ -67,7 +68,6 @@ class ReactInstanceManagerHolder {
|
||||
new DropboxModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new JavaScriptSandboxModule(reactContext),
|
||||
new JitsiMeetMediaProjectionModule(reactContext),
|
||||
new LocaleDetector(reactContext),
|
||||
new LogBridgeModule(reactContext),
|
||||
new SplashScreenModule(reactContext),
|
||||
@@ -241,6 +241,8 @@ class ReactInstanceManagerHolder {
|
||||
|
||||
options.videoDecoderFactory = new H264AndSoftwareVideoDecoderFactory(eglContext);
|
||||
options.videoEncoderFactory = new H264AndSoftwareVideoEncoderFactory(eglContext);
|
||||
options.enableMediaProjectionService = true;
|
||||
// options.loggingSeverity = Logging.Severity.LS_INFO;
|
||||
|
||||
Log.d(TAG, "initializing RN with Activity");
|
||||
|
||||
|
||||
@@ -329,6 +329,8 @@ var config = {
|
||||
|
||||
// configuration for all things recording related. Existing settings will be migrated here in the future.
|
||||
// recordings: {
|
||||
// // IF true (default) recording audio and video is selected by default in the recording dialog.
|
||||
// // recordAudioAndVideo: true,
|
||||
// // If true, shows a notification at the start of the meeting with a call to action button
|
||||
// // to start recording (for users who can do so).
|
||||
// // suggestRecording: true,
|
||||
@@ -1300,6 +1302,8 @@ var config = {
|
||||
// remoteVideoMenu: {
|
||||
// // Whether the remote video context menu to be rendered or not.
|
||||
// disabled: true,
|
||||
// // If set to true the 'Switch to visitor' button will be disabled.
|
||||
// disableDemote: true,
|
||||
// // If set to true the 'Kick out' button will be disabled.
|
||||
// disableKick: true,
|
||||
// // If set to true the 'Grant moderator' button will be disabled.
|
||||
|
||||
@@ -453,7 +453,7 @@ PODS:
|
||||
- react-native-video/Video (6.0.0-alpha.11):
|
||||
- PromisesSwift
|
||||
- React-Core
|
||||
- react-native-webrtc (118.0.1):
|
||||
- react-native-webrtc (118.0.3):
|
||||
- JitsiWebRTC (~> 118.0.0)
|
||||
- React-Core
|
||||
- react-native-webview (13.5.1):
|
||||
@@ -881,7 +881,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
|
||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||
react-native-video: 472b7c366eaaaa0207e546d9a50410df89790bcf
|
||||
react-native-webrtc: 7adbde4b2ad20fb2b804fb11f329425c9323dccc
|
||||
react-native-webrtc: 6fc32f3d556aa60aa2334eeaf6cadcdab2432809
|
||||
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
|
||||
React-NativeModulesApple: 4225ac31a26696c02c54b471052b3e85e74a9a0c
|
||||
React-perflogger: cb433f318c6667060fc1f62e26eb58d6eb30a627
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>24.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>24.0.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>24.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>24.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -629,7 +629,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export NODE_BINARY=node\nexport NODE_ARGS=\"--max_old_space_size=4096\"\n../../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
shellScript = "WITH_ENVIRONMENT=\"../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
};
|
||||
DE9A016B289A9A9A00E41CBB /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>9.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>99.0.0</string>
|
||||
<string>9.0.1</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -305,6 +305,8 @@
|
||||
"contactSupport": "Contact support",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
"demoteParticipantDialog": "Are you sure you want to move this participant to visitor?",
|
||||
"demoteParticipantTitle": "Move to visitor",
|
||||
"dismiss": "Dismiss",
|
||||
"displayNameRequired": "Hi! What’s your name?",
|
||||
"done": "Done",
|
||||
@@ -813,6 +815,7 @@
|
||||
"videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits.",
|
||||
"videoUnmuteBlockedTitle": "Camera unmute and desktop sharing blocked!",
|
||||
"viewLobby": "View lobby",
|
||||
"viewVisitors": "View visitors",
|
||||
"waitingParticipants": "{{waitingParticipants}} people",
|
||||
"whiteboardLimitDescription": "Please save your progress, as the user limit will soon be reached and the whiteboard will close.",
|
||||
"whiteboardLimitTitle": "Whiteboard usage"
|
||||
@@ -1419,6 +1422,7 @@
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Connection Info",
|
||||
"demote": "Move to visitor",
|
||||
"domute": "Mute",
|
||||
"domuteOthers": "Mute everyone else",
|
||||
"domuteVideo": "Disable camera",
|
||||
@@ -1473,6 +1477,7 @@
|
||||
"chatIndicator": "(visitor)",
|
||||
"labelTooltip": "Number of visitors: {{count}}",
|
||||
"notification": {
|
||||
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
|
||||
"description": "To participate raise your hand",
|
||||
"title": "You are a visitor in the meeting"
|
||||
}
|
||||
|
||||
38
package-lock.json
generated
38
package-lock.json
generated
@@ -17,7 +17,7 @@
|
||||
"@giphy/js-fetch-api": "4.7.1",
|
||||
"@giphy/react-components": "6.8.1",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.16/jitsi-excalidraw-0.0.16.tgz",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz",
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
"@jitsi/rnnoise-wasm": "0.1.0",
|
||||
@@ -54,14 +54,14 @@
|
||||
"i18n-iso-countries": "6.8.0",
|
||||
"i18next": "17.0.6",
|
||||
"i18next-browser-languagedetector": "3.0.1",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"i18next-http-backend": "2.2.1",
|
||||
"image-capture": "0.4.0",
|
||||
"jquery": "3.6.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/v1784.0.0+639ad566/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1790.0.0+311766e3/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -100,7 +100,7 @@
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-video": "6.0.0-alpha.11",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "118.0.1",
|
||||
"react-native-webrtc": "118.0.3",
|
||||
"react-native-webview": "13.5.1",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
@@ -3461,9 +3461,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jitsi/excalidraw": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.16/jitsi-excalidraw-0.0.16.tgz",
|
||||
"integrity": "sha512-j8dlh/TD663HgjDaCMjx56vDs3dFlNM9rPAdVSbi9OnQV4gxvzPmvzIe2dxSaz2zZGCOIyfQhhzsC98YC6MKUA==",
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz",
|
||||
"integrity": "sha512-8ClME6K/6s3JXi1e3zVjLgvMfqC07Jp3zGohG/buaMbCHQ1NUTvq2ejYAd/EYBTdHaLGI366WTydcrLIeDpVAw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2 || ^18.2.0",
|
||||
@@ -12772,8 +12772,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1784.0.0+639ad566/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-1K0PIItt5u88XpJXETi+7JsFGK6uN6FqY0DNcE9y01+gkdBvFkHc5ArILixh2L9fpOfnCEDKp1WIBDFSGZ6dLA==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1790.0.0+311766e3/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-rtXPegsdEOx7rxQnyxoony7BXD88ssM5prGPU2Ax6AChmzW933CZu/aW7m9bP4WSFHnVvABS3M6NEF76h282Nw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -16817,9 +16817,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "118.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-118.0.1.tgz",
|
||||
"integrity": "sha512-gjbBIV/0VyplavbOsQw9mpVJ4WHTEYZzi4PN7Oz18p2Ucsc5yEVUhtN5NQep8w6VDH1DNzuXXBPq5uJq9uqbMA==",
|
||||
"version": "118.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-118.0.3.tgz",
|
||||
"integrity": "sha512-qw+aa4rxGJTvltmYwwHonx4Qcgk/tcoojONu/6y5nsXGctkUqo886EIBb29Jv4ssHnudDzvkxyG/xVKK2vJc7Q==",
|
||||
"dependencies": {
|
||||
"base64-js": "1.5.1",
|
||||
"debug": "4.3.4",
|
||||
@@ -22262,8 +22262,8 @@
|
||||
"dev": true
|
||||
},
|
||||
"@jitsi/excalidraw": {
|
||||
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.16/jitsi-excalidraw-0.0.16.tgz",
|
||||
"integrity": "sha512-j8dlh/TD663HgjDaCMjx56vDs3dFlNM9rPAdVSbi9OnQV4gxvzPmvzIe2dxSaz2zZGCOIyfQhhzsC98YC6MKUA=="
|
||||
"version": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz",
|
||||
"integrity": "sha512-8ClME6K/6s3JXi1e3zVjLgvMfqC07Jp3zGohG/buaMbCHQ1NUTvq2ejYAd/EYBTdHaLGI366WTydcrLIeDpVAw=="
|
||||
},
|
||||
"@jitsi/js-utils": {
|
||||
"version": "2.2.1",
|
||||
@@ -29183,8 +29183,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1784.0.0+639ad566/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-1K0PIItt5u88XpJXETi+7JsFGK6uN6FqY0DNcE9y01+gkdBvFkHc5ArILixh2L9fpOfnCEDKp1WIBDFSGZ6dLA==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1790.0.0+311766e3/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-rtXPegsdEOx7rxQnyxoony7BXD88ssM5prGPU2Ax6AChmzW933CZu/aW7m9bP4WSFHnVvABS3M6NEF76h282Nw==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
@@ -32118,9 +32118,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "118.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-118.0.1.tgz",
|
||||
"integrity": "sha512-gjbBIV/0VyplavbOsQw9mpVJ4WHTEYZzi4PN7Oz18p2Ucsc5yEVUhtN5NQep8w6VDH1DNzuXXBPq5uJq9uqbMA==",
|
||||
"version": "118.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-118.0.3.tgz",
|
||||
"integrity": "sha512-qw+aa4rxGJTvltmYwwHonx4Qcgk/tcoojONu/6y5nsXGctkUqo886EIBb29Jv4ssHnudDzvkxyG/xVKK2vJc7Q==",
|
||||
"requires": {
|
||||
"base64-js": "1.5.1",
|
||||
"debug": "4.3.4",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"@giphy/js-fetch-api": "4.7.1",
|
||||
"@giphy/react-components": "6.8.1",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.16/jitsi-excalidraw-0.0.16.tgz",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz",
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
"@jitsi/rnnoise-wasm": "0.1.0",
|
||||
@@ -60,14 +60,14 @@
|
||||
"i18n-iso-countries": "6.8.0",
|
||||
"i18next": "17.0.6",
|
||||
"i18next-browser-languagedetector": "3.0.1",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"i18next-http-backend": "2.2.1",
|
||||
"image-capture": "0.4.0",
|
||||
"jquery": "3.6.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/v1784.0.0+639ad566/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1790.0.0+311766e3/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -106,7 +106,7 @@
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-video": "6.0.0-alpha.11",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "118.0.1",
|
||||
"react-native-webrtc": "118.0.3",
|
||||
"react-native-webview": "13.5.1",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
|
||||
@@ -133,7 +133,7 @@ dependencies {
|
||||
|
||||
if (isNewArchitectureEnabled()) {
|
||||
react {
|
||||
jsRootDir = file("../src/")
|
||||
jsRootDir = file("../")
|
||||
libraryName = "JitsiMeetReactNative"
|
||||
codegenJavaPackageName = "org.jitsi.meet.sdk"
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@ReactModule(name = JMOngoingConferenceModule.NAME)
|
||||
class JMOngoingConferenceModule
|
||||
extends ReactContextBaseJavaModule {
|
||||
|
||||
public static final String NAME = "JMOngoingConference";
|
||||
|
||||
public JMOngoingConferenceModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void launch() {
|
||||
Context context = getReactApplicationContext();
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
JitsiMeetOngoingConferenceService.launch(context, currentActivity);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void abort() {
|
||||
Context context = getReactApplicationContext();
|
||||
|
||||
JitsiMeetOngoingConferenceService.abort(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
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 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.
|
||||
* The service will help keep the app running while in the background.
|
||||
*
|
||||
* See: https://developer.android.com/guide/components/services
|
||||
*/
|
||||
public class JitsiMeetOngoingConferenceService extends Service {
|
||||
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
|
||||
|
||||
public static void launch(Context context, Activity currentActivity) {
|
||||
|
||||
RNOngoingNotification.createOngoingConferenceNotificationChannel(currentActivity);
|
||||
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
|
||||
ComponentName componentName;
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
componentName = context.startForegroundService(intent);
|
||||
} else {
|
||||
componentName = context.startService(intent);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
// Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
|
||||
// See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
|
||||
JitsiMeetLogger.w(TAG + " Ongoing conference service not started", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (componentName == null) {
|
||||
JitsiMeetLogger.w(TAG + " Ongoing conference service not started");
|
||||
}
|
||||
}
|
||||
|
||||
public static void abort(Context context) {
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
context.stopService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Notification notification = RNOngoingNotification.buildOngoingConferenceNotification(this);
|
||||
|
||||
if (notification == null) {
|
||||
stopSelf();
|
||||
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
|
||||
} else {
|
||||
startForeground(RNOngoingNotification.NOTIFICATION_ID, notification);
|
||||
JitsiMeetLogger.i(TAG + " Service started");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ public class JitsiMeetReactNativePackage implements ReactPackage {
|
||||
new AndroidSettingsModule(reactContext),
|
||||
new AppInfoModule(reactContext),
|
||||
new AudioModeModule(reactContext),
|
||||
new JMOngoingConferenceModule(reactContext),
|
||||
new JavaScriptSandboxModule(reactContext),
|
||||
new LocaleDetector(reactContext),
|
||||
new LogBridgeModule(reactContext),
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Helper class for creating the ongoing notification which is used with
|
||||
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
|
||||
* and to hangup from within the notification itself.
|
||||
*/
|
||||
class RNOngoingNotification {
|
||||
private static final String TAG = RNOngoingNotification.class.getSimpleName();
|
||||
|
||||
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
|
||||
|
||||
static void createOngoingConferenceNotificationChannel(Activity currentActivity) {
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentActivity == null) {
|
||||
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager
|
||||
= (NotificationManager) currentActivity.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
NotificationChannel channel
|
||||
= notificationManager.getNotificationChannel("JitsiOngoingConferenceChannel");
|
||||
if (channel != null) {
|
||||
// The channel was already created, no need to do it again.
|
||||
return;
|
||||
}
|
||||
|
||||
channel = new NotificationChannel("JitsiOngoingConferenceChannel", currentActivity.getString(R.string.ongoing_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(false);
|
||||
channel.setShowBadge(false);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
static Notification buildOngoingConferenceNotification(Context context) {
|
||||
|
||||
if (context == null) {
|
||||
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
|
||||
return null;
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "JitsiOngoingConferenceChannel");
|
||||
|
||||
builder
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setContentTitle(context.getString(R.string.ongoing_notification_title))
|
||||
.setContentText(context.getString(R.string.ongoing_notification_text))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setUsesChronometer(true)
|
||||
.setAutoCancel(false)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
2499
react-native-sdk/package-lock.json
generated
2499
react-native-sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jitsi/react-native-sdk",
|
||||
"version": "0.0.0",
|
||||
"version": "2.0.2",
|
||||
"description": "React Native SDK for Jitsi Meet.",
|
||||
"main": "index.tsx",
|
||||
"license": "Apache-2.0",
|
||||
@@ -11,7 +11,7 @@
|
||||
"url": "git+https://github.com/jitsi/jitsi-meet.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.1.3",
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
"@jitsi/rtcstats": "9.5.1",
|
||||
"@react-navigation/bottom-tabs": "6.5.8",
|
||||
@@ -20,7 +20,7 @@
|
||||
"@react-navigation/native": "6.1.7",
|
||||
"@react-navigation/stack": "6.3.17",
|
||||
"@xmldom/xmldom": "0.8.7",
|
||||
"base64-js": "1.3.1",
|
||||
"base64-js": "1.5.1",
|
||||
"grapheme-splitter": "1.0.4",
|
||||
"i18n-iso-countries": "6.8.0",
|
||||
"i18next": "17.0.6",
|
||||
@@ -28,7 +28,7 @@
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1687.0.0+cafe30d7/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1784.0.0+639ad566/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -53,12 +53,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@amplitude/react-native": "2.7.0",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@react-native-async-storage/async-storage": "1.19.3",
|
||||
"@react-native/metro-config": "0.72.9",
|
||||
"@react-native-async-storage/async-storage": "1.19.4",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/netinfo": "9.4.1",
|
||||
"@react-native-community/netinfo": "11.1.0",
|
||||
"@react-native-community/slider": "4.4.3",
|
||||
"@react-native-google-signin/google-signin": "10.0.1",
|
||||
"@react-native-google-signin/google-signin": "10.1.0",
|
||||
"react-native": "*",
|
||||
"react": "*",
|
||||
"react-native-background-timer": "2.4.1",
|
||||
@@ -72,16 +74,17 @@
|
||||
"react-native-pager-view": "6.2.0",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.0.0",
|
||||
"react-native-orientation-locker": "1.5.0",
|
||||
"react-native-orientation-locker": "1.6.0",
|
||||
"react-native-safe-area-context": "4.7.1",
|
||||
"react-native-screens": "3.24.0",
|
||||
"react-native-sound": "0.11.2",
|
||||
"react-native-splash-screen": "3.3.0",
|
||||
"react-native-svg": "13.13.0",
|
||||
"react-native-video": "6.0.0-alpha.7",
|
||||
"react-native-video": "6.0.0-alpha.11",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "111.0.3",
|
||||
"react-native-webview": "13.5.1"
|
||||
"react-native-webrtc": "118.0.2",
|
||||
"react-native-webview": "13.5.1",
|
||||
"text-encoding": "0.7.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@xmldom/xmldom": "0.8.7"
|
||||
@@ -96,4 +99,4 @@
|
||||
"keywords": [
|
||||
"react-native"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
react-native-sdk/prepare_sdk.js
vendored
7
react-native-sdk/prepare_sdk.js
vendored
@@ -79,6 +79,13 @@ function mergeDependencyVersions() {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates SDK overrides dependencies.
|
||||
for (const key in packageJSON.overrides) {
|
||||
if (SDKPackageJSON.overrides.hasOwnProperty(key)) {
|
||||
SDKPackageJSON.overrides[key] = packageJSON.overrides[key];
|
||||
}
|
||||
}
|
||||
|
||||
const data = JSON.stringify(SDKPackageJSON, null, 4);
|
||||
|
||||
fs.writeFileSync('package.json', data);
|
||||
|
||||
@@ -400,7 +400,7 @@ function _connectionFailed({ dispatch, getState }: IStore, next: Function, actio
|
||||
descriptionKey: errors ? 'dialog.tokenAuthFailedWithReasons' : 'dialog.tokenAuthFailed',
|
||||
descriptionArguments: { reason: errors },
|
||||
titleKey: 'dialog.tokenAuthFailedTitle'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -596,7 +596,10 @@ function _setRoom(state: IConferenceState, action: AnyAction) {
|
||||
*/
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
room
|
||||
localSubject: undefined,
|
||||
pendingSubjectChange: undefined,
|
||||
room,
|
||||
subject: undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,4 @@
|
||||
export type ToolbarButton = 'camera' |
|
||||
'chat' |
|
||||
'closedcaptions' |
|
||||
'desktop' |
|
||||
'download' |
|
||||
'embedmeeting' |
|
||||
'etherpad' |
|
||||
'feedback' |
|
||||
'filmstrip' |
|
||||
'fullscreen' |
|
||||
'hangup' |
|
||||
'help' |
|
||||
'highlight' |
|
||||
'invite' |
|
||||
'linktosalesforce' |
|
||||
'livestreaming' |
|
||||
'microphone' |
|
||||
'mute-everyone' |
|
||||
'mute-video-everyone' |
|
||||
'noisesuppression' |
|
||||
'participants-pane' |
|
||||
'profile' |
|
||||
'raisehand' |
|
||||
'reactions' |
|
||||
'recording' |
|
||||
'security' |
|
||||
'select-background' |
|
||||
'settings' |
|
||||
'shareaudio' |
|
||||
'sharedvideo' |
|
||||
'shortcuts' |
|
||||
'stats' |
|
||||
'tileview' |
|
||||
'toggle-camera' |
|
||||
'videoquality' |
|
||||
'whiteboard' |
|
||||
'__end';
|
||||
import { ToolbarButton } from '../../toolbox/types';
|
||||
|
||||
type ButtonsWithNotifyClick = 'camera' |
|
||||
'chat' |
|
||||
@@ -532,10 +496,12 @@ export interface IConfig {
|
||||
};
|
||||
recordingSharingUrl?: string;
|
||||
recordings?: {
|
||||
recordAudioAndVideo?: boolean;
|
||||
showPrejoinWarning?: boolean;
|
||||
suggestRecording?: boolean;
|
||||
};
|
||||
remoteVideoMenu?: {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: boolean;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ToolbarButton } from './configType';
|
||||
|
||||
/**
|
||||
* The prefix of the {@code localStorage} key into which {@link storeConfig}
|
||||
* stores and from which {@link restoreConfig} restores.
|
||||
@@ -9,50 +7,6 @@ import { ToolbarButton } from './configType';
|
||||
*/
|
||||
export const _CONFIG_STORE_PREFIX = 'config.js';
|
||||
|
||||
/**
|
||||
* The list of all possible UI buttons.
|
||||
*
|
||||
* @protected
|
||||
* @type Array<string>
|
||||
*/
|
||||
export const TOOLBAR_BUTTONS: ToolbarButton[] = [
|
||||
'camera',
|
||||
'chat',
|
||||
'closedcaptions',
|
||||
'desktop',
|
||||
'download',
|
||||
'embedmeeting',
|
||||
'etherpad',
|
||||
'feedback',
|
||||
'filmstrip',
|
||||
'fullscreen',
|
||||
'hangup',
|
||||
'help',
|
||||
'highlight',
|
||||
'invite',
|
||||
'linktosalesforce',
|
||||
'livestreaming',
|
||||
'microphone',
|
||||
'mute-everyone',
|
||||
'mute-video-everyone',
|
||||
'participants-pane',
|
||||
'profile',
|
||||
'raisehand',
|
||||
'recording',
|
||||
'security',
|
||||
'select-background',
|
||||
'settings',
|
||||
'shareaudio',
|
||||
'noisesuppression',
|
||||
'sharedvideo',
|
||||
'shortcuts',
|
||||
'stats',
|
||||
'tileview',
|
||||
'toggle-camera',
|
||||
'videoquality',
|
||||
'whiteboard'
|
||||
];
|
||||
|
||||
/**
|
||||
* The toolbar buttons to show on premeeting screens.
|
||||
*/
|
||||
@@ -63,11 +17,6 @@ export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background',
|
||||
*/
|
||||
export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ];
|
||||
|
||||
/**
|
||||
* The toolbar buttons to show when in visitors mode.
|
||||
*/
|
||||
export const VISITORS_MODE_BUTTONS = [ 'chat', 'hangup', 'raisehand', 'settings', 'tileview' ];
|
||||
|
||||
/**
|
||||
* The set of feature flags.
|
||||
*
|
||||
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
IDeeplinkingConfig,
|
||||
IDeeplinkingDesktopConfig,
|
||||
IDeeplinkingMobileConfig,
|
||||
NotifyClickButton,
|
||||
ToolbarButton
|
||||
NotifyClickButton
|
||||
} from './configType';
|
||||
import { TOOLBAR_BUTTONS } from './constants';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
@@ -34,25 +32,6 @@ export function getReplaceParticipant(state: IReduxState): string | undefined {
|
||||
return state['features/base/config'].replaceParticipant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of enabled toolbar buttons.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Array<string>} - The list of enabled toolbar buttons.
|
||||
*/
|
||||
export function getToolbarButtons(state: IReduxState): Array<string> {
|
||||
const { toolbarButtons, customToolbarButtons } = state['features/base/config'];
|
||||
const customButtons = customToolbarButtons?.map(({ id }) => id);
|
||||
|
||||
const buttons = Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
|
||||
|
||||
if (customButtons) {
|
||||
buttons.push(...customButtons as ToolbarButton[]);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration value of web-hid feature.
|
||||
*
|
||||
@@ -63,19 +42,6 @@ export function getWebHIDFeatureConfig(state: IReduxState): boolean {
|
||||
return state['features/base/config'].enableWebHIDFeature || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the specified button is enabled.
|
||||
*
|
||||
* @param {string} buttonName - The name of the button. See {@link interfaceConfig}.
|
||||
* @param {Object|Array<string>} state - The redux state or the array with the enabled buttons.
|
||||
* @returns {boolean} - True if the button is enabled and false otherwise.
|
||||
*/
|
||||
export function isToolbarButtonEnabled(buttonName: string, state: IReduxState | Array<string>) {
|
||||
const buttons = Array.isArray(state) ? state : getToolbarButtons(state);
|
||||
|
||||
return buttons.includes(buttonName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether audio level measurement is enabled or not.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { CONFERENCE_INFO } from '../../conference/components/constants';
|
||||
import { TOOLBAR_BUTTONS } from '../../toolbox/constants';
|
||||
import { ToolbarButton } from '../../toolbox/types';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { equals } from '../redux/functions';
|
||||
|
||||
@@ -16,10 +18,8 @@ import {
|
||||
IDeeplinkingConfig,
|
||||
IDeeplinkingDesktopConfig,
|
||||
IDeeplinkingMobileConfig,
|
||||
IMobileDynamicLink,
|
||||
ToolbarButton
|
||||
IMobileDynamicLink
|
||||
} from './configType';
|
||||
import { TOOLBAR_BUTTONS } from './constants';
|
||||
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,6 +52,16 @@ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
|
||||
*/
|
||||
export const SET_LOCATION_URL = 'SET_LOCATION_URL';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the preferVisitor in store.
|
||||
*
|
||||
* {
|
||||
* type: SET_PREFER_VISITOR,
|
||||
* preferVisitor: ?boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_PREFER_VISITOR = 'SET_PREFER_VISITOR';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which tells whether connection info should be displayed
|
||||
* on context menu.
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
CONNECTION_ESTABLISHED,
|
||||
CONNECTION_FAILED,
|
||||
CONNECTION_WILL_CONNECT,
|
||||
SET_LOCATION_URL
|
||||
SET_LOCATION_URL,
|
||||
SET_PREFER_VISITOR
|
||||
} from './actionTypes';
|
||||
import { JITSI_CONNECTION_URL_KEY } from './constants';
|
||||
import logger from './logger';
|
||||
@@ -180,6 +181,22 @@ export function setLocationURL(locationURL?: URL) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To change prefer visitor in the store. Used later to decide what to request from jicofo on connection.
|
||||
*
|
||||
* @param {boolean} preferVisitor - The value to set.
|
||||
* @returns {{
|
||||
* type: SET_PREFER_VISITOR,
|
||||
* preferVisitor: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setPreferVisitor(preferVisitor: boolean) {
|
||||
return {
|
||||
type: SET_PREFER_VISITOR,
|
||||
preferVisitor
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens new connection.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CONNECTION_FAILED,
|
||||
CONNECTION_WILL_CONNECT,
|
||||
SET_LOCATION_URL,
|
||||
SET_PREFER_VISITOR,
|
||||
SHOW_CONNECTION_INFO
|
||||
} from './actionTypes';
|
||||
import { ConnectionFailedError } from './types';
|
||||
@@ -57,6 +58,11 @@ ReducerRegistry.register<IConnectionState>(
|
||||
case SET_LOCATION_URL:
|
||||
return _setLocationURL(state, action);
|
||||
|
||||
case SET_PREFER_VISITOR:
|
||||
return assign(state, {
|
||||
preferVisitor: action.preferVisitor
|
||||
});
|
||||
|
||||
case SET_ROOM:
|
||||
return _setRoom(state);
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import { IReduxState } from '../../../../app/types';
|
||||
import DeviceStatus from '../../../../prejoin/components/web/preview/DeviceStatus';
|
||||
import { isRoomNameEnabled } from '../../../../prejoin/functions';
|
||||
import Toolbox from '../../../../toolbox/components/web/Toolbox';
|
||||
import { isButtonEnabled } from '../../../../toolbox/functions.web';
|
||||
import { getConferenceName } from '../../../conference/functions';
|
||||
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
|
||||
import { getToolbarButtons, isToolbarButtonEnabled } from '../../../config/functions.web';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
import ConnectionStatus from './ConnectionStatus';
|
||||
@@ -229,7 +229,7 @@ const PreMeetingScreen = ({
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { hiddenPremeetingButtons } = state['features/base/config'];
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const premeetingButtons = (ownProps.thirdParty
|
||||
? THIRD_PARTY_PREJOIN_BUTTONS
|
||||
: PREMEETING_BUTTONS).filter((b: any) => !(hiddenPremeetingButtons || []).includes(b));
|
||||
@@ -244,7 +244,7 @@ function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
// toolbarButtons config overwrite.
|
||||
_buttons: hiddenPremeetingButtons
|
||||
? premeetingButtons
|
||||
: premeetingButtons.filter(b => isToolbarButtonEnabled(b, toolbarButtons)),
|
||||
: premeetingButtons.filter(b => isButtonEnabled(b, toolbarButtons)),
|
||||
_premeetingBackground: premeetingBackground,
|
||||
_roomName: isRoomNameEnabled(state) ? getConferenceName(state) : ''
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { NativeModules, Platform } from 'react-native';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { setPictureInPictureEnabled } from '../../mobile/picture-in-picture/functions';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
@@ -16,7 +14,6 @@ import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import { addLocalTrack, replaceLocalTrack } from './actions.any';
|
||||
import { getLocalDesktopTrack, getTrackState, isLocalVideoTrackDesktop } from './functions.native';
|
||||
|
||||
const { JitsiMeetMediaProjectionModule } = NativeModules;
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -35,10 +32,7 @@ export function toggleScreensharing(enabled: boolean, _ignore1?: boolean, _ignor
|
||||
if (enabled) {
|
||||
const isSharing = isLocalVideoTrackDesktop(state);
|
||||
|
||||
if (isSharing) {
|
||||
Platform.OS === 'android' && JitsiMeetMediaProjectionModule.abort();
|
||||
} else {
|
||||
Platform.OS === 'android' && JitsiMeetMediaProjectionModule.launch();
|
||||
if (!isSharing) {
|
||||
_startScreenSharing(dispatch, state);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { withStyles } from 'tss-react/mui';
|
||||
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent, createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getToolbarButtons } from '../../../base/config/functions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
@@ -884,11 +883,11 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { _hasScroll = false, filmstripType, _topPanelFilmstrip, _remoteParticipants } = ownProps;
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const { topPanelHeight, topPanelVisible, visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
const { isOpen: shiftRight } = state['features/chat'];
|
||||
const disableSelfView = getHideSelfView(state);
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getToolbarButtons } from '../../../base/config/functions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { LAYOUTS } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.web';
|
||||
@@ -107,9 +106,9 @@ const MainFilmstrip = (props: IProps) => (
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const { remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length;
|
||||
const {
|
||||
gridDimensions: dimensions = { columns: undefined,
|
||||
rows: undefined },
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getToolbarButtons } from '../../../base/config/functions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { LAYOUTS, LAYOUT_CLASSNAMES } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.web';
|
||||
@@ -108,9 +107,9 @@ const StageFilmstrip = (props: IProps) =>
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const activeParticipants = getActiveParticipantsIds(state);
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length;
|
||||
const {
|
||||
gridDimensions: dimensions = { columns: undefined,
|
||||
rows: undefined },
|
||||
|
||||
@@ -116,9 +116,7 @@ export function admitMultiple(participants: Array<IKnockingParticipant>) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
participants.forEach(p => {
|
||||
conference?.lobbyApproveAccess(p.id);
|
||||
});
|
||||
conference?.lobbyApproveAccess(participants.map(p => p.id));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { NativeModules, Platform } from 'react-native';
|
||||
|
||||
import { getAppProp } from '../../base/app/functions';
|
||||
import {
|
||||
CONFERENCE_BLURRED,
|
||||
@@ -11,7 +9,6 @@ import {
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
|
||||
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
|
||||
import { READY_TO_CLOSE } from '../external-api/actionTypes';
|
||||
import { participantToParticipantInfo } from '../external-api/functions';
|
||||
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
|
||||
@@ -19,7 +16,6 @@ import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
|
||||
import { isExternalAPIAvailable } from './functions';
|
||||
|
||||
const externalAPIEnabled = isExternalAPIAvailable();
|
||||
const { JMOngoingConference } = NativeModules;
|
||||
|
||||
|
||||
/**
|
||||
@@ -78,21 +74,3 @@ const { JMOngoingConference } = NativeModules;
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Before enabling media projection service control on Android,
|
||||
* we need to check if native modules are being used or not.
|
||||
*/
|
||||
Platform.OS === 'android' && !externalAPIEnabled && StateListenerRegistry.register(
|
||||
state => state['features/base/conference'].conference,
|
||||
(conference, previousConference) => {
|
||||
if (!conference) {
|
||||
JMOngoingConference.abort();
|
||||
} else if (conference && !previousConference) {
|
||||
JMOngoingConference.launch();
|
||||
} else if (conference !== previousConference) {
|
||||
JMOngoingConference.abort();
|
||||
JMOngoingConference.launch();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
isParticipantAudioMuted,
|
||||
isParticipantVideoMuted
|
||||
} from '../../../base/tracks/functions.native';
|
||||
import { showConnectionStatus, showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
|
||||
import { showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
|
||||
import type { MediaState } from '../../constants';
|
||||
import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
|
||||
|
||||
@@ -117,11 +117,7 @@ class MeetingParticipantItem extends PureComponent<IProps> {
|
||||
if (_fakeParticipant && _localVideoOwner) {
|
||||
dispatch(showSharedVideoMenu(_participantID));
|
||||
} else if (!_fakeParticipant) {
|
||||
if (_local) {
|
||||
dispatch(showConnectionStatus(_participantID));
|
||||
} else {
|
||||
dispatch(showContextMenuDetails(_participantID));
|
||||
}
|
||||
dispatch(showContextMenuDetails(_participantID, _local));
|
||||
} // else no-op
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { rejectParticipantAudio, rejectParticipantVideo } from '../../../av-moderation/actions';
|
||||
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
||||
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { getParticipantById, isScreenShareParticipant } from '../../../base/participants/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
@@ -14,7 +13,7 @@ import Input from '../../../base/ui/components/web/Input';
|
||||
import useContextMenu from '../../../base/ui/hooks/useContextMenu.web';
|
||||
import { normalizeAccents } from '../../../base/util/strings.web';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { isButtonEnabled, showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { muteRemote } from '../../../video-menu/actions.web';
|
||||
import { getSortedParticipantIds, isCurrentRoomRenamable, shouldRenderInviteButton } from '../../functions';
|
||||
import { useParticipantDrawer } from '../../hooks';
|
||||
@@ -184,7 +183,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
});
|
||||
|
||||
const participantsCount = sortedParticipantIds.length;
|
||||
const showInviteButton = shouldRenderInviteButton(state) && isToolbarButtonEnabled('invite', state);
|
||||
const showInviteButton = shouldRenderInviteButton(state) && isButtonEnabled('invite', state);
|
||||
const overflowDrawer = showOverflowDrawer(state);
|
||||
const currentRoomId = getCurrentRoomId(state);
|
||||
const currentRoom = getBreakoutRooms(state)[currentRoomId];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getToolbarButtons } from '../base/config/functions.web';
|
||||
|
||||
import { shouldDisplayReactionsButtons } from './functions.any';
|
||||
import { isReactionsEnabled } from './functions.any';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
@@ -22,5 +21,7 @@ export function getReactionsMenuVisibility(state: IReduxState): boolean {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isReactionsButtonEnabled(state: IReduxState) {
|
||||
return Boolean(getToolbarButtons(state).includes('reactions')) && shouldDisplayReactionsButtons(state);
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
|
||||
return Boolean(toolbarButtons?.includes('reactions')) && isReactionsEnabled(state);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { getToolbarButtons } from '../../../../base/config/functions.web';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import AbstractLiveStreamButton, {
|
||||
@@ -50,11 +49,11 @@ class LiveStreamButton extends AbstractLiveStreamButton<IProps> {
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const abstractProps = _abstractMapStateToProps(state, ownProps);
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
let { visible } = ownProps;
|
||||
|
||||
if (typeof visible === 'undefined') {
|
||||
visible = toolbarButtons.includes('livestreaming') && abstractProps.visible;
|
||||
visible = Boolean(toolbarButtons?.includes('livestreaming') && abstractProps.visible);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -64,6 +64,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_rToken: string;
|
||||
|
||||
/**
|
||||
* Whether the record audio / video option is enabled by default.
|
||||
*/
|
||||
_recordAudioAndVideo: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is screensharing.
|
||||
*/
|
||||
@@ -187,7 +192,7 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
isValidating: false,
|
||||
userName: undefined,
|
||||
sharingEnabled: true,
|
||||
shouldRecordAudioAndVideo: true,
|
||||
shouldRecordAudioAndVideo: this.props._recordAudioAndVideo,
|
||||
shouldRecordTranscription: this.props._autoTranscribeOnRecord,
|
||||
spaceLeft: undefined,
|
||||
selectedRecordingService,
|
||||
@@ -452,7 +457,8 @@ export function mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const {
|
||||
recordingService,
|
||||
dropbox = { appKey: undefined },
|
||||
localRecording
|
||||
localRecording,
|
||||
recordings = { recordAudioAndVideo: true }
|
||||
} = state['features/base/config'];
|
||||
const {
|
||||
_displaySubtitles,
|
||||
@@ -469,6 +475,7 @@ export function mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
_isDropboxEnabled: isDropboxEnabled(state),
|
||||
_localRecordingEnabled: !localRecording?.disable,
|
||||
_rToken: state['features/dropbox'].rToken ?? '',
|
||||
_recordAudioAndVideo: recordings?.recordAudioAndVideo ?? true,
|
||||
_subtitlesLanguage,
|
||||
_tokenExpireDate: state['features/dropbox'].expireDate,
|
||||
_token: state['features/dropbox'].token ?? ''
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { getToolbarButtons } from '../../../../base/config/functions.web';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import AbstractRecordButton, {
|
||||
@@ -49,8 +48,8 @@ class RecordingButton extends AbstractRecordButton<IProps> {
|
||||
*/
|
||||
export function _mapStateToProps(state: IReduxState) {
|
||||
const abstractProps = _abstractMapStateToProps(state);
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
const visible = toolbarButtons.includes('recording') && abstractProps.visible;
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const visible = Boolean(toolbarButtons?.includes('recording') && abstractProps.visible);
|
||||
|
||||
return {
|
||||
...abstractProps,
|
||||
|
||||
@@ -253,12 +253,12 @@ export function getRecordButtonProps(state: IReduxState) {
|
||||
const localRecordingEnabled = !localRecording?.disable && supportsLocalRecording();
|
||||
|
||||
const dropboxEnabled = isDropboxEnabled(state);
|
||||
const recordingEnabled = recordingService?.enabled || localRecordingEnabled || dropboxEnabled;
|
||||
const recordingEnabled = recordingService?.enabled || dropboxEnabled;
|
||||
|
||||
if (isModerator) {
|
||||
if (localRecordingEnabled) {
|
||||
visible = true;
|
||||
} else if (isModerator) {
|
||||
visible = recordingEnabled ? isJwtFeatureEnabled(state, 'recording', true) : false;
|
||||
} else {
|
||||
visible = navigator.product !== 'ReactNative' && localRecordingEnabled;
|
||||
}
|
||||
|
||||
// disable the button if the livestreaming is running.
|
||||
|
||||
@@ -54,6 +54,17 @@ export const SET_OVERFLOW_DRAWER = 'SET_OVERFLOW_DRAWER';
|
||||
*/
|
||||
export const SET_OVERFLOW_MENU_VISIBLE = 'SET_OVERFLOW_MENU_VISIBLE';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the action which sets enabled toolbar buttons.
|
||||
*
|
||||
* {
|
||||
* type: SET_TOOLBAR_BUTTONS,
|
||||
* toolbarButtons: Array<string>
|
||||
* }
|
||||
*/
|
||||
export const SET_TOOLBAR_BUTTONS = 'SET_TOOLBAR_BUTTONS';
|
||||
|
||||
/**
|
||||
* The type of the action which sets the indicator which determines whether a
|
||||
* fToolbar in the Toolbox is hovered.
|
||||
|
||||
@@ -5,19 +5,12 @@ import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { NotifyClickButton } from '../../../base/config/configType';
|
||||
import { VISITORS_MODE_BUTTONS } from '../../../base/config/constants';
|
||||
import {
|
||||
getButtonNotifyMode,
|
||||
getButtonsWithNotifyClick,
|
||||
getToolbarButtons,
|
||||
isToolbarButtonEnabled
|
||||
} from '../../../base/config/functions.web';
|
||||
import { getButtonNotifyMode, getButtonsWithNotifyClick } from '../../../base/config/functions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../../reactions/functions.web';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import {
|
||||
setHangupMenuVisible,
|
||||
setOverflowMenuVisible,
|
||||
@@ -28,6 +21,7 @@ import { NOT_APPLICABLE, THRESHOLDS } from '../../constants';
|
||||
import {
|
||||
getAllToolboxButtons,
|
||||
getJwtDisabledButtons,
|
||||
isButtonEnabled,
|
||||
isToolboxVisible
|
||||
} from '../../functions.web';
|
||||
import { useKeyboardShortcuts } from '../../hooks.web';
|
||||
@@ -289,7 +283,7 @@ const Toolbox = ({
|
||||
const buttons = getAllToolboxButtons(_customToolbarButtons);
|
||||
|
||||
setButtonsNotifyClickMode(buttons);
|
||||
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
|
||||
const isHangupVisible = isButtonEnabled('hangup', _toolbarButtons);
|
||||
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|
||||
|| THRESHOLDS[THRESHOLDS.length - 1];
|
||||
|
||||
@@ -300,7 +294,7 @@ const Toolbox = ({
|
||||
...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
|
||||
].filter(({ key, alias = NOT_APPLICABLE }) =>
|
||||
!_jwtDisabledButtons.includes(key)
|
||||
&& (isToolbarButtonEnabled(key, _toolbarButtons) || isToolbarButtonEnabled(alias, _toolbarButtons))
|
||||
&& (isButtonEnabled(key, _toolbarButtons) || isButtonEnabled(alias, _toolbarButtons))
|
||||
);
|
||||
|
||||
let sliceIndex = _overflowDrawer || _reactionsButtonEnabled ? order.length + 2 : order.length + 1;
|
||||
@@ -423,7 +417,7 @@ const Toolbox = ({
|
||||
showReactionsMenu = { showReactionsInOverflowMenu } />
|
||||
)}
|
||||
|
||||
{isToolbarButtonEnabled('hangup', _toolbarButtons) && (
|
||||
{isButtonEnabled('hangup', _toolbarButtons) && (
|
||||
_endConferenceSupported
|
||||
? <HangupMenuButton
|
||||
ariaControls = 'hangup-menu'
|
||||
@@ -453,7 +447,7 @@ const Toolbox = ({
|
||||
customClass = 'hangup-button'
|
||||
key = 'hangup-button'
|
||||
notifyMode = { getButtonNotifyMode('hangup', _buttonsWithNotifyClick) }
|
||||
visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
|
||||
visible = { isButtonEnabled('hangup', _toolbarButtons) } />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,11 +496,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
overflowDrawer
|
||||
} = state['features/toolbox'];
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
let toolbarButtons = ownProps.toolbarButtons || getToolbarButtons(state);
|
||||
|
||||
if (iAmVisitor(state)) {
|
||||
toolbarButtons = VISITORS_MODE_BUTTONS.filter(e => toolbarButtons.indexOf(e) > -1);
|
||||
}
|
||||
const toolbarButtons = ownProps.toolbarButtons || state['features/toolbox'].toolbarButtons;
|
||||
|
||||
return {
|
||||
_buttonsWithNotifyClick: getButtonsWithNotifyClick(state),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ToolbarButton } from './types';
|
||||
|
||||
/**
|
||||
* Thresholds for displaying toolbox buttons.
|
||||
*/
|
||||
@@ -50,3 +52,62 @@ export const ZINDEX_DIALOG_PORTAL = 302;
|
||||
* Color for spinner displayed in the toolbar.
|
||||
*/
|
||||
export const SPINNER_COLOR = '#929292';
|
||||
|
||||
|
||||
/**
|
||||
* The list of all possible UI buttons.
|
||||
*
|
||||
* @protected
|
||||
* @type Array<string>
|
||||
*/
|
||||
export const TOOLBAR_BUTTONS: ToolbarButton[] = [
|
||||
'camera',
|
||||
'chat',
|
||||
'closedcaptions',
|
||||
'desktop',
|
||||
'download',
|
||||
'embedmeeting',
|
||||
'etherpad',
|
||||
'feedback',
|
||||
'filmstrip',
|
||||
'fullscreen',
|
||||
'hangup',
|
||||
'help',
|
||||
'highlight',
|
||||
'invite',
|
||||
'linktosalesforce',
|
||||
'livestreaming',
|
||||
'microphone',
|
||||
'mute-everyone',
|
||||
'mute-video-everyone',
|
||||
'participants-pane',
|
||||
'profile',
|
||||
'raisehand',
|
||||
'recording',
|
||||
'security',
|
||||
'select-background',
|
||||
'settings',
|
||||
'shareaudio',
|
||||
'noisesuppression',
|
||||
'sharedvideo',
|
||||
'shortcuts',
|
||||
'stats',
|
||||
'tileview',
|
||||
'toggle-camera',
|
||||
'videoquality',
|
||||
'whiteboard'
|
||||
];
|
||||
|
||||
/**
|
||||
* The toolbar buttons to show when in visitors mode.
|
||||
*/
|
||||
export const VISITORS_MODE_BUTTONS: ToolbarButton[] = [
|
||||
'chat',
|
||||
'hangup',
|
||||
'raisehand',
|
||||
'settings',
|
||||
'tileview',
|
||||
'fullscreen',
|
||||
'stats',
|
||||
'videoquality'
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getToolbarButtons } from '../base/config/functions.web';
|
||||
import { hasAvailableDevices } from '../base/devices/functions';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
@@ -56,18 +55,16 @@ export function getToolboxHeight() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if a toolbar button is enabled.
|
||||
* Checks if the specified button is enabled.
|
||||
*
|
||||
* @param {string} name - The name of the setting section as defined in
|
||||
* interface_config.js.
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean|undefined} - True to indicate that the given toolbar button
|
||||
* is enabled, false - otherwise.
|
||||
* @param {string} buttonName - The name of the button. See {@link interfaceConfig}.
|
||||
* @param {Object|Array<string>} state - The redux state or the array with the enabled buttons.
|
||||
* @returns {boolean} - True if the button is enabled and false otherwise.
|
||||
*/
|
||||
export function isButtonEnabled(name: string, state: IReduxState) {
|
||||
const toolbarButtons = getToolbarButtons(state);
|
||||
export function isButtonEnabled(buttonName: string, state: IReduxState | Array<string>) {
|
||||
const buttons = Array.isArray(state) ? state : state['features/toolbox'].toolbarButtons || [];
|
||||
|
||||
return toolbarButtons.indexOf(name) !== -1;
|
||||
return buttons.includes(buttonName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,6 @@ import { batch, useDispatch, useSelector } from 'react-redux';
|
||||
import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getToolbarButtons, isToolbarButtonEnabled } from '../base/config/functions.web';
|
||||
import { toggleDialog } from '../base/dialog/actions';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
@@ -34,14 +33,15 @@ import { shouldDisplayTileView } from '../video-layout/functions.any';
|
||||
import VideoQualityDialog from '../video-quality/components/VideoQualityDialog.web';
|
||||
|
||||
import { setFullScreen } from './actions.web';
|
||||
import { isDesktopShareButtonDisabled } from './functions.web';
|
||||
import { isButtonEnabled, isDesktopShareButtonDisabled } from './functions.web';
|
||||
|
||||
export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
|
||||
const dispatch = useDispatch();
|
||||
const _isSpeakerStatsDisabled = useSelector(isSpeakerStatsDisabled);
|
||||
const _isParticipantsPaneEnabled = useSelector(isParticipantsPaneEnabled);
|
||||
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
|
||||
const _toolbarButtons = useSelector((state: IReduxState) => toolbarButtons || getToolbarButtons(state));
|
||||
const _toolbarButtons = useSelector(
|
||||
(state: IReduxState) => toolbarButtons || state['features/toolbox'].toolbarButtons);
|
||||
const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen);
|
||||
const desktopSharingButtonDisabled = useSelector(isDesktopShareButtonDisabled);
|
||||
const desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
|
||||
@@ -205,42 +205,42 @@ export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
|
||||
|
||||
useEffect(() => {
|
||||
const KEYBOARD_SHORTCUTS = [
|
||||
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
|
||||
isButtonEnabled('videoquality', _toolbarButtons) && {
|
||||
character: 'A',
|
||||
exec: onToggleVideoQuality,
|
||||
helpDescription: 'toolbar.callQuality'
|
||||
},
|
||||
isToolbarButtonEnabled('chat', _toolbarButtons) && {
|
||||
isButtonEnabled('chat', _toolbarButtons) && {
|
||||
character: 'C',
|
||||
exec: onToggleChat,
|
||||
helpDescription: 'keyboardShortcuts.toggleChat'
|
||||
},
|
||||
isToolbarButtonEnabled('desktop', _toolbarButtons) && {
|
||||
isButtonEnabled('desktop', _toolbarButtons) && {
|
||||
character: 'D',
|
||||
exec: onToggleScreenshare,
|
||||
helpDescription: 'keyboardShortcuts.toggleScreensharing'
|
||||
},
|
||||
_isParticipantsPaneEnabled && isToolbarButtonEnabled('participants-pane', _toolbarButtons) && {
|
||||
_isParticipantsPaneEnabled && isButtonEnabled('participants-pane', _toolbarButtons) && {
|
||||
character: 'P',
|
||||
exec: onToggleParticipantsPane,
|
||||
helpDescription: 'keyboardShortcuts.toggleParticipantsPane'
|
||||
},
|
||||
isToolbarButtonEnabled('raisehand', _toolbarButtons) && {
|
||||
isButtonEnabled('raisehand', _toolbarButtons) && {
|
||||
character: 'R',
|
||||
exec: onToggleRaiseHand,
|
||||
helpDescription: 'keyboardShortcuts.raiseHand'
|
||||
},
|
||||
isToolbarButtonEnabled('fullscreen', _toolbarButtons) && {
|
||||
isButtonEnabled('fullscreen', _toolbarButtons) && {
|
||||
character: 'S',
|
||||
exec: onToggleFullScreen,
|
||||
helpDescription: 'keyboardShortcuts.fullScreen'
|
||||
},
|
||||
isToolbarButtonEnabled('tileview', _toolbarButtons) && {
|
||||
isButtonEnabled('tileview', _toolbarButtons) && {
|
||||
character: 'W',
|
||||
exec: onToggleTileView,
|
||||
helpDescription: 'toolbar.tileViewToggle'
|
||||
},
|
||||
!_isSpeakerStatsDisabled && isToolbarButtonEnabled('stats', _toolbarButtons) && {
|
||||
!_isSpeakerStatsDisabled && isButtonEnabled('stats', _toolbarButtons) && {
|
||||
character: 'T',
|
||||
exec: onSpeakerStats,
|
||||
helpDescription: 'keyboardShortcuts.showSpeakerStats'
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IReduxState } from '../app/types';
|
||||
import { OVERWRITE_CONFIG, SET_CONFIG, UPDATE_CONFIG } from '../base/config/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import {
|
||||
CLEAR_TOOLBOX_TIMEOUT,
|
||||
SET_FULL_SCREEN,
|
||||
SET_TOOLBAR_BUTTONS,
|
||||
SET_TOOLBOX_TIMEOUT
|
||||
} from './actionTypes';
|
||||
import { TOOLBAR_BUTTONS, VISITORS_MODE_BUTTONS } from './constants';
|
||||
|
||||
import './subscriber.web';
|
||||
|
||||
@@ -25,6 +31,20 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
clearTimeout(timeoutID ?? undefined);
|
||||
break;
|
||||
}
|
||||
case UPDATE_CONFIG:
|
||||
case OVERWRITE_CONFIG:
|
||||
case I_AM_VISITOR_MODE:
|
||||
case SET_CONFIG: {
|
||||
const result = next(action);
|
||||
const toolbarButtons = _getToolbarButtons(store.getState());
|
||||
|
||||
store.dispatch({
|
||||
type: SET_TOOLBAR_BUTTONS,
|
||||
toolbarButtons
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case SET_FULL_SCREEN:
|
||||
return _setFullScreen(next, action);
|
||||
@@ -85,3 +105,25 @@ function _setFullScreen(next: Function, action: AnyAction) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of enabled toolbar buttons.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Array<string>} - The list of enabled toolbar buttons.
|
||||
*/
|
||||
function _getToolbarButtons(state: IReduxState): Array<string> {
|
||||
const { toolbarButtons, customToolbarButtons } = state['features/base/config'];
|
||||
const customButtons = customToolbarButtons?.map(({ id }) => id);
|
||||
let buttons = Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
|
||||
|
||||
if (iAmVisitor(state)) {
|
||||
buttons = VISITORS_MODE_BUTTONS.filter(button => buttons.indexOf(button) > -1);
|
||||
}
|
||||
|
||||
if (customButtons) {
|
||||
return [ ...buttons, ...customButtons ];
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SET_HANGUP_MENU_VISIBLE,
|
||||
SET_OVERFLOW_DRAWER,
|
||||
SET_OVERFLOW_MENU_VISIBLE,
|
||||
SET_TOOLBAR_BUTTONS,
|
||||
SET_TOOLBAR_HOVERED,
|
||||
SET_TOOLBOX_ENABLED,
|
||||
SET_TOOLBOX_SHIFT_UP,
|
||||
@@ -69,6 +70,13 @@ const INITIAL_STATE = {
|
||||
*/
|
||||
timeoutID: null,
|
||||
|
||||
/**
|
||||
* The list of enabled toolbar buttons.
|
||||
*
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
toolbarButtons: [],
|
||||
|
||||
|
||||
/**
|
||||
* The indicator that determines whether the Toolbox is visible.
|
||||
@@ -87,6 +95,7 @@ export interface IToolboxState {
|
||||
overflowMenuVisible: boolean;
|
||||
shiftUp: boolean;
|
||||
timeoutID?: number | null;
|
||||
toolbarButtons: Array<string>;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
@@ -124,6 +133,12 @@ ReducerRegistry.register<IToolboxState>(
|
||||
overflowMenuVisible: action.visible
|
||||
};
|
||||
|
||||
case SET_TOOLBAR_BUTTONS:
|
||||
return {
|
||||
...state,
|
||||
toolbarButtons: action.toolbarButtons
|
||||
};
|
||||
|
||||
case SET_TOOLBAR_HOVERED:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -6,3 +6,41 @@ export interface IToolboxButton {
|
||||
group: number;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export type ToolbarButton = 'camera' |
|
||||
'chat' |
|
||||
'closedcaptions' |
|
||||
'desktop' |
|
||||
'download' |
|
||||
'embedmeeting' |
|
||||
'etherpad' |
|
||||
'feedback' |
|
||||
'filmstrip' |
|
||||
'fullscreen' |
|
||||
'hangup' |
|
||||
'help' |
|
||||
'highlight' |
|
||||
'invite' |
|
||||
'linktosalesforce' |
|
||||
'livestreaming' |
|
||||
'microphone' |
|
||||
'mute-everyone' |
|
||||
'mute-video-everyone' |
|
||||
'noisesuppression' |
|
||||
'participants-pane' |
|
||||
'profile' |
|
||||
'raisehand' |
|
||||
'reactions' |
|
||||
'recording' |
|
||||
'security' |
|
||||
'select-background' |
|
||||
'settings' |
|
||||
'shareaudio' |
|
||||
'sharedvideo' |
|
||||
'shortcuts' |
|
||||
'stats' |
|
||||
'tileview' |
|
||||
'toggle-camera' |
|
||||
'videoquality' |
|
||||
'whiteboard' |
|
||||
'__end';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
// @ts-ignore
|
||||
export { default as DemoteToVisitorDialog } from './native/DemoteToVisitorDialog';
|
||||
// @ts-ignore
|
||||
export { default as GrantModeratorDialog } from './native/GrantModeratorDialog';
|
||||
// @ts-ignore
|
||||
export { default as KickRemoteParticipantDialog } from './native/KickRemoteParticipantDialog';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as DemoteToVisitorDialog } from './web/DemoteToVisitorDialog';
|
||||
export { default as GrantModeratorDialog } from './web/GrantModeratorDialog';
|
||||
export { default as KickRemoteParticipantDialog } from './web/KickRemoteParticipantDialog';
|
||||
export { default as MuteEveryoneDialog } from './web/MuteEveryoneDialog';
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
|
||||
import DemoteToVisitorDialog from './DemoteToVisitorDialog';
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant that this button is supposed to kick.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for demoting a participant to visitor.
|
||||
*/
|
||||
class DemoteToVisitorButton extends AbstractButton<IProps> {
|
||||
accessibilityLabel = 'videothumbnail.demote';
|
||||
icon = IconUsers;
|
||||
label = 'videothumbnail.demote';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and demoting the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(openDialog(DemoteToVisitorDialog, { participantID }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
visible: state['features/visitors'].supported
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(DemoteToVisitorButton));
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
|
||||
import { DialogProps } from '../../../base/dialog/constants';
|
||||
import { demoteRequest } from '../../../visitors/actions';
|
||||
|
||||
interface IProps extends DialogProps {
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be demoted.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to confirm a remote participant demote to visitor action.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function DemoteToVisitorDialog({ participantID }: IProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const handleSubmit = useCallback(() => {
|
||||
dispatch(demoteRequest(participantID));
|
||||
|
||||
return true; // close dialog
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = 'dialog.Cancel'
|
||||
confirmLabel = 'dialog.confirm'
|
||||
descriptionKey = 'dialog.demoteParticipantDialog'
|
||||
isConfirmDestructive = { true }
|
||||
onSubmit = { handleSubmit }
|
||||
title = 'dialog.demoteParticipantTitle' />
|
||||
);
|
||||
}
|
||||
@@ -4,17 +4,20 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { hideSheet } from '../../../base/dialog/actions';
|
||||
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
|
||||
import { bottomSheetStyles } from '../../../base/dialog/components/native/styles';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCount,
|
||||
getParticipantDisplayName
|
||||
} from '../../../base/participants/functions';
|
||||
import { ILocalParticipant } from '../../../base/participants/types';
|
||||
import ToggleSelfViewButton from '../../../toolbox/components/native/ToggleSelfViewButton';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
@@ -34,6 +37,11 @@ interface IProps {
|
||||
*/
|
||||
_participantDisplayName: string;
|
||||
|
||||
/**
|
||||
* Shows/hides the local switch to visitor button.
|
||||
*/
|
||||
_showDemote: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
@@ -57,6 +65,7 @@ class LocalVideoMenu extends PureComponent<IProps> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._renderMenuHeader = this._renderMenuHeader.bind(this);
|
||||
}
|
||||
|
||||
@@ -66,8 +75,9 @@ class LocalVideoMenu extends PureComponent<IProps> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _participant } = this.props;
|
||||
const { _participant, _showDemote } = this.props;
|
||||
const buttonProps = {
|
||||
afterClick: this._onCancel,
|
||||
showLabel: true,
|
||||
participantID: _participant?.id ?? '',
|
||||
styles: bottomSheetStyles.buttons
|
||||
@@ -78,6 +88,7 @@ class LocalVideoMenu extends PureComponent<IProps> {
|
||||
renderHeader = { this._renderMenuHeader }
|
||||
showSlidingView = { true }>
|
||||
<ToggleSelfViewButton { ...buttonProps } />
|
||||
{ _showDemote && <DemoteToVisitorButton { ...buttonProps } /> }
|
||||
<ConnectionStatusButton { ...buttonProps } />
|
||||
</BottomSheet>
|
||||
);
|
||||
@@ -105,6 +116,16 @@ class LocalVideoMenu extends PureComponent<IProps> {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to hide the {@code RemoteVideoMenu}.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.dispatch(hideSheet());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,7 +140,8 @@ function _mapStateToProps(state: IReduxState) {
|
||||
|
||||
return {
|
||||
_participant: participant,
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant?.id ?? '')
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant?.id ?? ''),
|
||||
_showDemote: getParticipantCount(state) > 1
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import PrivateMessageButton from '../../../chat/components/native/PrivateMessage
|
||||
|
||||
import AskUnmuteButton from './AskUnmuteButton';
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import MuteButton from './MuteButton';
|
||||
@@ -92,6 +93,11 @@ interface IProps {
|
||||
*/
|
||||
_rooms: Array<IRoom>;
|
||||
|
||||
/**
|
||||
* Whether to display the demote button.
|
||||
*/
|
||||
_showDemote: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
@@ -139,6 +145,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
|
||||
_isParticipantAvailable,
|
||||
_moderator,
|
||||
_rooms,
|
||||
_showDemote,
|
||||
_currentRoomId,
|
||||
participantId,
|
||||
t
|
||||
@@ -168,6 +175,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
|
||||
{ !_disableKick && <KickButton { ...buttonProps } /> }
|
||||
{ !_disableGrantModerator && !_isBreakoutRoom && <GrantModeratorButton { ...buttonProps } /> }
|
||||
<PinButton { ...buttonProps } />
|
||||
{ _showDemote && <DemoteToVisitorButton { ...buttonProps } /> }
|
||||
{ !_disablePrivateChat && <PrivateMessageButton { ...buttonProps } /> }
|
||||
<ConnectionStatusButton { ...connectionStatusButtonProps } />
|
||||
{_moderator && _rooms.length > 1 && <>
|
||||
@@ -252,7 +260,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_isParticipantAvailable: Boolean(isParticipantAvailable),
|
||||
_moderator: moderator,
|
||||
_participantDisplayName: getParticipantDisplayName(state, participantId),
|
||||
_rooms
|
||||
_rooms,
|
||||
_showDemote: moderator
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/constants';
|
||||
import { IButtonProps } from '../../types';
|
||||
|
||||
import DemoteToVisitorDialog from './DemoteToVisitorDialog';
|
||||
|
||||
interface IProps extends IButtonProps {
|
||||
|
||||
/**
|
||||
* Button text class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* Whether the icon should be hidden or not.
|
||||
*/
|
||||
noIcon?: boolean;
|
||||
|
||||
/**
|
||||
* Click handler executed aside from the main action.
|
||||
*/
|
||||
onClick?: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for demoting a participant to visitor.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function DemoteToVisitorButton({
|
||||
className,
|
||||
noIcon = false,
|
||||
notifyClick,
|
||||
notifyMode,
|
||||
participantID
|
||||
}: IProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
notifyClick?.();
|
||||
if (notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY) {
|
||||
return;
|
||||
}
|
||||
dispatch(openDialog(DemoteToVisitorDialog, { participantID }));
|
||||
}, [ dispatch, notifyClick, notifyMode, participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { t('videothumbnail.demote') }
|
||||
className = { className || 'demotelink' } // can be used in tests
|
||||
icon = { noIcon ? null : IconUsers }
|
||||
id = { `demotelink_${participantID}` }
|
||||
onClick = { handleClick }
|
||||
text = { t('videothumbnail.demote') } />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { DialogProps } from '../../../base/dialog/constants';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { demoteRequest } from '../../../visitors/actions';
|
||||
|
||||
interface IProps extends DialogProps {
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be demoted.
|
||||
*/
|
||||
participantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog to confirm a remote participant demote action.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function DemoteToVisitorDialog({ participantID }: IProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
dispatch(demoteRequest(participantID));
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.confirm' }}
|
||||
onSubmit = { handleSubmit }
|
||||
titleKey = 'dialog.demoteParticipantTitle'>
|
||||
<div>
|
||||
{ t('dialog.demoteParticipantDialog') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class HideSelfViewVideoButton extends PureComponent<IProps> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code FlipLocalVideoButton}'s props.
|
||||
* Maps (parts of) the Redux state to the associated {@code HideSelfViewVideoButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getButtonNotifyMode, getParticipantMenuButtonsWithNotifyClick } from '../../../base/config/functions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconDotsHorizontal } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getLocalParticipant, getParticipantCount } from '../../../base/participants/functions';
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import { setParticipantContextMenuOpen } from '../../../base/responsive-ui/actions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions.web';
|
||||
@@ -23,6 +23,7 @@ import { renderConnectionStatus } from '../../actions.web';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import FlipLocalVideoButton from './FlipLocalVideoButton';
|
||||
import HideSelfViewVideoButton from './HideSelfViewVideoButton';
|
||||
import TogglePinToStageButton from './TogglePinToStageButton';
|
||||
@@ -54,6 +55,11 @@ interface IProps {
|
||||
*/
|
||||
_showConnectionInfo: boolean;
|
||||
|
||||
/**
|
||||
* Shows/hides the local switch to visitor button.
|
||||
*/
|
||||
_showDemote: boolean;
|
||||
|
||||
/**
|
||||
* Whether to render the hide self view button.
|
||||
*/
|
||||
@@ -131,6 +137,7 @@ const LocalVideoMenuTriggerButton = ({
|
||||
_menuPosition,
|
||||
_overflowDrawer,
|
||||
_showConnectionInfo,
|
||||
_showDemote,
|
||||
_showHideSelfViewButton,
|
||||
_showLocalVideoFlipButton,
|
||||
_showPinToStage,
|
||||
@@ -143,6 +150,7 @@ const LocalVideoMenuTriggerButton = ({
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
const visitorsSupported = useSelector((state: IReduxState) => state['features/visitors'].supported);
|
||||
|
||||
const notifyClick = useCallback(
|
||||
(buttonKey: string) => {
|
||||
@@ -206,6 +214,16 @@ const LocalVideoMenuTriggerButton = ({
|
||||
onClick = { hidePopover }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
{
|
||||
_showDemote && visitorsSupported && <DemoteToVisitorButton
|
||||
className = { _overflowDrawer ? classes.flipText : '' }
|
||||
noIcon = { true }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
notifyClick = { () => notifyClick(BUTTONS.DEMOTE) }
|
||||
notifyMode = { getButtonNotifyMode(BUTTONS.DEMOTE, buttonsWithNotifyClick) }
|
||||
onClick = { hidePopover }
|
||||
participantID = { _localParticipantId } />
|
||||
}
|
||||
{
|
||||
isMobileBrowser() && <ConnectionStatusButton
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
@@ -274,6 +292,7 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
|
||||
return {
|
||||
_menuPosition,
|
||||
_showDemote: getParticipantCount(state) > 1,
|
||||
_showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
|
||||
_showHideSelfViewButton: showHideSelfViewButton,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
|
||||
@@ -31,6 +31,7 @@ import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
import AskToUnmuteButton from './AskToUnmuteButton';
|
||||
import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import CustomOptionButton from './CustomOptionButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import MuteButton from './MuteButton';
|
||||
@@ -141,7 +142,8 @@ const ParticipantContextMenu = ({
|
||||
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
|
||||
= useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
|
||||
const { disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
|
||||
const visitorsSupported = useSelector((state: IReduxState) => state['features/visitors'].supported);
|
||||
const { disableDemote, disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
|
||||
const { participantsVolume } = useSelector((state: IReduxState) => state['features/filmstrip']);
|
||||
const _volume = (participant?.local ?? true ? undefined
|
||||
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
|
||||
@@ -245,6 +247,10 @@ const ParticipantContextMenu = ({
|
||||
buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
|
||||
}
|
||||
|
||||
if (!disableDemote && visitorsSupported && _isModerator) {
|
||||
buttons2.push(<DemoteToVisitorButton { ...getButtonProps(BUTTONS.DEMOTE) } />);
|
||||
}
|
||||
|
||||
if (!disableKick) {
|
||||
buttons2.push(<KickButton { ...getButtonProps(BUTTONS.KICK) } />);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export const PARTICIPANT_MENU_BUTTONS = {
|
||||
ALLOW_VIDEO: 'allow-video',
|
||||
ASK_UNMUTE: 'ask-unmute',
|
||||
CONN_STATUS: 'conn-status',
|
||||
DEMOTE: 'demote',
|
||||
FLIP_LOCAL_VIDEO: 'flip-local-video',
|
||||
GRANT_MODERATOR: 'grant-moderator',
|
||||
HIDE_SELF_VIEW: 'hide-self-view',
|
||||
|
||||
@@ -38,3 +38,23 @@ export const VISITOR_PROMOTION_REQUEST = 'VISITOR_PROMOTION_REQUEST';
|
||||
* }
|
||||
*/
|
||||
export const CLEAR_VISITOR_PROMOTION_REQUEST = 'CLEAR_VISITOR_PROMOTION_REQUEST';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets visitor demote actor.
|
||||
*
|
||||
* {
|
||||
* type: SET_VISITOR_DEMOTE_ACTOR,
|
||||
* displayName: string
|
||||
* }
|
||||
*/
|
||||
export const SET_VISITOR_DEMOTE_ACTOR = 'SET_VISITOR_DEMOTE_ACTOR';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets visitors support.
|
||||
*
|
||||
* {
|
||||
* type: SET_VISITORS_SUPPORTED,
|
||||
* value: string
|
||||
* }
|
||||
*/
|
||||
export const SET_VISITORS_SUPPORTED = 'SET_VISITORS_SUPPORTED';
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { createRemoteVideoMenuButtonEvent } from '../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../analytics/functions';
|
||||
import { IStore } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { connect, disconnect, setPreferVisitor } from '../base/connection/actions';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
|
||||
import {
|
||||
CLEAR_VISITOR_PROMOTION_REQUEST,
|
||||
I_AM_VISITOR_MODE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
UPDATE_VISITORS_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
@@ -19,13 +25,11 @@ export function admitMultiple(requests: Array<IPromotionRequest>): Function {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
requests.forEach(r => {
|
||||
conference?.sendMessage({
|
||||
type: 'visitors',
|
||||
action: 'promotion-response',
|
||||
approved: true,
|
||||
id: r.from
|
||||
});
|
||||
conference?.sendMessage({
|
||||
type: 'visitors',
|
||||
action: 'promotion-response',
|
||||
approved: true,
|
||||
ids: requests.map(r => r.from)
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -72,6 +76,34 @@ export function denyRequest(request: IPromotionRequest) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a demote request to a main participant to join the meeting as a visitor.
|
||||
*
|
||||
* @param {string} id - The ID for the participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function demoteRequest(id: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const conference = getCurrentConference(getState);
|
||||
const localParticipant = getLocalParticipant(getState());
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent('demote.button', { 'participant_id': id }));
|
||||
|
||||
if (id === localParticipant?.id) {
|
||||
dispatch(disconnect(true))
|
||||
.then(() => dispatch(setPreferVisitor(true)))
|
||||
.then(() => dispatch(connect()));
|
||||
} else {
|
||||
conference?.sendMessage({
|
||||
type: 'visitors',
|
||||
action: 'demote-request',
|
||||
id,
|
||||
actor: localParticipant?.id
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a promotion request from the state.
|
||||
*
|
||||
@@ -118,6 +150,36 @@ export function setIAmVisitor(enabled: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets visitor demote actor.
|
||||
*
|
||||
* @param {string|undefined} displayName - The display name of the participant.
|
||||
* @returns {{
|
||||
* type: SET_VISITOR_DEMOTE_ACTOR,
|
||||
* }}
|
||||
*/
|
||||
export function setVisitorDemoteActor(displayName: string | undefined) {
|
||||
return {
|
||||
type: SET_VISITOR_DEMOTE_ACTOR,
|
||||
displayName
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitors count has been updated.
|
||||
*
|
||||
* @param {boolean} value - The new value whether visitors are supported.
|
||||
* @returns {{
|
||||
* type: SET_VISITORS_SUPPORTED,
|
||||
* }}
|
||||
*/
|
||||
export function setVisitorsSupported(value: boolean) {
|
||||
return {
|
||||
type: SET_VISITORS_SUPPORTED,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitors count has been updated.
|
||||
*
|
||||
|
||||
3
react/features/visitors/logger.ts
Normal file
3
react/features/visitors/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/visitors');
|
||||
@@ -7,8 +7,11 @@ import {
|
||||
CONFERENCE_JOIN_IN_PROGRESS,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { connect, setPreferVisitor } from '../base/connection/actions';
|
||||
import { disconnect } from '../base/connection/actions.any';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
import { hideNotification, showNotification } from '../notifications/actions';
|
||||
@@ -17,6 +20,7 @@ import {
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
VISITORS_PROMOTION_NOTIFICATION_ID
|
||||
} from '../notifications/constants';
|
||||
import { INotificationProps } from '../notifications/types';
|
||||
import { open as openParticipantsPane } from '../participants-pane/actions';
|
||||
|
||||
import {
|
||||
@@ -24,9 +28,12 @@ import {
|
||||
clearPromotionRequest,
|
||||
denyRequest,
|
||||
promotionRequestReceived,
|
||||
setVisitorDemoteActor,
|
||||
setVisitorsSupported,
|
||||
updateVisitorsCount
|
||||
} from './actions';
|
||||
import { getPromotionRequests } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
switch (action.type) {
|
||||
@@ -46,28 +53,70 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { conference } = action;
|
||||
|
||||
if (getState()['features/visitors'].iAmVisitor) {
|
||||
dispatch(showNotification({
|
||||
const { demoteActorDisplayName } = getState()['features/visitors'];
|
||||
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
|
||||
const notificationParams: INotificationProps = {
|
||||
titleKey: 'visitors.notification.title',
|
||||
descriptionKey: 'visitors.notification.description'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
};
|
||||
|
||||
if (demoteActorDisplayName) {
|
||||
notificationParams.descriptionKey = 'visitors.notification.demoteDescription';
|
||||
notificationParams.descriptionArguments = {
|
||||
actor: demoteActorDisplayName
|
||||
};
|
||||
}
|
||||
|
||||
// check for demote actor and update notification
|
||||
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
} else {
|
||||
dispatch(setVisitorsSupported(conference.isVisitorsSupported()));
|
||||
conference.on(JitsiConferenceEvents.VISITORS_SUPPORTED_CHANGED, (value: boolean) => {
|
||||
dispatch(setVisitorsSupported(value));
|
||||
});
|
||||
}
|
||||
|
||||
conference.on(JitsiConferenceEvents.VISITORS_MESSAGE, (
|
||||
msg: { from: string; nick: string; on: boolean; }) => {
|
||||
const request = {
|
||||
from: msg.from,
|
||||
nick: msg.nick
|
||||
};
|
||||
msg: { action: string; actor: string; from: string; id: string; nick: string; on: boolean; }) => {
|
||||
|
||||
if (msg.on) {
|
||||
dispatch(promotionRequestReceived(request));
|
||||
if (msg.action === 'demote-request') {
|
||||
// we need it before the disconnect
|
||||
const participantById = getParticipantById(getState, msg.actor);
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
|
||||
if (localParticipant && localParticipant.id === msg.id) {
|
||||
// handle demote
|
||||
dispatch(disconnect(true))
|
||||
.then(() => dispatch(setPreferVisitor(true)))
|
||||
.then(() => {
|
||||
// we need to set the name, so we can use it later in the notification
|
||||
if (participantById) {
|
||||
dispatch(setVisitorDemoteActor(participantById.name));
|
||||
}
|
||||
|
||||
return dispatch(connect());
|
||||
});
|
||||
}
|
||||
} else if (msg.action === 'promotion-request') {
|
||||
const request = {
|
||||
from: msg.from,
|
||||
nick: msg.nick
|
||||
};
|
||||
|
||||
if (msg.on) {
|
||||
dispatch(promotionRequestReceived(request));
|
||||
} else {
|
||||
dispatch(clearPromotionRequest(request));
|
||||
}
|
||||
_handlePromotionNotification({
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
} else {
|
||||
dispatch(clearPromotionRequest(request));
|
||||
logger.error('Unknown action:', msg.action);
|
||||
}
|
||||
_handlePromotionNotification({
|
||||
dispatch,
|
||||
getState
|
||||
});
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.VISITORS_REJECTION, () => {
|
||||
@@ -137,7 +186,7 @@ function _handlePromotionNotification(
|
||||
waitingParticipants: requests.length
|
||||
});
|
||||
icon = NOTIFICATION_ICON.PARTICIPANTS;
|
||||
customActionNameKey = [ 'notify.viewLobby' ];
|
||||
customActionNameKey = [ 'notify.viewVisitors' ];
|
||||
customActionType = [ BUTTON_TYPES.PRIMARY ];
|
||||
customActionHandler = [ () => batch(() => {
|
||||
dispatch(hideNotification(VISITORS_PROMOTION_NOTIFICATION_ID));
|
||||
|
||||
@@ -4,6 +4,8 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import {
|
||||
CLEAR_VISITOR_PROMOTION_REQUEST,
|
||||
I_AM_VISITOR_MODE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
UPDATE_VISITORS_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
@@ -13,13 +15,16 @@ const DEFAULT_STATE = {
|
||||
count: -1,
|
||||
iAmVisitor: false,
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: []
|
||||
};
|
||||
|
||||
export interface IVisitorsState {
|
||||
count?: number;
|
||||
demoteActorDisplayName?: string;
|
||||
iAmVisitor: boolean;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
}
|
||||
ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_STATE, action): IVisitorsState => {
|
||||
switch (action.type) {
|
||||
@@ -50,6 +55,18 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
iAmVisitor: action.enabled
|
||||
};
|
||||
}
|
||||
case SET_VISITOR_DEMOTE_ACTOR: {
|
||||
return {
|
||||
...state,
|
||||
demoteActorDisplayName: action.displayName
|
||||
};
|
||||
}
|
||||
case SET_VISITORS_SUPPORTED: {
|
||||
return {
|
||||
...state,
|
||||
supported: action.value
|
||||
};
|
||||
}
|
||||
case VISITOR_PROMOTION_REQUEST: {
|
||||
const currentRequests = state.promotionRequests || [];
|
||||
|
||||
|
||||
@@ -238,16 +238,6 @@ function destroy_lobby_room(room, newjid, message)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle multiple items at once
|
||||
function handle_admin_query_set_command(self, origin, stanza)
|
||||
for i=1,#stanza.tags[1] do
|
||||
if handle_admin_query_set_command_item(self, origin, stanza, stanza.tags[1].tags[i]) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
-- This is a copy of the function(handle_admin_query_set_command) from prosody 12 (d7857ef7843a)
|
||||
function handle_admin_query_set_command_item(self, origin, stanza, item)
|
||||
if not item then
|
||||
@@ -309,6 +299,58 @@ function handle_admin_query_set_command_item(self, origin, stanza, item)
|
||||
end
|
||||
end
|
||||
|
||||
-- this is extracted from prosody to handle multiple invites
|
||||
function handle_mediated_invite(room, origin, stanza, payload, host_module)
|
||||
local invitee = jid_prep(payload.attr.to);
|
||||
if not invitee then
|
||||
origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
|
||||
return true;
|
||||
elseif host_module:fire_event("muc-pre-invite", {room = room, origin = origin, stanza = stanza}) then
|
||||
return true;
|
||||
end
|
||||
local invite = muc_util.filter_muc_x(st.clone(stanza));
|
||||
invite.attr.from = room.jid;
|
||||
invite.attr.to = invitee;
|
||||
invite:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
|
||||
:tag('invite', {from = stanza.attr.from;})
|
||||
:tag('reason'):text(payload:get_child_text("reason")):up()
|
||||
:up()
|
||||
:up();
|
||||
if not host_module:fire_event("muc-invite", {room = room, stanza = invite, origin = origin, incoming = stanza}) then
|
||||
room:route_stanza(invite);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local prosody_overrides = {
|
||||
-- handle multiple items at once
|
||||
handle_admin_query_set_command = function(self, origin, stanza)
|
||||
for i=1,#stanza.tags[1] do
|
||||
if handle_admin_query_set_command_item(self, origin, stanza, stanza.tags[1].tags[i]) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end,
|
||||
-- this is extracted from prosody to handle multiple invites
|
||||
handle_message_to_room = function(room, origin, stanza, host_module)
|
||||
local type = stanza.attr.type;
|
||||
if type == nil or type == "normal" then
|
||||
local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
|
||||
if x then
|
||||
local handled = false;
|
||||
for _, payload in pairs(x.tags) do
|
||||
if payload ~= nil and payload.name == "invite" and payload.attr.to then
|
||||
handled = true;
|
||||
handle_mediated_invite(room, origin, stanza, payload, host_module)
|
||||
end
|
||||
end
|
||||
return handled;
|
||||
end
|
||||
end
|
||||
end
|
||||
};
|
||||
|
||||
-- operates on already loaded lobby muc module
|
||||
function process_lobby_muc_loaded(lobby_muc, host_module)
|
||||
module:log('debug', 'Lobby muc loaded');
|
||||
@@ -520,8 +562,10 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
for event_name, method in pairs {
|
||||
-- Normal room interactions
|
||||
["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["message/bare"] = "handle_message_to_room" ;
|
||||
-- Host room
|
||||
["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["message/host"] = "handle_message_to_room" ;
|
||||
} do
|
||||
host_module:hook(event_name, function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
@@ -529,7 +573,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if room then
|
||||
return handle_admin_query_set_command(room, origin, stanza);
|
||||
return prosody_overrides[method](room, origin, stanza, host_module);
|
||||
end
|
||||
end, 1) -- make sure we handle it before prosody that uses priority -2 for this
|
||||
end
|
||||
|
||||
@@ -22,7 +22,9 @@ local function load_config()
|
||||
-- Max allowed login rate in events per second.
|
||||
config.login_rate = module:get_option_number("rate_limit_login_rate", 3);
|
||||
-- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
|
||||
config.session_rate = module:get_option_number("rate_limit_session_rate", 2000);
|
||||
config.ip_rate = module:get_option_number("rate_limit_ip_rate", 2000);
|
||||
-- The rate to which sessions exceeding the stanza(iq, presence, message) rate will be limited, in bytes per second.
|
||||
config.session_rate = module:get_option_number("rate_limit_session_rate", 1000);
|
||||
-- The time in seconds, after which the limit for an IP address is lifted.
|
||||
config.timeout = module:get_option_number("rate_limit_timeout", 60);
|
||||
-- List of regular expressions for IP addresses that are not limited by this module.
|
||||
@@ -33,7 +35,7 @@ local function load_config()
|
||||
-- Max allowed presence rate in events per second.
|
||||
config.presence_rate = module:get_option_number("rate_limit_presence_rate", 4);
|
||||
-- Max allowed iq rate in events per second.
|
||||
config.iq_rate = module:get_option_number("rate_limit_iq_rate", 10);
|
||||
config.iq_rate = module:get_option_number("rate_limit_iq_rate", 15);
|
||||
-- Max allowed message rate in events per second.
|
||||
config.message_rate = module:get_option_number("rate_limit_message_rate", 3);
|
||||
|
||||
@@ -45,8 +47,8 @@ local function load_config()
|
||||
local wl_hosts = "";
|
||||
for j in config.whitelist_hosts do wl_hosts = wl_hosts .. j .. "," end
|
||||
module:log("info", "Loaded configuration: ");
|
||||
module:log("info", "- session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_hosts=%s",
|
||||
config.session_rate, config.timeout, config.cache_size, wl, wl_hosts);
|
||||
module:log("info", "- ip_rate=%s bytes/sec, session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_hosts=%s",
|
||||
config.ip_rate, config.session_rate, config.timeout, config.cache_size, wl, wl_hosts);
|
||||
module:log("info", "- login_rate=%s/sec, presence_rate=%s/sec, iq_rate=%s/sec, message_rate=%s/sec",
|
||||
config.login_rate, config.presence_rate, config.iq_rate, config.message_rate);
|
||||
end
|
||||
@@ -94,7 +96,7 @@ local function limit_bytes_in(bytes, session)
|
||||
if sess_throttle then
|
||||
-- if the limit timeout has elapsed let's stop the throttle
|
||||
if not sess_throttle.start or gettime() - sess_throttle.start > config.timeout then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session, session.ip);
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
|
||||
session.jitsi_throttle = nil;
|
||||
return bytes;
|
||||
end
|
||||
@@ -121,15 +123,30 @@ local function limit_bytes_in(bytes, session)
|
||||
end
|
||||
|
||||
-- Throttles reading from the connection of a specific session.
|
||||
local function throttle_session(session)
|
||||
if not session.jitsi_throttle then
|
||||
local function throttle_session(session, rate, timeout)
|
||||
if not session.jitsi_throttle then
|
||||
if (session.conn and session.conn.setlimit) then
|
||||
-- TODO: we don't have a mechanism to unthrottle a session in this case.
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s.", config.session_rate, session.id, session.ip);
|
||||
session.conn:setlimit(config.session_rate);
|
||||
session.jitsi_throttle_counter = session.jitsi_throttle_counter + 1;
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s, counter=%s.",
|
||||
rate, session.id, session.ip, session.jitsi_throttle_counter);
|
||||
session.conn:setlimit(rate);
|
||||
if timeout then
|
||||
if session.jitsi_throttle_timer then
|
||||
-- if there was a timer stop it as we will schedule a new one
|
||||
session.jitsi_throttle_timer:stop();
|
||||
session.jitsi_throttle_timer = nil;
|
||||
end
|
||||
session.jitsi_throttle_timer = module:add_timer(timeout, function()
|
||||
if session.conn then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
|
||||
session.conn:setlimit(0);
|
||||
end
|
||||
session.jitsi_throttle_timer = nil;
|
||||
end);
|
||||
end
|
||||
else
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", config.session_rate, session.id, session.ip);
|
||||
session.jitsi_throttle = new_throttle(config.session_rate, 2);
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", rate, session.id, session.ip);
|
||||
session.jitsi_throttle = new_throttle(rate, 2);
|
||||
filters.add_filter(session, "bytes/in", limit_bytes_in, 1000);
|
||||
-- throttle.start used for stop throttling after the timeout
|
||||
session.jitsi_throttle.start = gettime();
|
||||
@@ -147,7 +164,7 @@ function filter_stanza(stanza, session)
|
||||
local ok, _, _ = rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "%s rate exceeded for %s, limiting.", stanza.name, session.full_jid);
|
||||
throttle_session(session);
|
||||
throttle_session(session, config.session_rate, config.timeout);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -166,7 +183,6 @@ local function on_login(session, ip)
|
||||
if not ok then
|
||||
module:log("info", "Join rate exceeded for %s, limiting.", ip);
|
||||
limit_ip(ip);
|
||||
throttle_session(session);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -186,6 +202,7 @@ local function filter_hook(session)
|
||||
on_login(session, ip);
|
||||
|
||||
-- creates the stanzas rates
|
||||
session.jitsi_throttle_counter = 0;
|
||||
session.presence_rate = new_throttle(config.presence_rate, 2);
|
||||
session.iq_rate = new_throttle(config.iq_rate, 2);
|
||||
session.message_rate = new_throttle(config.message_rate, 2);
|
||||
@@ -195,12 +212,12 @@ local function filter_hook(session)
|
||||
if oldt then
|
||||
local newt = gettime();
|
||||
local elapsed = newt - oldt;
|
||||
if elapsed < 5 then
|
||||
module:log("info", "IP address %s was limitted %s seconds ago, refreshing.", ip, elapsed);
|
||||
limited_ips:set(ip, newt);
|
||||
throttle_session(session);
|
||||
elseif elapsed < config.timeout then
|
||||
throttle_session(session);
|
||||
if elapsed < config.timeout then
|
||||
if elapsed < 5 then
|
||||
module:log("info", "IP address %s was limited %s seconds ago, refreshing.", ip, elapsed);
|
||||
limited_ips:set(ip, newt);
|
||||
end
|
||||
throttle_session(session, config.ip_rate);
|
||||
else
|
||||
module:log("info", "Removing the limit for %s", ip);
|
||||
limited_ips:set(ip, nil);
|
||||
|
||||
@@ -15,6 +15,7 @@ local process_host_module = util.process_host_module;
|
||||
local new_id = require 'util.id'.medium;
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local json = require 'util.json';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
@@ -30,6 +31,10 @@ local ignore_list = module:context(muc_domain_base):get_option_set('visitors_ign
|
||||
|
||||
local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promotion', false);
|
||||
|
||||
-- whether to always advertise that visitors feature is enabled for rooms
|
||||
-- can be set to off and being controlled by another module, turning it on and off for rooms
|
||||
local always_visitors_enabled = module:get_option_boolean('always_visitors_enabled', true);
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
@@ -90,11 +95,13 @@ local function request_promotion_received(room, from_jid, from_vnode, nick, time
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
local node = jid.node(room.jid);
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = string.gsub(room.jid, muc_domain_base, req_from),
|
||||
room = jid.join(node, muc_domain_prefix..'.'..req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
@@ -261,6 +268,37 @@ local function stanza_handler(event)
|
||||
return processed;
|
||||
end
|
||||
|
||||
local function process_promotion_response(room, id, approved)
|
||||
-- lets reply to participant that requested promotion
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
from = visitors_promotion_requests[room.jid][id].from;
|
||||
jid = id;
|
||||
};
|
||||
|
||||
local req_from = visitors_promotion_map[room.jid][username].from;
|
||||
local req_jid = visitors_promotion_map[room.jid][username].jid;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
local node = jid.node(room.jid);
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = jid.join(node, muc_domain_prefix..'.'..req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
jid = req_jid,
|
||||
username = username,
|
||||
allow = approved }):up());
|
||||
end
|
||||
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_module, host)
|
||||
@@ -349,7 +387,8 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
return;
|
||||
end
|
||||
local data = json.decode(json_data);
|
||||
if not data or data.type ~= 'visitors' or data.action ~= "promotion-response" then
|
||||
if not data or data.type ~= 'visitors'
|
||||
or (data.action ~= "promotion-response" and data.action ~= "demote-request") then
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -367,48 +406,58 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
return false;
|
||||
end
|
||||
|
||||
-- let's forward to every moderator, this is so they now that this moderator
|
||||
-- took action and they can update UI, as this msg was initially a group chat but we are
|
||||
-- sending it now as provide chat, let's change the type
|
||||
stanza.attr.type = 'chat'; -- it is safe as we are not using this stanza instance anymore
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- if moderator send the message
|
||||
if room_occupant.role == 'moderator'
|
||||
and room_occupant.jid ~= occupant.jid
|
||||
and not is_admin(room_occupant.bare_jid) then
|
||||
stanza.attr.to = room_occupant.nick;
|
||||
room:route_stanza(stanza);
|
||||
if data.action == "demote-request" then
|
||||
if occupant.nick ~= room.jid..'/'..data.actor then
|
||||
module:log('error', 'Bad actor in demote request %s', stanza);
|
||||
event.origin.send(st.error_reply(stanza, "cancel", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- when demoting we want to send message to the demoted participant and to moderators
|
||||
local target_jid = room.jid..'/'..data.id;
|
||||
stanza.attr.type = 'chat'; -- it is safe as we are not using this stanza instance anymore
|
||||
stanza.attr.from = module.host;
|
||||
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- do not send it to jicofo or back to the sender
|
||||
if room_occupant.jid ~= occupant.jid and not is_admin(room_occupant.bare_jid) then
|
||||
if room_occupant.role == 'moderator'
|
||||
or room_occupant.nick == target_jid then
|
||||
stanza.attr.to = room_occupant.jid;
|
||||
room:route_stanza(stanza);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
if data.id then
|
||||
process_promotion_response(room, data.id, data.approved and 'true' or 'false');
|
||||
else
|
||||
-- we are in the case with admit all, we need to read data.ids
|
||||
for i in pairs(data.ids) do
|
||||
process_promotion_response(room, data.id, data.approved and 'true' or 'false');
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- lets reply to participant that requested promotion
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
from = visitors_promotion_requests[room.jid][data.id].from;
|
||||
jid = data.id;
|
||||
};
|
||||
|
||||
local req_from = visitors_promotion_map[room.jid][username].from;
|
||||
local req_jid = visitors_promotion_map[room.jid][username].jid;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = string.gsub(room.jid, muc_domain_base, req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
jid = req_jid,
|
||||
username = username ,
|
||||
allow = data.approved and 'true' or 'false' }):up());
|
||||
return true; -- halt processing, but return true that we handled it
|
||||
end);
|
||||
|
||||
if always_visitors_enabled then
|
||||
local visitorsEnabledField = {
|
||||
name = "muc#roominfo_visitorsEnabled";
|
||||
type = "boolean";
|
||||
label = "Whether visitors are enabled.";
|
||||
value = 1;
|
||||
};
|
||||
-- Append "visitors enabled" to the MUC config form.
|
||||
host_module:context(host):hook("muc-disco#info", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
host_module:context(host):hook("muc-config-form", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
prosody.events.add_handler('pre-jitsi-authentication', function(session)
|
||||
|
||||
@@ -439,7 +439,8 @@ function Util:verify_room(session, room_address)
|
||||
if session.jitsi_meet_str_tenant
|
||||
and string.lower(session.jitsi_meet_str_tenant) ~= session.jitsi_web_query_prefix then
|
||||
module:log('warn', 'Tenant differs for user:%s group:%s url_tenant:%s token_tenant:%s',
|
||||
inspect(session.jitsi_meet_context_user), session.jitsi_meet_context_group,
|
||||
session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or '',
|
||||
session.jitsi_meet_context_group,
|
||||
session.jitsi_web_query_prefix, session.jitsi_meet_str_tenant);
|
||||
session.jitsi_meet_tenant_mismatch = true;
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user