Compare commits

...

39 Commits

Author SHA1 Message Date
Calin-Teodor
5a38ba6257 chore(rn, versions): bump app and sdk versions 2024-03-12 14:06:11 +02:00
Calin-Teodor
36dfb4c956 chore(rn, versions): bump rnsdk and update dependencies 2024-03-12 14:03:47 +02:00
Calin-Teodor
29c33ed38b react-native-sdk(chore/deps): reset deps to 0 so we can update to latest 2024-03-12 14:03:25 +02:00
Calin-Teodor
c159a54fbf chore(rn, versions): bump rnsdk version 2024-03-12 14:03:25 +02:00
Calin-Teodor
107a5b845c chore(rn, versions): bump rnsdk version 2024-03-12 14:03:23 +02:00
Calin-Teodor
6953255375 chore(rn, versions): bump app and sdk versions 2024-03-12 14:02:28 +02:00
Saúl Ibarra Corretgé
d358dd8ec6 chore(deps) react-native-webrtc@118.0.3
Fixes spurious exceptions on Android 14.
2024-03-12 12:40:00 +01:00
Saúl Ibarra Corretgé
3d158fb2b4 fix(conference) fix incorrect meeting name in CallKit
Reset subject when setting a new room name.
2024-03-12 12:38:02 +01:00
Saúl Ibarra Corretgé
b7785a9f91 feat(recording) add ability to change recording defaults
If recordings.recordAudioAndVideo is set to false don't record
audio-video by default.
2024-03-11 21:39:23 +01:00
Дамян Минков
86d869a107 fix(visitors): Fixes replacing visitor domain. (#14457)
* fix(visitors): Fixes replacing visitor domain.

Constructing the visitor room jid instead of doing a risky replacement.

The problem is having a room like:
[meet-jit-si-shard]someroomname@conference.meet.jit.si and replacing with 'meet.jit.si', the dots match the -.
2024-03-08 13:08:01 -06:00
Jaya Allamsetty
1c81b93c1d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1789.0.0+a8f8666b...v1790.0.0+311766e3
2024-03-07 14:53:04 -05:00
Calin-Teodor
052070a6c1 chore(deps, i18next-http-backend): removed caret 2024-03-07 17:57:52 +02:00
Calin-Teodor
c531c0e65c react-native-sdk(chore/deps): reset link deps to 0 2024-03-07 16:49:15 +02:00
Calinteodor
e1055ebf9b react-native-sdk(chore/overrides): update prepare_sdk script to take care of overrides (#14449)
* react-native-sdk(chore/overrides): update prepare_sdk script to take care of overrides
2024-03-07 16:20:34 +02:00
Calin-Teodor
467023f77a react-native-sdk(chore/deps): reset deps to 0 so we can update to latest 2024-03-07 15:56:09 +02:00
Calin-Teodor
1249aa2dcb react-native-sdk(android): readded react native package 2024-03-07 14:52:16 +02:00
Calinteodor
0c45d87d1a react-native-sdk(android): screen share updates (#14440)
* react-native-sdk(android): removed related modules, services to screen-share feature and updated peerDeps
2024-03-06 17:33:08 +02:00
damencho
7140a90201 fix(visitors): Fixes demoting correct participant.
Moderators receive all demote messages so they can show notification if we need to.
2024-03-05 08:22:32 -06:00
Jaya Allamsetty
0a846606fc chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1786.0.0+0129be6c...v1789.0.0+a8f8666b
2024-03-05 09:02:32 -05:00
Avram Tudor
68dc111e3c fix: decouple local recording from needing a valid jwt (#14434) 2024-03-05 12:41:44 +02:00
Saúl Ibarra Corretgé
c81184df69 fix(ios) sync SDK and Lite SDK building commands 2024-03-05 12:09:03 +02:00
damencho
9b0747a0d9 feat(visitors): Demote a visitor for mobile. 2024-03-04 13:18:04 -06:00
damencho
c8cd80a8df feat(visitors): Checks for visitors support per room. 2024-03-04 13:18:04 -06:00
damencho
f1d4332668 feat(visitors): Adds an option to demote participants to visitors. 2024-03-04 13:18:04 -06:00
damencho
55b3256dc4 fix: Changes jwt error dialog to be sticky. 2024-03-04 13:18:04 -06:00
damencho
aa8bb55f3e feat(visitors): Drops not-used messages. 2024-03-04 13:18:04 -06:00
damencho
58b73e21de fix(visitors): Fixes missing import. 2024-03-04 13:18:04 -06:00
damencho
b1c955890a feat(visitors): Admit all function. 2024-03-04 13:18:04 -06:00
damencho
6ab945c2cb fix(visitors): Fixes wrong text in notification on multiple promote requests. 2024-03-04 13:18:04 -06:00
damencho
7291e1ef00 feat(lobby): Approve multiple participants. 2024-03-04 08:13:27 -06:00
Дамян Минков
43e075d48e feat: Rate limits update (#14429)
* feat: Introduces new rate limit setting.

No can have two different values per ip that is used to limit session creation and one that is used when that stanza rate limit is exceeded.

* feat: Introduces unthrottle logic.

* fix: Bumps default iq rate limits.

* feat: Prints how many times a session hits the rates.

* Update resources/prosody-plugins/mod_rate_limit.lua

Co-authored-by: Aaron van Meerten <aaron.van.meerten@8x8.com>

---------

Co-authored-by: Aaron van Meerten <aaron.van.meerten@8x8.com>
2024-03-01 13:23:04 -06:00
Jaya Allamsetty
885e1afdaa chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1784.0.0+639ad566...v1786.0.0+0129be6c
2024-02-29 23:34:47 -05:00
Hristo Terezov
e2ec4842a1 fix(toolbarButtons): filter visitor buttons in redux.
Filters the toolbarButtons in redux depending on the visitor state instead of filtering them every time in mapStateToProps. This will prevent unnecessary rerenders of the toolbar.

Additionally:
 - Moves visitor buttons const from features/config in features/toolbox.
 - Removes dublicate functions isButtonEnabled and isToolbarButtonEnabled.
 - Adds more buttons to the visitor allowed buttons which functionality has been any way accessible trough shortcuts or somewhere else.
 - Enables customButtons to be visible for visitors.
2024-02-29 18:51:47 -06:00
Hristo Terezov
ea075d9bae fix(toolbarButtons): Store all buttons in redux.
The previous version of getToolbarButtons function was actually adding the custom buttons on every call to the config toolbarButtons array, effectively creating dublicates of every custom button. The PR fixes this issue.

Also now we will be running the getToolbarButtons calculation only when needed.
2024-02-29 16:36:52 -06:00
qnafin
68f7448624 Update build.gradle / jsRootDir = file("../")
When newArchEnabled=true. Refers to a non-existent category
2024-02-29 19:58:14 +01:00
damencho
954ef6df4f fix: Drops inspect print. 2024-02-29 12:52:19 -06:00
Calinteodor
6a3c12b316 feat(android): fix screen sharing for android 14 (#14419)
* feat(android): media projection is now done through react native webrtc
2024-02-29 16:34:24 +02:00
Calin-Teodor
5be616a224 chore(deps, rn-webrtc): updated to 118.0.2 2024-02-29 15:56:20 +02:00
Mihaela Dumitru
58d8f3be12 chore(deps) update excalidraw version (#14420) 2024-02-29 15:30:44 +02:00
83 changed files with 2477 additions and 1914 deletions

3
.gitignore vendored
View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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"

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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");

View File

@@ -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.

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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! Whats 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
View File

@@ -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",

View File

@@ -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",

View File

@@ -133,7 +133,7 @@ dependencies {
if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/")
jsRootDir = file("../")
libraryName = "JitsiMeetReactNative"
codegenJavaPackageName = "org.jitsi.meet.sdk"
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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),

View File

@@ -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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
]
}
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -596,7 +596,10 @@ function _setRoom(state: IConferenceState, action: AnyAction) {
*/
return assign(state, {
error: undefined,
room
localSubject: undefined,
pendingSubjectChange: undefined,
room,
subject: undefined
});
}

View File

@@ -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;

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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';
/**

View File

@@ -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.

View File

@@ -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.
*

View File

@@ -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);

View File

@@ -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) : ''
};

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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 },

View File

@@ -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 },

View File

@@ -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));
};
}

View File

@@ -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();
}
}
);

View File

@@ -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
}

View File

@@ -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];

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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 ?? ''

View File

@@ -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,

View File

@@ -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.

View File

@@ -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.

View File

@@ -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),

View File

@@ -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'
];

View File

@@ -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);
}
/**

View File

@@ -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'

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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));

View File

@@ -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' />
);
}

View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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') } />
);
}

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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) } />);
}

View File

@@ -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',

View File

@@ -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';

View File

@@ -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.
*

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/visitors');

View File

@@ -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));

View File

@@ -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 || [];

View File

@@ -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

View File

@@ -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);

View File

@@ -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)

View File

@@ -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