Compare commits

...

57 Commits

Author SHA1 Message Date
damencho
dc98fc4839 feat(tests): Adds video layout test. 2025-02-14 12:00:49 -06:00
damencho
a815f97c7e feat(tests): Adds udp test. 2025-02-14 12:00:49 -06:00
damencho
8261cf2811 feat(tests): Adds tile view test. 2025-02-14 12:00:49 -06:00
damencho
f2238935b5 feat(tests): Adds switch video test. 2025-02-14 12:00:49 -06:00
damencho
5f12f76ada feat(tests): Adds subject test. 2025-02-14 12:00:49 -06:00
damencho
5a9464697f feat(tests): Adds stop video test. 2025-02-14 12:00:49 -06:00
damencho
f44601a82b feat(tests): Adds singlePort test. 2025-02-14 12:00:49 -06:00
damencho
3d3de4a884 feat(tests): Adds preJoin test. 2025-02-14 12:00:49 -06:00
damencho
c02ad56b6d feat(tests): Adds oneOnOne test. 2025-02-14 12:00:49 -06:00
damencho
ea7c5ccd58 fix(tests): Uses utility methods for mute/unmute. 2025-02-14 12:00:49 -06:00
Hristo Terezov
7ec3eae72b feat(test): Implement hangupAllParticipants 2025-02-14 11:07:00 -06:00
Hristo Terezov
edf7d18308 feat(tests): Print error on execute failure. 2025-02-14 11:07:00 -06:00
Hristo Terezov
6bf4a4e91d fix(tests): ensureTwoParticipants.
Now we are waiting for the second participant to join before starting waitForRemoteStreams.
2025-02-14 11:07:00 -06:00
damencho
5fd966f042 fix(tests): Adds mute test. 2025-02-13 14:40:28 -06:00
damencho
e275f20055 fix(tests): Moves muteAudio to ParticipantsPane. 2025-02-13 14:40:28 -06:00
damencho
ff624a34d8 feat(tests): Adds grant moderator test. 2025-02-13 14:40:28 -06:00
damencho
c98050224c feat(tests): Adds lock room with digits only test. 2025-02-13 14:40:28 -06:00
damencho
5bee373091 feat(tests): Adds lock room test. 2025-02-13 14:40:28 -06:00
Jaya Allamsetty
db4ab34ddf fix(tracks) Replace the tracks directly on camera toggle.
Fixes an issue where p2p peer stops rendering remote video when the mobile client toggles camera. This happens only when the peer starts video muted.
2025-02-13 11:37:22 -05:00
Calinteodor
ef138fb5aa feat(android/ios): start/stop recording events for native (#15598)
Added native android and ios events for start and stop recording.
2025-02-13 18:36:11 +02:00
Saúl Ibarra Corretgé
13bfdaed68 feat(external_api) facilitate gDM Electron
In order to use gDM in Electron the flow is somewhat reversed. It starts
from the Electron main process, so we need an API in the external_api
that can trigger the builtin picker. The picker is still necessary.
2025-02-13 12:12:19 +01:00
Saúl Ibarra Corretgé
ff656f4e6b fix(tracks) don't throw if creating a desktop track fails
There is nobody to catch it and we already show the error as a
notification.
2025-02-13 12:12:19 +01:00
Saúl Ibarra Corretgé
a27b78cef0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1910.0.0+31897f9b...v1912.0.0+522577a4
2025-02-13 11:15:19 +01:00
damencho
4fa426ace0 fix: Fixes wrong state in password dialog.
Fixes the following: Warning: A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components

Detected due to failure while moving locked room test.
2025-02-12 18:13:32 -06:00
Дамян Минков
ac34f524fa feat(tests): Small improvements to dial-in test. (#15600)
* feat(tests): Small improvements to dial-in test.

* squash: Fix lint.
2025-02-12 15:32:14 -06:00
Joshua Irmer
31a4f2a4ec fix(prejoin): do not show conference info in prejoin or lobby (#15591)
* do not show conference info in prejoin or lobby

Signed-off-by: Joshua Irmer <irmer@gonicus.de>

* fix typo

Signed-off-by: Joshua Irmer <irmer@gonicus.de>

---------

Signed-off-by: Joshua Irmer <irmer@gonicus.de>
2025-02-12 09:53:35 -06:00
damencho
dc908512f9 feat(prosody): Updates checks in presence_identity avoids setting missing user. 2025-02-11 13:51:16 -06:00
Hristo Terezov
ae983645d1 fix(tests): add more time for getNotificationText.
The lobby tests were failing.
2025-02-11 10:22:10 -06:00
Mihaela Dumitru
3514b22191 fix(recordings) dismiss notification when recording in progress (#15588) 2025-02-11 17:06:23 +02:00
Calinteodor
405af3af5f feat(toolbox/native): reorganizing buttons in the toolbox and overflow menu (#15543)
Configures what buttons can be visible inside Toolbox and OverflowMenu, based on priority and config overrides, just like web does.
2025-02-11 16:17:13 +02:00
Mihaela Dumitru
a6d333c07a fix(recordings) improve label to clearly reflect current status (#15570) 2025-02-10 15:39:00 +02:00
damencho
0387cdc888 feat(notifications): Make all error notifications sticky.
There are many cases where the error disappears and users easily miss the information.
2025-02-10 06:17:50 -06:00
Calinteodor
f670f39dd2 feat(android/ios): Native API events for show/hide notification (#15577)
Added show/hide notification events for native Android/iOS
2025-02-10 11:34:50 +02:00
damencho
7262465777 feat(prosody): Introduces events for json messages and transcripts.
Optimizes json parsing of incoming messages. Now we do it in centralized place and firing an event.
2025-02-07 22:10:26 -06:00
Calin-Teodor
75b4049529 feat(android): use fresco 3.2.0 in order to fix animation for gifs 2025-02-07 17:08:19 +02:00
Calin-Teodor
ac6185424c dep(react-native): update to 0.75.5 2025-02-07 16:57:12 +02:00
Hristo Terezov
9e15df8e3d fix(analytics): remove overwritesWatchRTC* props 2025-02-06 17:02:13 -06:00
Jaya Allamsetty
83f83c17eb chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1908.0.0+2a5d7fcc...v1910.0.0+31897f9b
2025-02-06 14:16:33 -05:00
Hristo Terezov
3e1adcd9b7 feat(tests): Add start muted test. 2025-02-04 15:39:36 -06:00
Saúl Ibarra Corretgé
8105127571 fix(rn) remove Pomise.allSettled polyfill
This one is already builtin.
2025-02-04 18:36:24 +01:00
Saúl Ibarra Corretgé
bc99a72984 Revert "fix(rn,polyfills) use core-js for promise polyfills"
This reverts commit e9a8fd5392.
2025-02-04 17:39:47 +01:00
Hristo Terezov
e10eaaa3d9 feat(package.json): Add test-ff-single script. 2025-02-04 08:58:37 -06:00
Hristo Terezov
0e831074c0 fix(av-moderation-test): random timing failures. 2025-02-04 08:18:30 -06:00
Hristo Terezov
326b694bf2 fix(tests): wdio.cong ffExcludes undefined error. 2025-02-04 08:17:25 -06:00
Calin-Teodor
9e1f3de4e5 feat(base/devices): removed unused helper 2025-02-04 14:56:47 +02:00
Saúl Ibarra Corretgé
07a25a1f00 feat(ios) add ability to configure the native WebRTC logging level 2025-02-04 10:37:43 +01:00
Saúl Ibarra Corretgé
d6bbe07cf2 feat(ios) add ability to inject a custom RTCAudioDevice implementation
It allows for full control over the audio handling.
2025-02-04 10:37:43 +01:00
Saúl Ibarra Corretgé
e9a8fd5392 fix(rn,polyfills) use core-js for promise polyfills
We use the same on the web, on browsers that don't support them.
2025-02-04 10:35:49 +01:00
damencho
aea9c5e79e fix: Fixes is_jibri check. 2025-02-03 15:47:47 -06:00
Saúl Ibarra Corretgé
b60210d0ad feat(analytics) drop defunct Google Analytics integration
We haven't used in years. Those who want to use it can still create
their own custom script and include it, since it wasn't even included by
default.
2025-02-03 22:44:12 +01:00
Saúl Ibarra Corretgé
f0d2106c1a fix(build) apply @babel/preset-env also to TS files
Without it, we cannot detect what features to polyfill.

Some bundles have seen a size increase, this is due to necessary
polyfills now being included as usage was detected.
2025-02-03 20:51:15 +01:00
Saúl Ibarra Corretgé
13f1cb13c5 fix(ts) drop bogus method anotations 2025-02-03 20:51:15 +01:00
Saúl Ibarra Corretgé
c27ca779ab feat(build) use core-js to polyfill modern JavaScript features
This should prevent us accidentally breaking compatibility with older
browsers because polyfilling happens automatically based on usage
detection.
2025-02-03 20:51:15 +01:00
Saúl Ibarra Corretgé
aedb43ec5b feat(build) drop export-default-from plugin
The proposal never passed stage 1 and was last updated 4 years ago,
which signals it won't make it into the language: https://github.com/tc39/proposal-export-default-from

The alternative is just a couple of characters longer.
2025-02-03 20:51:15 +01:00
Saúl Ibarra Corretgé
0a68eed294 fix(build) don't use babel-loader on node_modules
Libraries should already be in a consumable state. Note how I bumped
rnnoise-wasm to fix an issue with non-standard import syntax.
2025-02-03 20:51:15 +01:00
Saúl Ibarra Corretgé
3f51b10245 fix(ts) set ES2024 as our target for web
We depend on ES2024 features. For environments without full support,
webpack will add polyfills.
2025-02-03 20:51:15 +01:00
Calin-Teodor
5260cd7e30 feat(android/sdk): custom button pressed event name updated 2025-02-03 18:27:26 +02:00
132 changed files with 3636 additions and 1548 deletions

View File

@@ -48,8 +48,6 @@ deploy-appbundle:
$(BUILD_DIR)/external_api.min.js.map \
$(BUILD_DIR)/alwaysontop.min.js \
$(BUILD_DIR)/alwaysontop.min.js.map \
$(BUILD_DIR)/analytics-ga.min.js \
$(BUILD_DIR)/analytics-ga.min.js.map \
$(BUILD_DIR)/face-landmarks-worker.min.js \
$(BUILD_DIR)/face-landmarks-worker.min.js.map \
$(BUILD_DIR)/noise-suppressor-worklet.min.js \

View File

@@ -42,7 +42,7 @@ ext {
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
//React Native and Hermes Version
rnVersion = "0.75.4"
rnVersion = "0.75.5"
}
allprojects {
@@ -69,10 +69,29 @@ allprojects {
}
}
// Due to a dependency conflict between React Native and the Fresco library used by GiphySDK,
// GIFs appear as static images instead of animating
// https://github.com/Giphy/giphy-react-native-sdk/commit/7fe466ed6fddfaec95f9cbc959d33bd75ad8f900
configurations.configureEach {
resolutionStrategy {
forcedModules = [
'com.facebook.fresco:fresco:3.2.0',
'com.facebook.fresco:animated-gif:3.2.0',
'com.facebook.fresco:animated-base:3.2.0',
'com.facebook.fresco:animated-drawable:3.2.0',
'com.facebook.fresco:animated-webp:3.2.0',
'com.facebook.fresco:webpsupport:3.2.0',
'com.facebook.fresco:imagepipeline-okhttp3:3.2.0',
'com.facebook.fresco:middleware:3.2.0',
'com.facebook.fresco:nativeimagetranscoder:3.2.0'
]
}
}
// Third-party react-native modules which Jitsi Meet SDK for Android depends
// on and which are not available in third-party Maven repositories need to
// be deployed in a Maven repository of ours.
//
if (project.name.startsWith('react-native-')) {
apply plugin: 'maven-publish'

View File

@@ -78,7 +78,11 @@ public class BroadcastAction {
SEND_CHAT_MESSAGE("org.jitsi.meet.SEND_CHAT_MESSAGE"),
SET_VIDEO_MUTED("org.jitsi.meet.SET_VIDEO_MUTED"),
SET_CLOSED_CAPTIONS_ENABLED("org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED"),
TOGGLE_CAMERA("org.jitsi.meet.TOGGLE_CAMERA");
TOGGLE_CAMERA("org.jitsi.meet.TOGGLE_CAMERA"),
SHOW_NOTIFICATION("org.jitsi.meet.SHOW_NOTIFICATION"),
HIDE_NOTIFICATION("org.jitsi.meet.HIDE_NOTIFICATION"),
START_RECORDING("org.jitsi.meet.START_RECORDING"),
STOP_RECORDING("org.jitsi.meet.STOP_RECORDING");
private final String action;

View File

@@ -91,7 +91,7 @@ public class BroadcastEvent {
VIDEO_MUTED_CHANGED("org.jitsi.meet.VIDEO_MUTED_CHANGED"),
READY_TO_CLOSE("org.jitsi.meet.READY_TO_CLOSE"),
TRANSCRIPTION_CHUNK_RECEIVED("org.jitsi.meet.TRANSCRIPTION_CHUNK_RECEIVED"),
CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED("org.jitsi.meet.CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED");
CUSTOM_BUTTON_PRESSED("org.jitsi.meet.CUSTOM_BUTTON_PRESSED");
private static final String CONFERENCE_BLURRED_NAME = "CONFERENCE_BLURRED";
private static final String CONFERENCE_FOCUSED_NAME = "CONFERENCE_FOCUSED";
@@ -109,7 +109,7 @@ public class BroadcastEvent {
private static final String VIDEO_MUTED_CHANGED_NAME = "VIDEO_MUTED_CHANGED";
private static final String READY_TO_CLOSE_NAME = "READY_TO_CLOSE";
private static final String TRANSCRIPTION_CHUNK_RECEIVED_NAME = "TRANSCRIPTION_CHUNK_RECEIVED";
private static final String CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED_NAME = "CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED";
private static final String CUSTOM_BUTTON_PRESSED_NAME = "CUSTOM_BUTTON_PRESSED";
private final String action;
@@ -164,8 +164,8 @@ public class BroadcastEvent {
return READY_TO_CLOSE;
case TRANSCRIPTION_CHUNK_RECEIVED_NAME:
return TRANSCRIPTION_CHUNK_RECEIVED;
case CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED_NAME:
return CUSTOM_OVERFLOW_MENU_BUTTON_PRESSED;
case CUSTOM_BUTTON_PRESSED_NAME:
return CUSTOM_BUTTON_PRESSED;
}
return null;

View File

@@ -1,11 +1,13 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.os.Bundle;
public class BroadcastIntentHelper {
public static Intent buildSetAudioMutedIntent(boolean muted) {
Intent intent = new Intent(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
intent.putExtra("muted", muted);
return intent;
}
@@ -17,18 +19,21 @@ public class BroadcastIntentHelper {
Intent intent = new Intent(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
intent.putExtra("to", to);
intent.putExtra("message", message);
return intent;
}
public static Intent buildToggleScreenShareIntent(boolean enabled) {
Intent intent = new Intent(BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
intent.putExtra("enabled", enabled);
return intent;
}
public static Intent buildOpenChatIntent(String participantId) {
Intent intent = new Intent(BroadcastAction.Type.OPEN_CHAT.getAction());
intent.putExtra("to", participantId);
return intent;
}
@@ -40,28 +45,98 @@ public class BroadcastIntentHelper {
Intent intent = new Intent(BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
intent.putExtra("to", participantId);
intent.putExtra("message", message);
return intent;
}
public static Intent buildSetVideoMutedIntent(boolean muted) {
Intent intent = new Intent(BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
intent.putExtra("muted", muted);
return intent;
}
public static Intent buildSetClosedCaptionsEnabledIntent(boolean enabled) {
Intent intent = new Intent(BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
intent.putExtra("enabled", enabled);
return intent;
}
public static Intent buildRetrieveParticipantsInfo(String requestId) {
Intent intent = new Intent(BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction());
intent.putExtra("requestId", requestId);
return intent;
}
public static Intent buildToggleCameraIntent() {
return new Intent(BroadcastAction.Type.TOGGLE_CAMERA.getAction());
}
public static Intent buildShowNotificationIntent(
String appearance, String description, String timeout, String title, String uid) {
Intent intent = new Intent(BroadcastAction.Type.SHOW_NOTIFICATION.getAction());
intent.putExtra("appearance", appearance);
intent.putExtra("description", description);
intent.putExtra("timeout", timeout);
intent.putExtra("title", title);
intent.putExtra("uid", uid);
return intent;
}
public static Intent buildHideNotificationIntent(String uid) {
Intent intent = new Intent(BroadcastAction.Type.HIDE_NOTIFICATION.getAction());
intent.putExtra("uid", uid);
return intent;
}
public enum RecordingMode {
FILE("file"),
STREAM("stream");
private final String mode;
RecordingMode(String mode) {
this.mode = mode;
}
public String getMode() {
return mode;
}
}
public static Intent buildStartRecordingIntent(
RecordingMode mode,
String dropboxToken,
boolean shouldShare,
String rtmpStreamKey,
String rtmpBroadcastID,
String youtubeStreamKey,
String youtubeBroadcastID,
Bundle extraMetadata,
boolean transcription) {
Intent intent = new Intent(BroadcastAction.Type.START_RECORDING.getAction());
intent.putExtra("mode", mode.getMode());
intent.putExtra("dropboxToken", dropboxToken);
intent.putExtra("shouldShare", shouldShare);
intent.putExtra("rtmpStreamKey", rtmpStreamKey);
intent.putExtra("rtmpBroadcastID", rtmpBroadcastID);
intent.putExtra("youtubeStreamKey", youtubeStreamKey);
intent.putExtra("youtubeBroadcastID", youtubeBroadcastID);
intent.putExtra("extraMetadata", extraMetadata);
intent.putExtra("transcription", transcription);
return intent;
}
public static Intent buildStopRecordingIntent(RecordingMode mode, boolean transcription) {
Intent intent = new Intent(BroadcastAction.Type.STOP_RECORDING.getAction());
intent.putExtra("mode", mode.getMode());
intent.putExtra("transcription", transcription);
return intent;
}
}

View File

@@ -97,6 +97,10 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
constants.put("SET_VIDEO_MUTED", BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
constants.put("SET_CLOSED_CAPTIONS_ENABLED", BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
constants.put("TOGGLE_CAMERA", BroadcastAction.Type.TOGGLE_CAMERA.getAction());
constants.put("SHOW_NOTIFICATION", BroadcastAction.Type.SHOW_NOTIFICATION.getAction());
constants.put("HIDE_NOTIFICATION", BroadcastAction.Type.HIDE_NOTIFICATION.getAction());
constants.put("START_RECORDING", BroadcastAction.Type.START_RECORDING.getAction());
constants.put("STOP_RECORDING", BroadcastAction.Type.STOP_RECORDING.getAction());
return constants;
}

4
app.js
View File

@@ -1,9 +1,5 @@
/* Jitsi Meet app main entrypoint. */
// Polyfill Promise.withReolvers.
// FIXME(saghul) webpack + core-js v3 should polyfill this.
import 'promise.withresolvers/auto';
// Re-export jQuery
// FIXME: Remove this requirement from torture tests.
import $ from 'jquery';

View File

@@ -172,7 +172,9 @@ let room;
/*
* Logic to open a desktop picker put on the window global for
* lib-jitsi-meet to detect and invoke
* lib-jitsi-meet to detect and invoke.
*
* TODO: remove once the Electron SDK supporting gDM has been out for a while.
*/
window.JitsiMeetScreenObtainer = {
openDesktopPicker(options, onSourceChoose) {
@@ -287,7 +289,7 @@ class ConferenceConnector {
},
descriptionKey: 'dialog.reservationErrorMsg',
titleKey: 'dialog.reservationError'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
break;
}
@@ -295,7 +297,7 @@ class ConferenceConnector {
APP.store.dispatch(showErrorNotification({
descriptionKey: 'dialog.gracefulShutdown',
titleKey: 'dialog.serviceUnavailable'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
break;
// FIXME FOCUS_DISCONNECTED is a confusing event name.

View File

@@ -1091,9 +1091,6 @@ var config = {
// True if the analytics should be disabled
// disabled: false,
// The Google Analytics Tracking ID:
// googleAnalyticsTrackingId: 'your-tracking-id-UA-123456-1',
// Matomo configuration:
// matomoEndpoint: 'https://your-matomo-endpoint/',
// matomoSiteID: '42',
@@ -1131,7 +1128,6 @@ var config = {
// Array of script URLs to load as lib-jitsi-meet "analytics handlers".
// scriptURLs: [
// "libs/analytics-ga.min.js", // google-analytics
// "https://example.com/my-custom-analytics.js",
// ],

View File

@@ -78,8 +78,6 @@ target 'JitsiMeetSDKLite' do
end
post_install do |installer|
PLIST_BUDDY_PATH = '/usr/libexec/PlistBuddy'
react_native_post_install(
installer,
use_native_modules![:reactNativePath],
@@ -98,23 +96,5 @@ post_install do |installer|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.1'
config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -no-verify-emitted-module-interface'
end
# Can be removed when updated to RN 0.76
# Issue https://github.com/facebook/react-native/issues/35863#issuecomment-1387465588
if target.name == "hermes-engine"
installer.pods_project.files.each do |fileref|
if fileref.path.end_with? "hermes.xcframework"
hermes_plist_file = "#{fileref.real_path}/Info.plist"
# Patch Hermes to remove the debug symbols entry from the Info.plist (as it's not shipped with it)
# This might be removed once Hermes starts to ship with Debug symbols or we remove our
# direct dependency from the Main iOS target on "hermes.xcframework"
Open3.capture3(PLIST_BUDDY_PATH, '-c', 'Delete :AvailableLibraries:0:DebugSymbolsPath', hermes_plist_file)
Open3.capture3(PLIST_BUDDY_PATH, '-c', 'Delete :AvailableLibraries:1:DebugSymbolsPath', hermes_plist_file)
Open3.capture3(PLIST_BUDDY_PATH, '-c', 'Delete :AvailableLibraries:2:DebugSymbolsPath', hermes_plist_file)
Open3.capture3(PLIST_BUDDY_PATH, '-c', 'Delete :AvailableLibraries:3:DebugSymbolsPath', hermes_plist_file)
Open3.capture3(PLIST_BUDDY_PATH, '-c', 'Delete :AvailableLibraries:4:DebugSymbolsPath', hermes_plist_file)
end
end
end
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,10 @@
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
JitsiMeet *jitsiMeet = [JitsiMeet sharedInstance];
#if 0
jitsiMeet.webRtcLoggingSeverity = WebRTCLoggingSeverityVerbose;
#endif
jitsiMeet.conferenceActivityType = JitsiMeetConferenceActivityType;
jitsiMeet.customUrlScheme = @"org.jitsi.meet";
jitsiMeet.universalLinkDomains = @[@"meet.jit.si", @"alpha.jitsi.net", @"beta.meet.jit.si"];
@@ -36,7 +40,7 @@
jitsiMeet.defaultConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
// For testing configOverrides a room needs to be set
// builder.room = @"test0988test";
// builder.room = @"https://meet.jit.si/test0988test";
[builder setFeatureFlag:@"welcomepage.enabled" withBoolean:YES];
[builder setFeatureFlag:@"ios.screensharing.enabled" withBoolean:YES];

View File

@@ -18,6 +18,11 @@
static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
typedef NS_ENUM(NSInteger, RecordingMode) {
RecordingModeFile,
RecordingModeStream
};
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
- (void)sendHangUp;
@@ -27,9 +32,13 @@ static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completion;
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
- (void)sendChatMessage:(NSString*)message :(NSString*)to;
- (void)sendSetVideoMuted:(BOOL)muted;
- (void)sendSetClosedCaptionsEnabled:(BOOL)enabled;
- (void)toggleCamera;
- (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid;
- (void)hideNotification:(NSString*)uid;
- (void)startRecording:(RecordingMode)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription;
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription;
@end

View File

@@ -28,6 +28,10 @@ static NSString * const sendChatMessageAction = @"org.jitsi.meet.SEND_CHAT_MESSA
static NSString * const setVideoMutedAction = @"org.jitsi.meet.SET_VIDEO_MUTED";
static NSString * const setClosedCaptionsEnabledAction = @"org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED";
static NSString * const toggleCameraAction = @"org.jitsi.meet.TOGGLE_CAMERA";
static NSString * const showNotificationAction = @"org.jitsi.meet.SHOW_NOTIFICATION";
static NSString * const hideNotificationAction = @"org.jitsi.meet.HIDE_NOTIFICATION";
static NSString * const startRecordingAction = @"org.jitsi.meet.START_RECORDING";
static NSString * const stopRecordingAction = @"org.jitsi.meet.STOP_RECORDING";
@implementation ExternalAPI
@@ -52,7 +56,11 @@ RCT_EXPORT_MODULE();
@"SEND_CHAT_MESSAGE": sendChatMessageAction,
@"SET_VIDEO_MUTED" : setVideoMutedAction,
@"SET_CLOSED_CAPTIONS_ENABLED": setClosedCaptionsEnabledAction,
@"TOGGLE_CAMERA": toggleCameraAction
@"TOGGLE_CAMERA": toggleCameraAction,
@"SHOW_NOTIFICATION": showNotificationAction,
@"HIDE_NOTIFICATION": hideNotificationAction,
@"START_RECORDING": startRecordingAction,
@"STOP_RECORDING": stopRecordingAction
};
};
@@ -78,7 +86,11 @@ RCT_EXPORT_MODULE();
sendChatMessageAction,
setVideoMutedAction,
setClosedCaptionsEnabledAction,
toggleCameraAction
toggleCameraAction,
showNotificationAction,
hideNotificationAction,
startRecordingAction,
stopRecordingAction
];
}
@@ -180,4 +192,59 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:toggleCameraAction body:nil];
}
- (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
data[@"appearance"] = appearance;
data[@"description"] = description;
data[@"timeout"] = timeout;
data[@"title"] = title;
data[@"uid"] = uid;
[self sendEventWithName:showNotificationAction body:data];
}
- (void)hideNotification:(NSString*)uid {
NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
data[@"uid"] = uid;
[self sendEventWithName:hideNotificationAction body:data];
}
static inline NSString *RecordingModeToString(RecordingMode mode) {
switch (mode) {
case RecordingModeFile:
return @"file";
case RecordingModeStream:
return @"stream";
default:
return nil;
}
}
- (void)startRecording:(RecordingMode)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription {
NSString *modeString = RecordingModeToString(mode);
NSDictionary *data = @{
@"mode": modeString,
@"dropboxToken": dropboxToken,
@"shouldShare": @(shouldShare),
@"rtmpStreamKey": rtmpStreamKey,
@"rtmpBroadcastID": rtmpBroadcastID,
@"youtubeStreamKey": youtubeStreamKey,
@"youtubeBroadcastID": youtubeBroadcastID,
@"extraMetadata": extraMetadata,
@"transcription": @(transcription)
};
[self sendEventWithName:startRecordingAction body:data];
}
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
NSString *modeString = RecordingModeToString(mode);
NSDictionary *data = @{
@"mode": modeString,
@"transcription": @(transcription)
};
[self sendEventWithName:stopRecordingAction body:data];
}
@end

View File

@@ -19,6 +19,15 @@
#import <JitsiMeetSDK/JitsiMeetConferenceOptions.h>
// Matches RTCLoggingSeverity from RTCLogging.h
typedef NS_ENUM(NSInteger, WebRTCLoggingSeverity) {
WebRTCLoggingSeverityVerbose,
WebRTCLoggingSeverityInfo,
WebRTCLoggingSeverityWarning,
WebRTCLoggingSeverityError,
WebRTCLoggingSeverityNone,
};
@interface JitsiMeet : NSObject
/**
@@ -26,20 +35,35 @@
* SiriKit or Handoff, for example.
*/
@property (copy, nonatomic, nullable) NSString *conferenceActivityType;
/**
* Custom URL scheme used for deep-linking.
*/
@property (copy, nonatomic, nullable) NSString *customUrlScheme;
/**
* List of domains used for universal linking.
*/
@property (copy, nonatomic, nullable) NSArray<NSString *> *universalLinkDomains;
/**
* Default conference options used for all conferences. These options will be merged
* with those passed to JitsiMeetView.join when joining a conference.
*/
@property (nonatomic, nullable) JitsiMeetConferenceOptions *defaultConferenceOptions;
/**
* Custom RTCAudioDevice implementation.
* https://github.com/jitsi/webrtc/blob/M124/sdk/objc/components/audio/RTCAudioDevice.h
* https://github.com/mstyura/RTCAudioDevice
*/
@property (nonatomic, strong, nullable) id rtcAudioDevice;
/**
* Specify WebRTC logging severity.
*/
@property (nonatomic, assign) WebRTCLoggingSeverity webRtcLoggingSeverity;
#pragma mark - This class is a singleton
+ (instancetype _Nonnull)sharedInstance;

View File

@@ -54,14 +54,9 @@
- (instancetype)init {
if (self = [super init]) {
#if 0
// Initialize WebRTC options.
WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
options.loggingSeverity = RTCLoggingSeverityInfo;
#endif
// Initialize the one and only bridge for interfacing with React Native.
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
self.rtcAudioDevice = nil;
self.webRtcLoggingSeverity = WebRTCLoggingSeverityNone;
// Initialize the listener for handling start/stop screensharing notifications.
_screenshareEventEmiter = [[ScheenshareEventEmiter alloc] init];
@@ -142,6 +137,12 @@
return;
};
// Initialize WebRTC options.
WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
options.audioDevice = _rtcAudioDevice;
options.loggingSeverity = (RTCLoggingSeverity)_webRtcLoggingSeverity;
// Initialize the one and only bridge for interfacing with React Native.
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
}
@@ -249,6 +250,8 @@
}
- (RCTBridge *)getReactBridge {
// Initialize bridge lazily.
[self instantiateReactNativeBridge];
return _bridgeWrapper.bridge;
}

View File

@@ -21,6 +21,8 @@
#import "JitsiMeetConferenceOptions.h"
#import "JitsiMeetViewDelegate.h"
typedef NS_ENUM(NSInteger, RecordingMode);
@interface JitsiMeetView : UIView
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@@ -47,5 +49,9 @@
- (void)setVideoMuted:(BOOL)muted;
- (void)setClosedCaptionsEnabled:(BOOL)enabled;
- (void)toggleCamera;
- (void)showNotification:(NSString * _Nonnull)appearance :(NSString * _Nullable)description :(NSString * _Nullable)timeout :(NSString * _Nullable)title :(NSString * _Nullable)uid;
- (void)hideNotification:(NSString * _Nullable)uid;
- (void)startRecording:(RecordingMode)mode :(NSString * _Nullable)dropboxToken :(BOOL)shouldShare :(NSString * _Nullable)rtmpStreamKey :(NSString * _Nullable)rtmpBroadcastID :(NSString * _Nullable)youtubeStreamKey :(NSString * _Nullable)youtubeBroadcastID :(NSString * _Nullable)extraMetadata :(BOOL)transcription;
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription;
@end

View File

@@ -143,6 +143,26 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
[externalAPI toggleCamera];
}
- (void)showNotification:(NSString *)appearance :(NSString *)description :(NSString *)timeout :(NSString *)title :(NSString *)uid {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI showNotification:appearance :description :timeout :title :uid];
}
-(void)hideNotification:(NSString *)uid {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI hideNotification:uid];
}
- (void)startRecording:(RecordingMode)mode :(NSString *)dropboxToken :(BOOL)shouldShare :(NSString *)rtmpStreamKey :(NSString *)rtmpBroadcastID :(NSString *)youtubeStreamKey :(NSString *)youtubeBroadcastID :(NSString *)extraMetadata :(BOOL)transcription {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI startRecording:mode :dropboxToken :shouldShare :rtmpStreamKey :rtmpBroadcastID :youtubeStreamKey :youtubeBroadcastID :extraMetadata :transcription];
}
- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI stopRecording:mode :transcription];
}
#pragma mark Private methods
- (void)registerObservers {

View File

@@ -1405,7 +1405,8 @@
"ccButtonTooltip": "Start / Stop subtitles",
"expandedLabel": "Transcribing is currently on",
"failed": "Transcribing failed",
"labelToolTip": "The meeting is being transcribed",
"labelTooltip": "This meeting is being transcribed.",
"labelTooltipExtra": "In addition, a transcript will be available later.",
"sourceLanguageDesc": "Currently the meeting language is set to <b>{{sourceLanguage}}</b>. <br/> You can change it from ",
"sourceLanguageHere": "here",
"start": "Start showing subtitles",
@@ -1444,7 +1445,7 @@
"ldTooltip": "Viewing low definition video",
"lowDefinition": "Low definition",
"performanceSettings": "Performance settings",
"recording": "Recording in progress",
"recording": "This meeting is being recorded.",
"sd": "SD",
"sdTooltip": "Viewing standard definition video",
"standardDefinition": "Standard definition",

View File

@@ -75,6 +75,7 @@ import {
toggleChat
} from '../../react/features/chat/actions';
import { openChat } from '../../react/features/chat/actions.web';
import { showDesktopPicker } from '../../react/features/desktop-picker/actions';
import {
processExternalDeviceRequest
} from '../../react/features/device-selection/functions';
@@ -1048,6 +1049,23 @@ function initCommands() {
}
case '_new_electron_screensharing_supported': {
callback(true);
break;
}
case 'open-desktop-picker': {
const { desktopSharingSources } = APP.store.getState()['features/base/config'];
const options = {
desktopSharingSources: desktopSharingSources ?? [ 'screen', 'window' ]
};
const onSourceChoose = (_streamId, _type, screenShareAudio, source) => {
callback({
screenShareAudio,
source
});
};
dispatch(showDesktopPicker(options, onSourceChoose));
break;
}
default:

View File

@@ -1243,7 +1243,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* @returns {Promise}
*
* TODO: should be removed after we make sure that all Electron clients use only versions
* after with the legacy SS suport was removed from the electron SDK. If we remove it now the SS for Electron
* after with the legacy SS support was removed from the electron SDK. If we remove it now the SS for Electron
* clients with older versions wont work.
*/
_isNewElectronScreensharingSupported() {
@@ -1455,4 +1455,15 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
setVirtualBackground(enabled, backgroundImage) {
this.executeCommand('setVirtualBackground', enabled, backgroundImage);
}
/**
* Opens the desktop picker. This is invoked by the Electron SDK when gDM is used.
*
* @returns {Promise}
*/
_openDesktopPicker() {
return this._transport.sendRequest({
name: 'open-desktop-picker'
});
}
}

View File

@@ -1,2 +1,2 @@
export default from './API';
export { default } from './API';
export * from './constants';

416
package-lock.json generated
View File

@@ -20,7 +20,7 @@
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
"@jitsi/rnnoise-wasm": "0.2.0",
"@jitsi/rnnoise-wasm": "0.2.1",
"@jitsi/rtcstats": "9.5.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
@@ -62,14 +62,13 @@
"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/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
"pixelmatch": "5.3.0",
"promise.allsettled": "1.0.4",
"promise.withresolvers": "1.0.3",
"punycode": "2.3.0",
"react": "18.2.0",
@@ -78,7 +77,7 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.75.4",
"react-native": "0.75.5",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-default-preference": "1.4.4",
@@ -125,12 +124,11 @@
"devDependencies": {
"@babel/core": "7.25.9",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-transform-private-methods": "7.25.9",
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "5.0.9",
"@react-native/metro-config": "0.75.4",
"@react-native/metro-config": "0.75.5",
"@stylistic/eslint-plugin": "2.12.1",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
@@ -169,6 +167,7 @@
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
"clean-css-cli": "4.3.0",
"core-js": "3.40.0",
"css-loader": "6.8.1",
"eslint": "8.57.0",
"eslint-plugin-import": "2.31.0",
@@ -4199,9 +4198,9 @@
"integrity": "sha512-L/gnRjnFxpQqvfILXeFoVTHEtwL01zoSKAIqMVxMmaD0ahozML8uztlxRPlYSbKrT31j3Cm1ss9KdSNeO3f9FQ=="
},
"node_modules/@jitsi/rnnoise-wasm": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@jitsi/rnnoise-wasm/-/rnnoise-wasm-0.2.0.tgz",
"integrity": "sha512-ZpNws6UjbNjkhD68DQu862IjzfjYgLOwknqdlLotPbJqnBTA91vo5uh9+D0aUqSoY/U1aQhPZZB3IRj5ZNfw3Q=="
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jitsi/rnnoise-wasm/-/rnnoise-wasm-0.2.1.tgz",
"integrity": "sha512-iEj77www43pS2Yq+cfLZb+hFuI7L5ccisBzzPMcOjjLsG4/LAlkD1CY58/8gc84nHdLBGmD/OPIWGnvYnXvB0A=="
},
"node_modules/@jitsi/rtcstats": {
"version": "9.5.1",
@@ -5960,30 +5959,30 @@
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.4.tgz",
"integrity": "sha512-WX6/LNHwyjislSFM+h3qQjBiPaXXPJW5ZV4TdgNKb6QOPO0g1KGYRQj44cI2xSpZ3fcWrvQFZfQgSMbVK9Sg7A==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.5.tgz",
"integrity": "sha512-5Arp90qqwT4svF9izcFxM2tiDZlHSVKBuLT6gS1FbOABzHkKDK06Pv+CWNKZ2TM0l7qxRxNAd6e86tvnkMlGZw==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-plugin-codegen": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.4.tgz",
"integrity": "sha512-gu5ZRIdr7+ufi09DJROhfDtbF4biTnCDJqtqcmtsku4cXOXPHE36QbC/vAmKEZ0PMPURBI8lwF2wfaeHLn7gig==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.5.tgz",
"integrity": "sha512-WUYzABcKhjCYCFsMTkvJ4CPSSeQ0ayLoIQjN87NtgrKzRq6yISTxF2J/2vgUuJgT99U7J6x6AgdNqlQ/U6M+qw==",
"license": "MIT",
"dependencies": {
"@react-native/codegen": "0.75.4"
"@react-native/codegen": "0.75.5"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-preset": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.4.tgz",
"integrity": "sha512-UtyYCDJ3rZIeggyFEfh/q5t/FZ5a1h9F8EI37Nbrwyk/OKPH+1XS4PbHROHJzBARlJwOAfmT75+ovYUO0eakJA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.5.tgz",
"integrity": "sha512-VEJvWUu2zg9PacO6RL3n10TQ05IdbTIKiCpRAulexSAZZamURelX8Ko9VbWpIP0Wg/zFyYoDwsKOhCe+rFfhvA==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.0",
@@ -6028,7 +6027,7 @@
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"@react-native/babel-plugin-codegen": "0.75.4",
"@react-native/babel-plugin-codegen": "0.75.5",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
},
@@ -6049,9 +6048,9 @@
}
},
"node_modules/@react-native/codegen": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.4.tgz",
"integrity": "sha512-0FplNAD/S5FUvm8YIn6uyarOcP4jdJPqWz17K4a/Gp2KSsG/JJKEskX3aj5wpePzVfNQl3WyvBJ0whODdCocIA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.5.tgz",
"integrity": "sha512-xIhr7UpnUySYJGgQmLrTPtTcaDlri7YECkAJwcLAxdDu2KVJzGoxNdHa2poCkTLceDBxvL+3hYYsoevl1PD45w==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.0",
@@ -6086,15 +6085,15 @@
}
},
"node_modules/@react-native/community-cli-plugin": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.4.tgz",
"integrity": "sha512-k/hevYPjEpW0MNVVyb3v9PJosOP+FzenS7+oqYNLXdEmgTnGHrAtYX9ABrJJgzeJt7I6g8g+RDvm8PSE+tnM5w==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.5.tgz",
"integrity": "sha512-rtZqsTb+2gtPw8BZgJlZ7p5ycXSGu4uql1gxybcv9qNgqlsNBZofiIdZEAade67/GfeQEvy/UY96SvoIsmVW6A==",
"license": "MIT",
"dependencies": {
"@react-native-community/cli-server-api": "14.1.0",
"@react-native-community/cli-tools": "14.1.0",
"@react-native/dev-middleware": "0.75.4",
"@react-native/metro-babel-transformer": "0.75.4",
"@react-native/dev-middleware": "0.75.5",
"@react-native/metro-babel-transformer": "0.75.5",
"chalk": "^4.0.0",
"execa": "^5.1.1",
"metro": "^0.80.3",
@@ -6178,22 +6177,22 @@
}
},
"node_modules/@react-native/debugger-frontend": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.4.tgz",
"integrity": "sha512-QfGurR5hV6bhMPn/6VxS2RomYrPRFGwA03jJr+zKyWHnxDAu5jOqYVyKAktIIbhYe5sPp78QVl1ZYuhcnsRbEw==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.5.tgz",
"integrity": "sha512-/mW0ELovNVAdZZFs0JC9147zF/GlvgrFPHhLhkgT95lOir/8cACLvR1kdVnH59sAQw2zKxyrFbrwhO80zPfp9g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/dev-middleware": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.4.tgz",
"integrity": "sha512-UhyBeQOG2wNcvrUGw3+IBrHBk/lIu7hHGmWt4j8W9Aqv9BwktHKkPyko+5A1yoUeO1O/VDnHWYqWeOejcA9wpQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.5.tgz",
"integrity": "sha512-agVYocKP6YjW2g296jS1fipvC4U1vaykPnIgIgQfXhhVAYsFFJwO4Fh8oLcI82ceGc3mikv9cPrv8IrUoIWKsg==",
"license": "MIT",
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.75.4",
"@react-native/debugger-frontend": "0.75.5",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@@ -6225,31 +6224,31 @@
"license": "MIT"
},
"node_modules/@react-native/gradle-plugin": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.4.tgz",
"integrity": "sha512-kKTmw7cF7p1raT30DC0L6N+xiVXN7dlRy0J+hYPiCRRVHplwgvyS7pszjxfzwXmHFqOxwpxQVI3du8opsma1Mg==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.5.tgz",
"integrity": "sha512-s6lPOVPLoGdQNbfgkVHvCC5Mg/mdTkg+5l5gChG6tNI4LZ6NVvaN0Ettt8bIKMoYlExsXQ2zpdBLopnjro5JgA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/js-polyfills": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.4.tgz",
"integrity": "sha512-NF5ID5FjcVHBYk1LQ4JMRjPmxBWEo4yoqW1m6vGOQZPT8D5Qs9afgx3f7gQatxbn3ivMh0FVbLW0zBx6LyxEzA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.5.tgz",
"integrity": "sha512-zDMMq2mtWZ1/P3CnOguBucMSsvzPSDuLHZPn33DjZv1VFSOjqyAbGy9F7ZEQ0Y+vNiApOH9tPcsBZRFuTATzng==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/metro-babel-transformer": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.4.tgz",
"integrity": "sha512-O0WMW/K8Ny/MAAeRebqGEQhrbzcioxcPHZtos+EH2hWeBTEKHQV8fMYYxfYDabpr392qdhSBwg3LlXUD4U3PXQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.5.tgz",
"integrity": "sha512-As/3zryghF12L983GaoBIgJFsDsepZfMCGGYz3zsfRqh4JjYaaGR5QAoWkk+NE+VDOwnvDp3zoVYtAqsu3wPbQ==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.20.0",
"@react-native/babel-preset": "0.75.4",
"@react-native/babel-preset": "0.75.5",
"hermes-parser": "0.22.0",
"nullthrows": "^1.1.1"
},
@@ -6276,14 +6275,14 @@
}
},
"node_modules/@react-native/metro-config": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.75.4.tgz",
"integrity": "sha512-gIIVlPUtZ1UKCxMJRtG88FoWS5REbI4YUmiyoM8eBUA85Zvk27b67iBX5Lkuxg8FGc7t9tjWQRpVGs2IK5uZpQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.75.5.tgz",
"integrity": "sha512-OrWZtYEznRRoTa4Cjj7zN2F6fZqfgzased7UQt7qRx1/O8DTrdVJDin4FBJ0mD570jTFF4yLHXcpgTbthPlBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@react-native/js-polyfills": "0.75.4",
"@react-native/metro-babel-transformer": "0.75.4",
"@react-native/js-polyfills": "0.75.5",
"@react-native/metro-babel-transformer": "0.75.5",
"metro-config": "^0.80.3",
"metro-runtime": "^0.80.3"
},
@@ -6292,15 +6291,15 @@
}
},
"node_modules/@react-native/normalize-colors": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.4.tgz",
"integrity": "sha512-90QrQDLg0/k9xqYesaKuIkayOSjD+FKa0hsHollbwT5h3kuGMY+lU7UZxnb8tU55Y1PKdvjYxqQsYWI/ql79zA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.5.tgz",
"integrity": "sha512-8yooxuLIwxI11O+pJ9LDtiaAiyXQeubROXALdAhU3kZQI49Nz0WB1ugLWj0Et+D8miD+79XPOFflcUqqoIY1nw==",
"license": "MIT"
},
"node_modules/@react-native/virtualized-lists": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.4.tgz",
"integrity": "sha512-iEauRiXjvWG/iOH8bV+9MfepCS+72cuL5rhkrenYZS0NUnDcNjF+wtaoS9+Gx5z1UJOfEXxSmyXRtQJZne8SnA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.5.tgz",
"integrity": "sha512-B1XMDXgptPpeWmmZABverRE9tBjDYuP97wQ/FeU7gHhZ+cwD+w68mwFZxcivr5fDz0Z4FaGlM7pR6oGjEn8NXA==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4",
@@ -9529,24 +9528,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array.prototype.map": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.4.tgz",
"integrity": "sha512-Qds9QnX7A0qISY7JT5WuJO0NJPE9CMlC6JzHQfhpqAAQQzufVRoeH7EzUY5GcPTx72voG8LV/5eo+b8Qi8hmhA==",
"dependencies": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.19.0",
"es-array-method-boxes-properly": "^1.0.0",
"is-string": "^1.0.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array.prototype.tosorted": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
@@ -11092,6 +11073,18 @@
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-js-compat": {
"version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz",
@@ -12244,11 +12237,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-array-method-boxes-properly": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -12265,24 +12253,6 @@
"node": ">= 0.4"
}
},
"node_modules/es-get-iterator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz",
"integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==",
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.0",
"has-symbols": "^1.0.1",
"is-arguments": "^1.1.0",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.5",
"isarray": "^2.0.5"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-iterator-helpers": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
@@ -15721,26 +15691,6 @@
"node": ">=0.10.0"
}
},
"node_modules/iterate-iterator": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz",
"integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/iterate-value": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
"integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
"dependencies": {
"es-get-iterator": "^1.0.2",
"iterate-iterator": "^1.0.1"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/iterator.prototype": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
@@ -16959,8 +16909,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"integrity": "sha512-POz5n9QppExZNX4ZguKoQOOLL0Gev2RL49ZtwiWs7QBHTat4gzBA9kYAeviRrmW4kVn7xcX0xeAxInbHDWs4lg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"integrity": "sha512-9gWT8koE7bS/32LuYrUKdsFYjJ0mkyQ1ctANG0KlRnEDqIzx4T+C+6F+RltiytSNxsMC+08+h1uC4BSxgqzyng==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -20085,25 +20035,6 @@
"asap": "~2.0.6"
}
},
"node_modules/promise.allsettled": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz",
"integrity": "sha512-o73CbvQh/OnPFShxHcHxk0baXR2a1m4ozb85ha0H14VEoi/EJJLa9mnPfEWJx9RjA9MLfhdjZ8I6HhWtBa64Ag==",
"dependencies": {
"array.prototype.map": "^1.0.3",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.2",
"get-intrinsic": "^1.0.2",
"iterate-value": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/promise.withresolvers": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/promise.withresolvers/-/promise.withresolvers-1.0.3.tgz",
@@ -20526,22 +20457,22 @@
}
},
"node_modules/react-native": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.4.tgz",
"integrity": "sha512-Jehg4AMNIAXu9cn0/1jbTCoNg3tc+t6EekwucCalN8YoRmxGd/PY6osQTI/5fSAM40JQ4O8uv8Qg09Ycpb5sxQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.5.tgz",
"integrity": "sha512-9vyUHhkmesL2A++b3dKuhprEq7Nh/1gKgB7MD0ybjCgI4ARob0+j6Myfphzes8fYRcVSAwuiNdp8H+N96+eXSA==",
"license": "MIT",
"dependencies": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native-community/cli": "14.1.0",
"@react-native-community/cli-platform-android": "14.1.0",
"@react-native-community/cli-platform-ios": "14.1.0",
"@react-native/assets-registry": "0.75.4",
"@react-native/codegen": "0.75.4",
"@react-native/community-cli-plugin": "0.75.4",
"@react-native/gradle-plugin": "0.75.4",
"@react-native/js-polyfills": "0.75.4",
"@react-native/normalize-colors": "0.75.4",
"@react-native/virtualized-lists": "0.75.4",
"@react-native/assets-registry": "0.75.5",
"@react-native/codegen": "0.75.5",
"@react-native/community-cli-plugin": "0.75.5",
"@react-native/gradle-plugin": "0.75.5",
"@react-native/js-polyfills": "0.75.5",
"@react-native/normalize-colors": "0.75.5",
"@react-native/virtualized-lists": "0.75.5",
"abort-controller": "^3.0.0",
"anser": "^1.4.9",
"ansi-regex": "^5.0.0",
@@ -28697,9 +28628,9 @@
"integrity": "sha512-L/gnRjnFxpQqvfILXeFoVTHEtwL01zoSKAIqMVxMmaD0ahozML8uztlxRPlYSbKrT31j3Cm1ss9KdSNeO3f9FQ=="
},
"@jitsi/rnnoise-wasm": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@jitsi/rnnoise-wasm/-/rnnoise-wasm-0.2.0.tgz",
"integrity": "sha512-ZpNws6UjbNjkhD68DQu862IjzfjYgLOwknqdlLotPbJqnBTA91vo5uh9+D0aUqSoY/U1aQhPZZB3IRj5ZNfw3Q=="
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@jitsi/rnnoise-wasm/-/rnnoise-wasm-0.2.1.tgz",
"integrity": "sha512-iEj77www43pS2Yq+cfLZb+hFuI7L5ccisBzzPMcOjjLsG4/LAlkD1CY58/8gc84nHdLBGmD/OPIWGnvYnXvB0A=="
},
"@jitsi/rtcstats": {
"version": "9.5.1",
@@ -29811,22 +29742,22 @@
"integrity": "sha512-9hhIlnNMfB6NO2738M437egA1p6inKdXXazh0qf6HJeBuLBTePSecePCOkVPczivM1mglquYXWCHJlx4D3Z7xw=="
},
"@react-native/assets-registry": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.4.tgz",
"integrity": "sha512-WX6/LNHwyjislSFM+h3qQjBiPaXXPJW5ZV4TdgNKb6QOPO0g1KGYRQj44cI2xSpZ3fcWrvQFZfQgSMbVK9Sg7A=="
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.75.5.tgz",
"integrity": "sha512-5Arp90qqwT4svF9izcFxM2tiDZlHSVKBuLT6gS1FbOABzHkKDK06Pv+CWNKZ2TM0l7qxRxNAd6e86tvnkMlGZw=="
},
"@react-native/babel-plugin-codegen": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.4.tgz",
"integrity": "sha512-gu5ZRIdr7+ufi09DJROhfDtbF4biTnCDJqtqcmtsku4cXOXPHE36QbC/vAmKEZ0PMPURBI8lwF2wfaeHLn7gig==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.75.5.tgz",
"integrity": "sha512-WUYzABcKhjCYCFsMTkvJ4CPSSeQ0ayLoIQjN87NtgrKzRq6yISTxF2J/2vgUuJgT99U7J6x6AgdNqlQ/U6M+qw==",
"requires": {
"@react-native/codegen": "0.75.4"
"@react-native/codegen": "0.75.5"
}
},
"@react-native/babel-preset": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.4.tgz",
"integrity": "sha512-UtyYCDJ3rZIeggyFEfh/q5t/FZ5a1h9F8EI37Nbrwyk/OKPH+1XS4PbHROHJzBARlJwOAfmT75+ovYUO0eakJA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.75.5.tgz",
"integrity": "sha512-VEJvWUu2zg9PacO6RL3n10TQ05IdbTIKiCpRAulexSAZZamURelX8Ko9VbWpIP0Wg/zFyYoDwsKOhCe+rFfhvA==",
"requires": {
"@babel/core": "^7.20.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
@@ -29870,7 +29801,7 @@
"@babel/plugin-transform-typescript": "^7.5.0",
"@babel/plugin-transform-unicode-regex": "^7.0.0",
"@babel/template": "^7.0.0",
"@react-native/babel-plugin-codegen": "0.75.4",
"@react-native/babel-plugin-codegen": "0.75.5",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
},
@@ -29883,9 +29814,9 @@
}
},
"@react-native/codegen": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.4.tgz",
"integrity": "sha512-0FplNAD/S5FUvm8YIn6uyarOcP4jdJPqWz17K4a/Gp2KSsG/JJKEskX3aj5wpePzVfNQl3WyvBJ0whODdCocIA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.75.5.tgz",
"integrity": "sha512-xIhr7UpnUySYJGgQmLrTPtTcaDlri7YECkAJwcLAxdDu2KVJzGoxNdHa2poCkTLceDBxvL+3hYYsoevl1PD45w==",
"requires": {
"@babel/parser": "^7.20.0",
"glob": "^7.1.1",
@@ -29913,14 +29844,14 @@
}
},
"@react-native/community-cli-plugin": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.4.tgz",
"integrity": "sha512-k/hevYPjEpW0MNVVyb3v9PJosOP+FzenS7+oqYNLXdEmgTnGHrAtYX9ABrJJgzeJt7I6g8g+RDvm8PSE+tnM5w==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.75.5.tgz",
"integrity": "sha512-rtZqsTb+2gtPw8BZgJlZ7p5ycXSGu4uql1gxybcv9qNgqlsNBZofiIdZEAade67/GfeQEvy/UY96SvoIsmVW6A==",
"requires": {
"@react-native-community/cli-server-api": "14.1.0",
"@react-native-community/cli-tools": "14.1.0",
"@react-native/dev-middleware": "0.75.4",
"@react-native/metro-babel-transformer": "0.75.4",
"@react-native/dev-middleware": "0.75.5",
"@react-native/metro-babel-transformer": "0.75.5",
"chalk": "^4.0.0",
"execa": "^5.1.1",
"metro": "^0.80.3",
@@ -29976,17 +29907,17 @@
}
},
"@react-native/debugger-frontend": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.4.tgz",
"integrity": "sha512-QfGurR5hV6bhMPn/6VxS2RomYrPRFGwA03jJr+zKyWHnxDAu5jOqYVyKAktIIbhYe5sPp78QVl1ZYuhcnsRbEw=="
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.75.5.tgz",
"integrity": "sha512-/mW0ELovNVAdZZFs0JC9147zF/GlvgrFPHhLhkgT95lOir/8cACLvR1kdVnH59sAQw2zKxyrFbrwhO80zPfp9g=="
},
"@react-native/dev-middleware": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.4.tgz",
"integrity": "sha512-UhyBeQOG2wNcvrUGw3+IBrHBk/lIu7hHGmWt4j8W9Aqv9BwktHKkPyko+5A1yoUeO1O/VDnHWYqWeOejcA9wpQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.75.5.tgz",
"integrity": "sha512-agVYocKP6YjW2g296jS1fipvC4U1vaykPnIgIgQfXhhVAYsFFJwO4Fh8oLcI82ceGc3mikv9cPrv8IrUoIWKsg==",
"requires": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.75.4",
"@react-native/debugger-frontend": "0.75.5",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@@ -30015,22 +29946,22 @@
}
},
"@react-native/gradle-plugin": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.4.tgz",
"integrity": "sha512-kKTmw7cF7p1raT30DC0L6N+xiVXN7dlRy0J+hYPiCRRVHplwgvyS7pszjxfzwXmHFqOxwpxQVI3du8opsma1Mg=="
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.75.5.tgz",
"integrity": "sha512-s6lPOVPLoGdQNbfgkVHvCC5Mg/mdTkg+5l5gChG6tNI4LZ6NVvaN0Ettt8bIKMoYlExsXQ2zpdBLopnjro5JgA=="
},
"@react-native/js-polyfills": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.4.tgz",
"integrity": "sha512-NF5ID5FjcVHBYk1LQ4JMRjPmxBWEo4yoqW1m6vGOQZPT8D5Qs9afgx3f7gQatxbn3ivMh0FVbLW0zBx6LyxEzA=="
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.75.5.tgz",
"integrity": "sha512-zDMMq2mtWZ1/P3CnOguBucMSsvzPSDuLHZPn33DjZv1VFSOjqyAbGy9F7ZEQ0Y+vNiApOH9tPcsBZRFuTATzng=="
},
"@react-native/metro-babel-transformer": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.4.tgz",
"integrity": "sha512-O0WMW/K8Ny/MAAeRebqGEQhrbzcioxcPHZtos+EH2hWeBTEKHQV8fMYYxfYDabpr392qdhSBwg3LlXUD4U3PXQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.75.5.tgz",
"integrity": "sha512-As/3zryghF12L983GaoBIgJFsDsepZfMCGGYz3zsfRqh4JjYaaGR5QAoWkk+NE+VDOwnvDp3zoVYtAqsu3wPbQ==",
"requires": {
"@babel/core": "^7.20.0",
"@react-native/babel-preset": "0.75.4",
"@react-native/babel-preset": "0.75.5",
"hermes-parser": "0.22.0",
"nullthrows": "^1.1.1"
},
@@ -30051,26 +29982,26 @@
}
},
"@react-native/metro-config": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.75.4.tgz",
"integrity": "sha512-gIIVlPUtZ1UKCxMJRtG88FoWS5REbI4YUmiyoM8eBUA85Zvk27b67iBX5Lkuxg8FGc7t9tjWQRpVGs2IK5uZpQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.75.5.tgz",
"integrity": "sha512-OrWZtYEznRRoTa4Cjj7zN2F6fZqfgzased7UQt7qRx1/O8DTrdVJDin4FBJ0mD570jTFF4yLHXcpgTbthPlBbA==",
"dev": true,
"requires": {
"@react-native/js-polyfills": "0.75.4",
"@react-native/metro-babel-transformer": "0.75.4",
"@react-native/js-polyfills": "0.75.5",
"@react-native/metro-babel-transformer": "0.75.5",
"metro-config": "^0.80.3",
"metro-runtime": "^0.80.3"
}
},
"@react-native/normalize-colors": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.4.tgz",
"integrity": "sha512-90QrQDLg0/k9xqYesaKuIkayOSjD+FKa0hsHollbwT5h3kuGMY+lU7UZxnb8tU55Y1PKdvjYxqQsYWI/ql79zA=="
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.75.5.tgz",
"integrity": "sha512-8yooxuLIwxI11O+pJ9LDtiaAiyXQeubROXALdAhU3kZQI49Nz0WB1ugLWj0Et+D8miD+79XPOFflcUqqoIY1nw=="
},
"@react-native/virtualized-lists": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.4.tgz",
"integrity": "sha512-iEauRiXjvWG/iOH8bV+9MfepCS+72cuL5rhkrenYZS0NUnDcNjF+wtaoS9+Gx5z1UJOfEXxSmyXRtQJZne8SnA==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.75.5.tgz",
"integrity": "sha512-B1XMDXgptPpeWmmZABverRE9tBjDYuP97wQ/FeU7gHhZ+cwD+w68mwFZxcivr5fDz0Z4FaGlM7pR6oGjEn8NXA==",
"requires": {
"invariant": "^2.2.4",
"nullthrows": "^1.1.1"
@@ -32427,18 +32358,6 @@
"es-shim-unscopables": "^1.0.2"
}
},
"array.prototype.map": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.4.tgz",
"integrity": "sha512-Qds9QnX7A0qISY7JT5WuJO0NJPE9CMlC6JzHQfhpqAAQQzufVRoeH7EzUY5GcPTx72voG8LV/5eo+b8Qi8hmhA==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.19.0",
"es-array-method-boxes-properly": "^1.0.0",
"is-string": "^1.0.7"
}
},
"array.prototype.tosorted": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
@@ -33572,6 +33491,12 @@
"toggle-selection": "^1.0.6"
}
},
"core-js": {
"version": "3.40.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
"dev": true
},
"core-js-compat": {
"version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz",
@@ -34398,11 +34323,6 @@
"which-typed-array": "^1.1.18"
}
},
"es-array-method-boxes-properly": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA=="
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -34413,21 +34333,6 @@
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-get-iterator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz",
"integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==",
"requires": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.0",
"has-symbols": "^1.0.1",
"is-arguments": "^1.1.0",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.5",
"isarray": "^2.0.5"
}
},
"es-iterator-helpers": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
@@ -36819,20 +36724,6 @@
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
},
"iterate-iterator": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz",
"integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw=="
},
"iterate-value": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz",
"integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==",
"requires": {
"es-get-iterator": "^1.0.2",
"iterate-iterator": "^1.0.1"
}
},
"iterator.prototype": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
@@ -37746,8 +37637,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"integrity": "sha512-POz5n9QppExZNX4ZguKoQOOLL0Gev2RL49ZtwiWs7QBHTat4gzBA9kYAeviRrmW4kVn7xcX0xeAxInbHDWs4lg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"integrity": "sha512-9gWT8koE7bS/32LuYrUKdsFYjJ0mkyQ1ctANG0KlRnEDqIzx4T+C+6F+RltiytSNxsMC+08+h1uC4BSxgqzyng==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
@@ -39992,19 +39883,6 @@
"asap": "~2.0.6"
}
},
"promise.allsettled": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz",
"integrity": "sha512-o73CbvQh/OnPFShxHcHxk0baXR2a1m4ozb85ha0H14VEoi/EJJLa9mnPfEWJx9RjA9MLfhdjZ8I6HhWtBa64Ag==",
"requires": {
"array.prototype.map": "^1.0.3",
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.2",
"get-intrinsic": "^1.0.2",
"iterate-value": "^1.0.2"
}
},
"promise.withresolvers": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/promise.withresolvers/-/promise.withresolvers-1.0.3.tgz",
@@ -40304,21 +40182,21 @@
}
},
"react-native": {
"version": "0.75.4",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.4.tgz",
"integrity": "sha512-Jehg4AMNIAXu9cn0/1jbTCoNg3tc+t6EekwucCalN8YoRmxGd/PY6osQTI/5fSAM40JQ4O8uv8Qg09Ycpb5sxQ==",
"version": "0.75.5",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.75.5.tgz",
"integrity": "sha512-9vyUHhkmesL2A++b3dKuhprEq7Nh/1gKgB7MD0ybjCgI4ARob0+j6Myfphzes8fYRcVSAwuiNdp8H+N96+eXSA==",
"requires": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native-community/cli": "14.1.0",
"@react-native-community/cli-platform-android": "14.1.0",
"@react-native-community/cli-platform-ios": "14.1.0",
"@react-native/assets-registry": "0.75.4",
"@react-native/codegen": "0.75.4",
"@react-native/community-cli-plugin": "0.75.4",
"@react-native/gradle-plugin": "0.75.4",
"@react-native/js-polyfills": "0.75.4",
"@react-native/normalize-colors": "0.75.4",
"@react-native/virtualized-lists": "0.75.4",
"@react-native/assets-registry": "0.75.5",
"@react-native/codegen": "0.75.5",
"@react-native/community-cli-plugin": "0.75.5",
"@react-native/gradle-plugin": "0.75.5",
"@react-native/js-polyfills": "0.75.5",
"@react-native/normalize-colors": "0.75.5",
"@react-native/virtualized-lists": "0.75.5",
"abort-controller": "^3.0.0",
"anser": "^1.4.9",
"ansi-regex": "^5.0.0",

View File

@@ -26,7 +26,7 @@
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
"@jitsi/rnnoise-wasm": "0.2.0",
"@jitsi/rnnoise-wasm": "0.2.1",
"@jitsi/rtcstats": "9.5.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
@@ -68,14 +68,13 @@
"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/v1908.0.0+2a5d7fcc/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1912.0.0+522577a4/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
"pixelmatch": "5.3.0",
"promise.allsettled": "1.0.4",
"promise.withresolvers": "1.0.3",
"punycode": "2.3.0",
"react": "18.2.0",
@@ -84,7 +83,7 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.75.4",
"react-native": "0.75.5",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-default-preference": "1.4.4",
@@ -131,12 +130,11 @@
"devDependencies": {
"@babel/core": "7.25.9",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-transform-private-methods": "7.25.9",
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "5.0.9",
"@react-native/metro-config": "0.75.4",
"@react-native/metro-config": "0.75.5",
"@stylistic/eslint-plugin": "2.12.1",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
@@ -175,6 +173,7 @@
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
"clean-css-cli": "4.3.0",
"core-js": "3.40.0",
"css-loader": "6.8.1",
"eslint": "8.57.0",
"eslint-plugin-import": "2.31.0",
@@ -224,8 +223,10 @@
"start": "make dev",
"test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
"test-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts --spec",
"test-ff-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts --spec",
"test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
"test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
"test-dev-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts --spec",
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts",
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec"
},

View File

@@ -87,7 +87,6 @@ export async function createHandlers({ getState }: IStore) {
amplitudeIncludeUTM,
blackListedEvents,
scriptURLs,
googleAnalyticsTrackingId,
matomoEndpoint,
matomoSiteID,
whiteListedEvents
@@ -98,7 +97,6 @@ export async function createHandlers({ getState }: IStore) {
amplitudeIncludeUTM,
blackListedEvents,
envType: deploymentInfo?.envType || 'dev',
googleAnalyticsTrackingId,
matomoEndpoint,
matomoSiteID,
group,
@@ -221,9 +219,6 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
overwritesPrejoinConfigICEUrl?: boolean;
overwritesSalesforceUrl?: boolean;
overwritesSupportUrl?: boolean;
overwritesWatchRTCConfigParams?: boolean;
overwritesWatchRTCProxyUrl?: boolean;
overwritesWatchRTCWSUrl?: boolean;
server?: string;
tenant?: string;
wasLobbyVisible?: boolean;
@@ -274,14 +269,6 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
permanentProperties.overwritesHosts = 'config.hosts' in params
|| Boolean(hostsProps.find(p => `config.hosts.${p}` in params || (typeof hosts === 'object' && p in hosts)));
permanentProperties.overwritesWatchRTCConfigParams = 'config.watchRTCConfigParams' in params;
const watchRTCConfigParams = params['config.watchRTCConfigParams'] ?? {};
permanentProperties.overwritesWatchRTCProxyUrl = ('config.watchRTCConfigParams.proxyUrl' in params)
|| (typeof watchRTCConfigParams === 'object' && 'proxyUrl' in watchRTCConfigParams);
permanentProperties.overwritesWatchRTCWSUrl = ('config.watchRTCConfigParams.wsUrl' in params)
|| (typeof watchRTCConfigParams === 'object' && 'wsUrl' in watchRTCConfigParams);
const prejoinConfig = params['config.prejoinConfig'] ?? {};
permanentProperties.overwritesPrejoinConfigICEUrl = ('config.prejoinConfig.preCallTestICEUrl' in params)

View File

@@ -14,7 +14,6 @@ interface IOptions {
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
envType?: string;
googleAnalyticsTrackingId?: string;
group?: string;
host?: string;
matomoEndpoint?: string;

View File

@@ -1,159 +0,0 @@
/* global ga */
import { getJitsiMeetGlobalNS } from '../../base/util/helpers';
import AbstractHandler, { IEvent } from './AbstractHandler';
/**
* Analytics handler for Google Analytics.
*/
class GoogleAnalyticsHandler extends AbstractHandler {
_userProperties: Object;
_userPropertiesString: string;
/**
* Creates new instance of the GA analytics handler.
*
* @param {Object} options - The Google Analytics options.
* @param {string} options.googleAnalyticsTrackingId - The GA track id
* required by the GA API.
*/
constructor(options: any) {
super(options);
this._userProperties = {};
if (!options.googleAnalyticsTrackingId) {
throw new Error('Failed to initialize Google Analytics handler, no tracking ID');
}
this._enabled = true;
this._initGoogleAnalytics(options);
}
/**
* Initializes the ga object.
*
* @param {Object} options - The Google Analytics options.
* @param {string} options.googleAnalyticsTrackingId - The GA track id
* required by the GA API.
* @returns {void}
*/
_initGoogleAnalytics(options: any) {
/**
* TODO: Keep this local, there's no need to add it to window.
*/
/* eslint-disable */ // @ts-ignore
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
// @ts-ignore
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
/* eslint-enable */
// @ts-ignore
ga('create', options.googleAnalyticsTrackingId, 'auto');
// @ts-ignore
ga('send', 'pageview');
}
/**
* Extracts the integer to use for a Google Analytics event's value field
* from a lib-jitsi-meet analytics event.
*
* @param {Object} event - The lib-jitsi-meet analytics event.
* @returns {number} - The integer to use for the 'value' of a Google
* analytics event, or NaN if the lib-jitsi-meet event doesn't contain a
* suitable value.
* @private
*/
_extractValue(event: IEvent) {
let value: string | number | undefined = event?.attributes?.value;
// Try to extract an integer from the "value" attribute.
value = Math.round(parseFloat(value ?? ''));
return value;
}
/**
* Extracts the string to use for a Google Analytics event's label field
* from a lib-jitsi-meet analytics event.
*
* @param {Object} event - The lib-jitsi-meet analytics event.
* @returns {string} - The string to use for the 'label' of a Google
* analytics event.
* @private
*/
_extractLabel(event: IEvent) {
const { attributes = {} } = event;
const labelsArray
= Object.keys(attributes).map(key => `${key}=${attributes[key]}`);
labelsArray.push(this._userPropertiesString);
return labelsArray.join('&');
}
/**
* Sets the permanent properties for the current session.
*
* @param {Object} userProps - The permanent portperties.
* @returns {void}
*/
setUserProperties(userProps: any = {}) {
if (!this._enabled) {
return;
}
// The label field is limited to 500B. We will concatenate all
// attributes of the event, except the user agent because it may be
// lengthy and is probably included from elsewhere.
const filter = [ 'user_agent', 'callstats_name' ];
this._userPropertiesString
= Object.keys(userProps)
.filter(key => filter.indexOf(key) === -1)
.map(key => `permanent_${key}=${userProps[key]}`)
.join('&');
}
/**
* This is the entry point of the API. The function sends an event to
* google analytics. The format of the event is described in
* analyticsAdapter in lib-jitsi-meet.
*
* @param {Object} event - The event in the format specified by
* lib-jitsi-meet.
* @returns {void}
*/
sendEvent(event: IEvent) {
if (this._shouldIgnore(event)) {
return;
}
const gaEvent: {
eventAction?: string;
eventCategory: string;
eventLabel: string;
eventValue?: number;
} = {
'eventCategory': 'jitsi-meet',
'eventAction': this._extractName(event),
'eventLabel': this._extractLabel(event)
};
const value = this._extractValue(event);
if (!isNaN(value)) {
gaEvent.eventValue = value;
}
// @ts-ignore
ga('send', 'event', gaEvent);
}
}
const globalNS = getJitsiMeetGlobalNS();
globalNS.analyticsHandlers = globalNS.analyticsHandlers || [];
globalNS.analyticsHandlers.push(GoogleAnalyticsHandler);

View File

@@ -26,11 +26,6 @@ export interface IProps {
* @abstract
*/
export class AbstractApp<P extends IProps = IProps> extends BaseApp<P> {
/**
* The deferred for the initialization {{promise, resolve, reject}}.
*/
_init: PromiseWithResolvers<any>;
/**
* Initializes the app.
*

View File

@@ -13,6 +13,7 @@ import '../mobile/react-native-sdk/middleware';
import '../mobile/watchos/middleware';
import '../share-room/middleware';
import '../shared-video/middleware';
import '../toolbox/middleware.native';
import '../whiteboard/middleware.native';
import './middlewares.any';

View File

@@ -237,7 +237,6 @@ export function getConferenceOptions(stateful: IStateful) {
if (options.disableThirdPartyRequests) {
delete config.analytics?.scriptURLs;
delete config.analytics?.amplitudeAPPKey;
delete config.analytics?.googleAnalyticsTrackingId;
}
return options;

View File

@@ -177,7 +177,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
dispatch(showErrorNotification({
description: 'Restart initiated because of a bridge failure',
titleKey: 'dialog.sessionRestarted'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
}
break;
@@ -190,7 +190,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
descriptionArguments: { msg },
descriptionKey: msg ? 'dialog.connectErrorWithMsg' : 'dialog.connectError',
titleKey: 'connection.CONNFAIL'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
break;
}
@@ -199,7 +199,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
hideErrorSupportLink: true,
descriptionKey: 'dialog.maxUsersLimitReached',
titleKey: 'dialog.maxUsersLimitReachedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
// In case of max users(it can be from a visitor node), let's restore
// oldConfig if any as we will be back to the main prosody.
@@ -236,7 +236,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
descriptionKey,
hideErrorSupportLink: true,
titleKey
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
}));
sendAnalytics(createNotAllowedErrorEvent(type, msg));
@@ -416,7 +416,7 @@ function _connectionFailed({ dispatch, getState }: IStore, next: Function, actio
descriptionKey: errors ? 'dialog.tokenAuthFailedWithReasons' : 'dialog.tokenAuthFailed',
descriptionArguments: { reason: errors },
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
}));
}
}

View File

@@ -185,7 +185,6 @@ export interface IConfig {
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
disabled?: boolean;
googleAnalyticsTrackingId?: string;
matomoEndpoint?: string;
matomoSiteID?: string;
obfuscateRoomName?: boolean;

View File

@@ -20,7 +20,6 @@ export function _cleanupConfig(config: IConfig) {
if (NativeModules.AppInfo.LIBRE_BUILD) {
delete config.analytics?.amplitudeAPPKey;
delete config.analytics?.googleAnalyticsTrackingId;
delete config.analytics?.rtcstatsEnabled;
delete config.analytics?.rtcstatsEndpoint;
delete config.analytics?.rtcstatsPollInterval;

View File

@@ -1,3 +1,5 @@
/* eslint-disable require-jsdoc */
import { IReduxState, IStore } from '../../app/types';
import JitsiMeetJS from '../lib-jitsi-meet';
import { updateSettings } from '../settings/actions';
@@ -157,7 +159,8 @@ export function getDevicesFromURL(state: IReduxState) {
* @returns {Object} An object with the media devices split by type. The keys
* are device type and the values are arrays with devices matching the device
* type.
*/
*/
// @ts-ignore
export function groupDevicesByKind(devices: MediaDeviceInfo[]): IDevicesState['availableDevices'] {
return {
audioInput: devices.filter(device => device.kind === 'audioinput'),
@@ -166,24 +169,16 @@ export function groupDevicesByKind(devices: MediaDeviceInfo[]): IDevicesState['a
};
}
/**
* Filters audio devices from a list of MediaDeviceInfo objects.
*
* @param {Array<MediaDeviceInfo>} devices - Unfiltered media devices.
* @private
* @returns {Array<MediaDeviceInfo>} Filtered audio devices.
*/
export function filterAudioDevices(devices: MediaDeviceInfo[]) {
return devices.filter(device => device.kind === 'audioinput');
}
/**
* Filters the devices that start with one of the prefixes from DEVICE_LABEL_PREFIXES_TO_IGNORE.
*
* @param {MediaDeviceInfo[]} devices - The devices to be filtered.
* @returns {MediaDeviceInfo[]} - The filtered devices.
*/
// @ts-ignore
export function filterIgnoredDevices(devices: MediaDeviceInfo[] = []) {
// @ts-ignore
const ignoredDevices: MediaDeviceInfo[] = [];
const filteredDevices = devices.filter(device => {
if (!device.label) {
@@ -212,6 +207,7 @@ export function filterIgnoredDevices(devices: MediaDeviceInfo[] = []) {
* @param {MediaDeviceInfo[]} devices2 - Array with devices to be compared.
* @returns {boolean} - True if the device arrays are different and false otherwise.
*/
// @ts-ignore
export function areDevicesDifferent(devices1: MediaDeviceInfo[] = [], devices2: MediaDeviceInfo[] = []) {
if (devices1.length !== devices2.length) {
return true;
@@ -315,6 +311,7 @@ export function getVideoDeviceIds(state: IReduxState) {
* @param {MediaDeviceInfo[]} devices - The devices.
* @returns {string}
*/
// @ts-ignore
function devicesToStr(devices?: MediaDeviceInfo[]) {
return devices?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
}
@@ -326,6 +323,7 @@ function devicesToStr(devices?: MediaDeviceInfo[]) {
* @param {string} title - The title that will be printed in the log.
* @returns {void}
*/
// @ts-ignore
export function logDevices(devices: MediaDeviceInfo[], title = '') {
const deviceList = groupDevicesByKind(devices);
const audioInputs = devicesToStr(deviceList.audioInput);

View File

@@ -5,7 +5,7 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../../app/types';
import DeviceStatus from '../../../../prejoin/components/web/preview/DeviceStatus';
import { isRoomNameEnabled } from '../../../../prejoin/functions';
import { isRoomNameEnabled } from '../../../../prejoin/functions.web';
import Toolbox from '../../../../toolbox/components/web/Toolbox';
import { isButtonEnabled } from '../../../../toolbox/functions.web';
import { getConferenceName } from '../../../conference/functions';

View File

@@ -291,7 +291,7 @@ export function showNoDataFromSourceVideoError(jitsiTrack: any) {
const notificationAction = dispatch(showErrorNotification({
descriptionKey: 'dialog.cameraNotSendingData',
titleKey: 'dialog.cameraNotSendingDataTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
notificationInfo = {
uid: notificationAction?.uid
@@ -838,16 +838,6 @@ export function toggleCamera() {
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
const currentFacingMode = localVideoTrack.getCameraFacingMode();
const { localFlipX } = state['features/base/settings'];
/**
* FIXME: Ideally, we should be dispatching {@code replaceLocalTrack} here,
* but it seems to not trigger the re-rendering of the local video on Chrome;
* could be due to a plan B vs unified plan issue. Therefore, we use the legacy
* method defined in conference.js that manually takes care of updating the local
* video as well.
*/
await APP.conference.useVideoStream(null);
const targetFacingMode = currentFacingMode === CAMERA_FACING_MODE.USER
? CAMERA_FACING_MODE.ENVIRONMENT
: CAMERA_FACING_MODE.USER;
@@ -857,7 +847,6 @@ export function toggleCamera() {
const newVideoTrack = await createLocalTrack('video', null, null, { facingMode: targetFacingMode });
// FIXME: See above.
await APP.conference.useVideoStream(newVideoTrack);
dispatch(replaceLocalTrack(localVideoTrack, newVideoTrack));
};
}

View File

@@ -157,9 +157,9 @@ async function _toggleScreenSharing(
try {
tracks = await createLocalTracksF(options) as any[];
} catch (error) {
dispatch(handleScreenSharingError(error, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(handleScreenSharingError(error));
throw error;
return;
}
}
@@ -171,9 +171,9 @@ async function _toggleScreenSharing(
desktopVideoTrack.dispose();
if (!desktopAudioTrack) {
dispatch(handleScreenSharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(handleScreenSharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK));
throw new Error(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK);
return;
}
} else if (desktopVideoTrack) {
if (localScreenshare) {
@@ -457,7 +457,7 @@ export function displayErrorsForCreateInitialLocalTracks(errors: IInitialTracksE
} = errors;
if (screenSharingError) {
dispatch(handleScreenSharingError(screenSharingError, NOTIFICATION_TIMEOUT_TYPE.LONG));
dispatch(handleScreenSharingError(screenSharingError));
}
if (audioOnlyError || videoOnlyError) {
if (audioOnlyError) {
@@ -476,12 +476,10 @@ export function displayErrorsForCreateInitialLocalTracks(errors: IInitialTracksE
*
* @private
* @param {Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK} error - The error.
* @param {NOTIFICATION_TIMEOUT_TYPE} timeout - The time for showing the notification.
* @returns {Function}
*/
export function handleScreenSharingError(
error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK,
timeout: NOTIFICATION_TIMEOUT_TYPE) {
error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
return (dispatch: IStore['dispatch']) => {
logger.error('failed to share local desktop', error);
@@ -508,6 +506,6 @@ export function handleScreenSharingError(
dispatch(showErrorNotification({
descriptionKey,
titleKey
}, timeout));
}));
};
}

View File

@@ -23,7 +23,7 @@ interface IState {
/**
* The id of the last read message.
*/
lastReadMessageId: string;
lastReadMessageId: string | null;
}
/**

View File

@@ -163,7 +163,7 @@ class Conference extends AbstractConference<IProps, any> {
// Bind event handler so it is only bound once for every instance.
this._onFullScreenChange = this._onFullScreenChange.bind(this);
this._onVidespaceTouchStart = this._onVidespaceTouchStart.bind(this);
this._onVideospaceTouchStart = this._onVideospaceTouchStart.bind(this);
this._setBackground = this._setBackground.bind(this);
}
@@ -241,11 +241,11 @@ class Conference extends AbstractConference<IProps, any> {
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
<ConferenceInfo />
{ _showPrejoin || _showLobby || <ConferenceInfo /> }
<Notice />
<div
id = 'videospace'
onTouchStart = { this._onVidespaceTouchStart }>
onTouchStart = { this._onVideospaceTouchStart }>
<LargeVideo />
{
_showPrejoin || _showLobby || (<>
@@ -322,7 +322,7 @@ class Conference extends AbstractConference<IProps, any> {
* @private
* @returns {void}
*/
_onVidespaceTouchStart() {
_onVideospaceTouchStart() {
this.props.dispatch(toggleToolboxVisible());
}

View File

@@ -270,7 +270,14 @@ class DesktopPicker extends PureComponent<IProps, IState> {
* @returns {void}
*/
_onCloseModal(id = '', type?: string, screenShareAudio = false) {
this.props.onSourceChoose(id, type, screenShareAudio);
// Find the entire source object from the id. We need the name in order
// to get getDisplayMedia working in Electron.
const { sources } = this.state;
// @ts-ignore
const source = sources.screen.concat(sources.window).find(s => s.id === id);
this.props.onSourceChoose(id, type, screenShareAudio, source);
this.props.dispatch(hideDialog());
}

View File

@@ -8,8 +8,8 @@ import { getLocalParticipant } from '../../../base/participants/functions';
import Platform from '../../../base/react/Platform.native';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { isToolboxVisible } from '../../../toolbox/functions';
import { setVisibleRemoteParticipants } from '../../actions';
import { isToolboxVisible } from '../../../toolbox/functions.native';
import { setVisibleRemoteParticipants } from '../../actions.native';
import {
getFilmstripDimensions,
isFilmstripVisible,

View File

@@ -26,7 +26,7 @@ import {
setUserFilmstripWidth,
setUserIsResizing,
setVisibleRemoteParticipants
} from '../../actions';
} from '../../actions.web';
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
@@ -39,10 +39,10 @@ import {
} from '../../constants';
import {
getVerticalViewMaxWidth,
isFilmstripDisabled,
isStageFilmstripTopPanel,
shouldRemoteVideosBeVisible
} from '../../functions';
import { isFilmstripDisabled } from '../../functions.web';
} from '../../functions.web';
import AudioTracksContainer from './AudioTracksContainer';
import Thumbnail from './Thumbnail';

View File

@@ -190,7 +190,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
}));
dispatch(showErrorNotification({
titleKey: 'addPeople.failedToAdd'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}));
} else if (!_callFlowsEnabled) {
const invitedCount = invitees.length;
let notificationProps: INotificationProps | undefined;

View File

@@ -4,7 +4,7 @@ import { debounce } from 'lodash-es';
import { NativeEventEmitter, NativeModules } from 'react-native';
import { AnyAction } from 'redux';
// @ts-expect-error
// @ts-ignore
import { ENDPOINT_TEXT_MESSAGE_NAME } from '../../../../modules/API/constants';
import { appNavigate } from '../../app/actions.native';
import { IStore } from '../../app/types';
@@ -32,8 +32,7 @@ import {
JITSI_CONNECTION_URL_KEY
} from '../../base/connection/constants';
import { getURLWithoutParams } from '../../base/connection/utils';
import {
JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
import { JitsiConferenceEvents, JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
import { toggleCameraFacingMode } from '../../base/media/actions';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../base/media/constants';
@@ -51,6 +50,11 @@ import { getLocalTracks, isLocalTrackMuted } from '../../base/tracks/functions.n
import { ITrack } from '../../base/tracks/types';
import { CLOSE_CHAT, OPEN_CHAT } from '../../chat/actionTypes';
import { closeChat, openChat, sendMessage, setPrivateMessageRecipient } from '../../chat/actions.native';
import { isEnabled as isDropboxEnabled } from '../../dropbox/functions.native';
import { hideNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../notifications/constants';
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../recording/constants';
import { getActiveSession } from '../../recording/functions';
import { setRequestingSubtitles } from '../../subtitles/actions.any';
import { CUSTOM_BUTTON_PRESSED } from '../../toolbox/actionTypes';
import { muteLocal } from '../../video-menu/actions.native';
@@ -419,6 +423,158 @@ function _registerForNativeEvents(store: IStore) {
eventEmitter.addListener(ExternalAPI.TOGGLE_CAMERA, () => {
dispatch(toggleCameraFacingMode());
});
eventEmitter.addListener(ExternalAPI.SHOW_NOTIFICATION,
({ appearance, description, timeout, title, uid }: any) => {
const validTypes = Object.values(NOTIFICATION_TYPE);
const validTimeouts = Object.values(NOTIFICATION_TIMEOUT_TYPE);
if (!validTypes.includes(appearance)) {
logger.error(`Invalid notification type "${appearance}". Expecting one of ${validTypes}`);
return;
}
if (!validTimeouts.includes(timeout)) {
logger.error(`Invalid notification timeout "${timeout}". Expecting one of ${validTimeouts}`);
return;
}
dispatch(showNotification({
appearance,
description,
title,
uid
}, timeout));
});
eventEmitter.addListener(ExternalAPI.HIDE_NOTIFICATION, ({ uid }: any) => {
dispatch(hideNotification(uid));
});
eventEmitter.addListener(ExternalAPI.START_RECORDING, (
{
mode,
dropboxToken,
shouldShare,
rtmpStreamKey,
rtmpBroadcastID,
youtubeStreamKey,
youtubeBroadcastID,
extraMetadata = {},
transcription
}: any) => {
const state = store.getState();
const conference = getCurrentConference(state);
if (!conference) {
logger.error('Conference is not defined');
return;
}
if (dropboxToken && !isDropboxEnabled(state)) {
logger.error('Failed starting recording: dropbox is not enabled on this deployment');
return;
}
if (mode === JitsiRecordingConstants.mode.STREAM && !(youtubeStreamKey || rtmpStreamKey)) {
logger.error('Failed starting recording: missing youtube or RTMP stream key');
return;
}
let recordingConfig;
if (mode === JitsiRecordingConstants.mode.FILE) {
const { recordingService } = state['features/base/config'];
if (!recordingService?.enabled && !dropboxToken) {
logger.error('Failed starting recording: the recording service is not enabled');
return;
}
if (dropboxToken) {
recordingConfig = {
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
...extraMetadata,
'upload_credentials': {
'service_name': RECORDING_TYPES.DROPBOX,
'token': dropboxToken
}
}
})
};
} else {
recordingConfig = {
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
...extraMetadata,
'share': shouldShare
}
})
};
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
recordingConfig = {
broadcastId: youtubeBroadcastID || rtmpBroadcastID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey || rtmpStreamKey
};
}
// Start audio / video recording, if requested.
if (typeof recordingConfig !== 'undefined') {
conference.startRecording(recordingConfig);
}
if (transcription) {
store.dispatch(setRequestingSubtitles(true, false, null));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: true
});
}
});
eventEmitter.addListener(ExternalAPI.STOP_RECORDING, ({ mode, transcription }: any) => {
const state = store.getState();
const conference = getCurrentConference(state);
if (!conference) {
logger.error('Conference is not defined');
return;
}
if (transcription) {
store.dispatch(setRequestingSubtitles(false, false, null));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false
});
}
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
logger.error('Invalid recording mode provided!');
return;
}
const activeSession = getActiveSession(state, mode);
if (!activeSession?.id) {
logger.error('No recording or streaming session found');
return;
}
conference.stopRecording(activeSession.id);
});
}
/**
@@ -439,6 +595,10 @@ function _unregisterForNativeEvents() {
eventEmitter.removeAllListeners(ExternalAPI.SEND_CHAT_MESSAGE);
eventEmitter.removeAllListeners(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED);
eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_CAMERA);
eventEmitter.removeAllListeners(ExternalAPI.SHOW_NOTIFICATION);
eventEmitter.removeAllListeners(ExternalAPI.HIDE_NOTIFICATION);
eventEmitter.removeAllListeners(ExternalAPI.START_RECORDING);
eventEmitter.removeAllListeners(ExternalAPI.STOP_RECORDING);
}
/**

View File

@@ -4,7 +4,6 @@ import { NativeModules, Platform } from 'react-native';
import BackgroundTimer from 'react-native-background-timer';
import { TextDecoder, TextEncoder } from 'text-encoding';
import 'promise.allsettled/auto'; // Promise.allSettled.
import 'promise.withresolvers/auto'; // Promise.withResolvers.
import 'react-native-url-polyfill/auto'; // Complete URL polyfill.

View File

@@ -1,7 +1,6 @@
import { IStore } from '../app/types';
import { getLocalJitsiAudioTrack } from '../base/tracks/functions';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { NoiseSuppressionEffect } from '../stream-effects/noise-suppression/NoiseSuppressionEffect';
import { SET_NOISE_SUPPRESSION_ENABLED } from './actionTypes';
@@ -93,7 +92,7 @@ export function setNoiseSuppressionEnabled(enabled: boolean): any {
dispatch(showErrorNotification({
titleKey: 'notify.noiseSuppressionFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}));
}
};
}

View File

@@ -97,7 +97,7 @@ export function setNotificationsEnabled(enabled: boolean) {
* @param {string} type - Notification type.
* @returns {Object}
*/
export function showErrorNotification(props: INotificationProps, type?: string) {
export function showErrorNotification(props: INotificationProps, type = NOTIFICATION_TIMEOUT_TYPE.STICKY) {
return showNotification({
...props,
appearance: NOTIFICATION_TYPE.ERROR

View File

@@ -126,10 +126,3 @@ export const SILENT_JOIN_THRESHOLD = 30;
* Amount of participants beyond which no left notification will be emitted.
*/
export const SILENT_LEFT_THRESHOLD = 30;
/**
* The identifier for the transcriber notifications.
*
* @type {string}
*/
export const TRANSCRIBING_NOTIFICATION_ID = 'TRANSCRIBING_NOTIFICATION';

View File

@@ -5,7 +5,6 @@ import { IStore } from '../app/types';
import { APP_WILL_MOUNT } from '../base/app/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import OldElectronAPPNotificationDescription from './components/OldElectronAPPNotificationDescription';
import { isOldJitsiMeetElectronApp } from './functions';
@@ -35,7 +34,7 @@ function _appWillMount(store: IStore, next: Function, action: AnyAction) {
dispatch(showErrorNotification({
titleKey: 'notify.OldElectronAPPTitle',
description: <OldElectronAPPNotificationDescription />
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
}
return next(action);

View File

@@ -16,7 +16,6 @@ import {
import { openURLInBrowser } from '../base/util/openURLInBrowser';
import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL } from '../invite/functions';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { INotificationProps } from '../notifications/types';
import {
@@ -108,7 +107,7 @@ function pollForStatus(
case DIAL_OUT_STATUS.DISCONNECTED: {
dispatch(showErrorNotification({
titleKey: 'prejoin.errorDialOutDisconnected'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
return onFail();
}
@@ -116,7 +115,7 @@ function pollForStatus(
case DIAL_OUT_STATUS.FAILED: {
dispatch(showErrorNotification({
titleKey: 'prejoin.errorDialOutFailed'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
return onFail();
}
@@ -124,7 +123,7 @@ function pollForStatus(
} catch (err) {
dispatch(showErrorNotification({
titleKey: 'prejoin.errorDialOutStatus'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
logger.error('Error getting dial out status', err);
onFail();
}
@@ -177,7 +176,7 @@ export function dialOut(onSuccess: Function, onFail: Function) {
}
}
dispatch(showErrorNotification(notification, NOTIFICATION_TIMEOUT_TYPE.LONG));
dispatch(showErrorNotification(notification));
logger.error('Error dialing out', err);
onFail();
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import RaiseHandButton from '../../../toolbox/components/native/RaiseHandButton';
import { shouldDisplayReactionsButtons } from '../../functions.native';
import ReactionsMenuButton from './ReactionsMenuButton';
const RaiseHandContainerButtons = (props: AbstractButtonProps) => {
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
return _shouldDisplayReactionsButtons
? <ReactionsMenuButton
{ ...props }
showRaiseHand = { true } />
: <RaiseHandButton { ...props } />;
};
export default RaiseHandContainerButtons;

View File

@@ -189,7 +189,7 @@ export function highlightMeetingMoment() {
* @returns {showErrorNotification}
*/
export function showRecordingError(props: Object) {
return showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.LONG);
return showErrorNotification(props);
}
/**
@@ -301,7 +301,7 @@ export function showStartedRecordingNotification(
} catch (err) {
dispatch(showErrorNotification({
titleKey: 'recording.errorFetchingLink'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}));
return logger.error('Could not fetch recording link', err);
}

View File

@@ -9,7 +9,6 @@ import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { updateDropboxToken } from '../../../dropbox/actions';
import { getDropboxData, getNewAccessToken, isEnabled as isDropboxEnabled } from '../../../dropbox/functions.any';
import { showErrorNotification } from '../../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
import { setSelectedRecordingService, startLocalVideoRecording } from '../../actions';
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../constants';
@@ -381,7 +380,7 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
} else {
dispatch(showErrorNotification({
titleKey: 'dialog.noDropboxToken'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
return;
}

View File

@@ -35,8 +35,6 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<IProps> {
);
}
_onSubmit: () => boolean;
/**
* Toggles screenshot capture.
*

View File

@@ -68,8 +68,12 @@ class RecordingExpandedLabel extends ExpandedLabel<IProps> {
let content = t(`${prefix}.${postfix}`);
if (_status === JitsiRecordingConstants.status.ON && this.props._isTranscribing) {
content += ` \u00B7 ${t('transcribing.labelToolTip')}`;
if (this.props._isTranscribing) {
if (_status === JitsiRecordingConstants.status.ON) {
content += ` ${t('transcribing.labelTooltipExtra')}`;
} else {
content = t('transcribing.labelTooltip');
}
}
return content;

View File

@@ -60,12 +60,12 @@ class RecordingLabel extends AbstractRecordingLabel<IProps> {
content = t(isRecording ? 'videoStatus.recording' : 'videoStatus.streaming');
if (_isTranscribing) {
content += ` \u00B7 ${t('transcribing.labelToolTip')}`;
content += ` ${t('transcribing.labelTooltipExtra')}`;
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
return null;
} else if (_isTranscribing) {
content = t('transcribing.labelToolTip');
content = t('transcribing.labelTooltip');
} else {
return null;
}

View File

@@ -162,7 +162,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
false, 'local', err.message, isRecorderTranscriptionsRunning(getState()));
}
dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
dispatch(showErrorNotification(props));
});
break;
}
@@ -204,10 +204,14 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
= getSessionById(state, action.sessionData.id);
const { initiator, mode = '', terminator } = updatedSessionData ?? {};
const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
const isRecordingStarting = updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING;
if (updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING) {
dispatch(showPendingRecordingNotification(mode));
if (isRecordingStarting || updatedSessionData?.status === ON) {
dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
}
if (isRecordingStarting) {
dispatch(showPendingRecordingNotification(mode));
break;
}

View File

@@ -149,7 +149,7 @@ class PasswordRequiredPrompt extends Component<IProps, IState> {
// We have used the password so let's clean it.
this.setState({
password: undefined
password: ''
});
return true;

View File

@@ -142,7 +142,7 @@ function _setPasswordFailed(store: IStore, next: Function, action: AnyAction) {
APP.store.dispatch(showErrorNotification({
descriptionKey,
titleKey
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
}
return next(action);

View File

@@ -6,9 +6,9 @@ import { timeout } from '../../virtual-background/functions';
import logger from '../../virtual-background/logger';
import JitsiStreamBackgroundEffect, { IBackgroundEffectOptions } from './JitsiStreamBackgroundEffect';
// @ts-expect-error
// @ts-ignore
import createTFLiteModule from './vendor/tflite/tflite';
// @ts-expect-error
// @ts-ignore
import createTFLiteSIMDModule from './vendor/tflite/tflite-simd';
const models = {
modelLandscape: 'libs/selfie_segmentation_landscape.tflite'

View File

@@ -13,9 +13,5 @@ var Module=typeof createTFLiteSIMDModule!=="undefined"?createTFLiteSIMDModule:{}
}
);
})();
if (typeof exports === 'object' && typeof module === 'object')
module.exports = createTFLiteSIMDModule;
else if (typeof define === 'function' && define['amd'])
define([], function() { return createTFLiteSIMDModule; });
else if (typeof exports === 'object')
exports["createTFLiteSIMDModule"] = createTFLiteSIMDModule;
export default createTFLiteSIMDModule;

View File

@@ -13,9 +13,5 @@ var Module=typeof createTFLiteModule!=="undefined"?createTFLiteModule:{};var rea
}
);
})();
if (typeof exports === 'object' && typeof module === 'object')
module.exports = createTFLiteModule;
else if (typeof define === 'function' && define['amd'])
define([], function() { return createTFLiteModule; });
else if (typeof exports === 'object')
exports["createTFLiteModule"] = createTFLiteModule;
export default createTFLiteModule;

View File

@@ -6,11 +6,13 @@ import { setVideoMuted } from '../base/media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../base/media/constants';
import {
SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_SHIFT_UP,
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
import { IMainToolbarButtonThresholds } from './types';
/**
* Enables/disables the toolbox.
@@ -118,3 +120,54 @@ export function setShiftUp(shiftUp: boolean) {
shiftUp
};
}
/**
* Sets the mainToolbarButtonsThresholds.
*
* @param {IMainToolbarButtonThresholds} thresholds - Thresholds for screen size and visible main toolbar buttons.
* @returns {Function}
*/
export function setMainToolbarThresholds(thresholds: IMainToolbarButtonThresholds) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { mainToolbarButtons } = getState()['features/base/config'];
if (!Array.isArray(mainToolbarButtons) || mainToolbarButtons.length === 0) {
return;
}
const mainToolbarButtonsThresholds: IMainToolbarButtonThresholds = [];
const mainToolbarButtonsLengthMap = new Map();
let orderIsChanged = false;
mainToolbarButtons.forEach(buttons => {
if (!Array.isArray(buttons) || buttons.length === 0) {
return;
}
mainToolbarButtonsLengthMap.set(buttons.length, buttons);
});
thresholds.forEach(({ width, order }) => {
let finalOrder = mainToolbarButtonsLengthMap.get(order.length);
if (finalOrder) {
orderIsChanged = true;
} else {
finalOrder = order;
}
mainToolbarButtonsThresholds.push({
order: finalOrder,
width
});
});
if (orderIsChanged) {
dispatch({
type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
mainToolbarButtonsThresholds
});
}
};
}

View File

@@ -36,7 +36,7 @@ export function setOverflowMenuVisible(_visible: boolean): any {
* text: string
* }}
*/
export function customButtonPressed(id: string, text: string) {
export function customButtonPressed(id: string, text: string | undefined) {
return {
type: CUSTOM_BUTTON_PRESSED,
id,

View File

@@ -8,16 +8,13 @@ import {
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN,
SET_HANGUP_MENU_VISIBLE,
SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import { setToolboxVisible } from './actions.web';
import { THRESHOLDS } from './constants';
import { getToolbarTimeout } from './functions.web';
import { IMainToolbarButtonThresholds } from './types';
export * from './actions.any';
@@ -124,56 +121,6 @@ export function setFullScreen(fullScreen: boolean) {
};
}
/**
* Sets the mainToolbarButtonsThresholds.
*
* @returns {Function}
*/
export function setMainToolbarThresholds() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { mainToolbarButtons } = getState()['features/base/config'];
if (!mainToolbarButtons || !Array.isArray(mainToolbarButtons) || mainToolbarButtons.length === 0) {
return;
}
const mainToolbarButtonsThresholds: IMainToolbarButtonThresholds = [];
const mainToolbarButtonsLenghtMap = new Map();
let orderIsChanged = false;
mainToolbarButtons.forEach(buttons => {
if (!Array.isArray(buttons) || buttons.length === 0) {
return;
}
mainToolbarButtonsLenghtMap.set(buttons.length, buttons);
});
THRESHOLDS.forEach(({ width, order }) => {
let finalOrder = mainToolbarButtonsLenghtMap.get(order.length);
if (finalOrder) {
orderIsChanged = true;
} else {
finalOrder = order;
}
mainToolbarButtonsThresholds.push({
order: finalOrder,
width
});
});
if (orderIsChanged) {
dispatch({
type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
mainToolbarButtonsThresholds
});
}
};
}
/**
* Shows the toolbox for specified timeout.
*

View File

@@ -10,12 +10,12 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import styles from './styles';
interface IProps extends AbstractButtonProps {
export interface ICustomOptionButton extends AbstractButtonProps {
backgroundColor?: string;
icon: any;
id?: string;
isToolboxButton?: boolean;
text?: string;
text: string;
}
/**
@@ -23,7 +23,7 @@ interface IProps extends AbstractButtonProps {
*
* @returns {Component}
*/
class CustomOptionButton extends AbstractButton<IProps> {
class CustomOptionButton extends AbstractButton<ICustomOptionButton> {
backgroundColor = this.props.backgroundColor;
iconSrc = this.props.icon;
id = this.props.id;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import HangupButton from '../HangupButton';
import HangupMenuButton from './HangupMenuButton';
const HangupContainerButtons = (props: AbstractButtonProps) => {
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
const endConferenceSupported = conference?.isEndConferenceSupported();
return endConferenceSupported
// @ts-ignore
? <HangupMenuButton { ...props } />
: <HangupButton { ...props } />;
};
export default HangupContainerButtons;

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { ViewStyle } from 'react-native';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { hideSheet } from '../../../base/dialog/actions';
@@ -22,18 +22,17 @@ import { isSharedVideoEnabled } from '../../../shared-video/functions';
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
import TileViewButton from '../../../video-layout/components/TileViewButton';
import styles from '../../../video-menu/components/native/styles';
import WhiteboardButton from '../../../whiteboard/components/native/WhiteboardButton';
import { customButtonPressed } from '../../actions.native';
import { getMovableButtons } from '../../functions.native';
import { getVisibleNativeButtons } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import AudioOnlyButton from './AudioOnlyButton';
import CustomOptionButton from './CustomOptionButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OpenCarmodeButton from './OpenCarmodeButton';
import RaiseHandButton from './RaiseHandButton';
import ScreenSharingButton from './ScreenSharingButton';
/**
@@ -41,11 +40,6 @@ import ScreenSharingButton from './ScreenSharingButton';
*/
interface IProps {
/**
* Custom Toolbar buttons.
*/
_customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>;
/**
* True if breakout rooms feature is available, false otherwise.
*/
@@ -66,6 +60,16 @@ interface IProps {
*/
_isSpeakerStatsDisabled?: boolean;
/**
* Toolbar buttons.
*/
_mainMenuButtons?: Array<IToolboxNativeButton>;
/**
* Overflow menu buttons.
*/
_overflowMenuButtons?: Array<IToolboxNativeButton>;
/**
* Whether the recoding button should be enabled or not.
*/
@@ -76,11 +80,6 @@ interface IProps {
*/
_shouldDisplayReactionsButtons: boolean;
/**
* The width of the screen.
*/
_width: number;
/**
* Used for hiding the dialog when the selection was completed.
*/
@@ -128,11 +127,8 @@ class OverflowMenu extends PureComponent<IProps, IState> {
_isBreakoutRoomsSupported,
_isSpeakerStatsDisabled,
_isSharedVideoEnabled,
_shouldDisplayReactionsButtons,
_width,
dispatch
} = this.props;
const toolbarButtons = getMovableButtons(_width);
const buttonProps = {
afterClick: this._onCancel,
@@ -156,18 +152,12 @@ class OverflowMenu extends PureComponent<IProps, IState> {
return (
<BottomSheet
renderFooter = { _shouldDisplayReactionsButtons && !toolbarButtons.has('raisehand')
? this._renderReactionMenu
: undefined }>
{ this._renderCustomOverflowMenuButtons(topButtonProps) }
renderFooter = { this._renderReactionMenu }>
<Divider style = { styles.divider as ViewStyle } />
<OpenCarmodeButton { ...topButtonProps } />
<AudioOnlyButton { ...buttonProps } />
{
!_shouldDisplayReactionsButtons && !toolbarButtons.has('raisehand')
&& <RaiseHandButton { ...buttonProps } />
}
{ this._renderRaiseHandButton(buttonProps) }
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
<SecurityDialogButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
@@ -176,9 +166,8 @@ class OverflowMenu extends PureComponent<IProps, IState> {
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{_isSharedVideoEnabled && <SharedVideoButton { ...buttonProps } />}
{!toolbarButtons.has('screensharing') && <ScreenSharingButton { ...buttonProps } />}
{ this._renderOverflowMenuButtons(topButtonProps) }
{!_isSpeakerStatsDisabled && <SpeakerStatsButton { ...buttonProps } />}
{!toolbarButtons.has('tileview') && <TileViewButton { ...buttonProps } />}
{_isBreakoutRoomsSupported && <BreakoutRoomsButton { ...buttonProps } />}
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
@@ -205,42 +194,72 @@ class OverflowMenu extends PureComponent<IProps, IState> {
* @returns {React.ReactElement}
*/
_renderReactionMenu() {
return (
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { true } />
);
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<ReactionMenu
onCancel = { this._onCancel }
overflowMenu = { true } />
);
}
}
/**
* Function to render the reaction menu as the footer of the bottom sheet.
*
* @param {Object} buttonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderRaiseHandButton(buttonProps: Object) {
const { _mainMenuButtons, _shouldDisplayReactionsButtons } = this.props;
// @ts-ignore
const isRaiseHandInMainMenu = _mainMenuButtons?.some(item => item.key === 'raisehand');
if (!_shouldDisplayReactionsButtons && !isRaiseHandInMainMenu) {
return (
<RaiseHandButton { ...buttonProps } />
);
}
}
/**
* Function to render the custom buttons for the overflow menu.
*
* @param {Object} topButtonProps - Button properties.
* @param {Object} topButtonProps - Styling button properties.
* @returns {React.ReactElement}
*/
_renderCustomOverflowMenuButtons(topButtonProps: Object) {
const { _customToolbarButtons, dispatch } = this.props;
_renderOverflowMenuButtons(topButtonProps: Object) {
const { _overflowMenuButtons, dispatch } = this.props;
if (!_customToolbarButtons?.length) {
if (!_overflowMenuButtons?.length) {
return;
}
return (
<>
{
_customToolbarButtons.map(({ id, text, icon, backgroundColor }) => (
<CustomOptionButton
{ ...topButtonProps }
backgroundColor = { backgroundColor }
/* eslint-disable react/jsx-no-bind */
handleClick = { () =>
dispatch(customButtonPressed(id, text))
}
icon = { icon }
isToolboxButton = { false }
key = { id }
text = { text } />
))
_overflowMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => {
if (key === 'raisehand') {
return null;
}
return (
<Content
{ ...topButtonProps }
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { false }
key = { key }
text = { text } />
);
})
}
<Divider style = { styles.divider as ViewStyle } />
</>
@@ -257,16 +276,38 @@ class OverflowMenu extends PureComponent<IProps, IState> {
*/
function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
const { customToolbarButtons } = state['features/base/config'];
return {
_customToolbarButtons: customToolbarButtons,
_isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(),
_isSharedVideoEnabled: isSharedVideoEnabled(state),
_isSpeakerStatsDisabled: isSpeakerStatsDisabled(state),
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state),
_width: state['features/base/responsive-ui'].clientWidth
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state)
};
}
export default connect(_mapStateToProps)(OverflowMenu);
export default connect(_mapStateToProps)(props => {
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons, overflowMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
mainToolbarButtonsThresholds,
toolbarButtons
});
return (
<OverflowMenu
// @ts-ignore
{ ... props }
_mainMenuButtons = { mainMenuButtons }
_overflowMenuButtons = { overflowMenuButtons } />
);
});

View File

@@ -25,6 +25,8 @@ class OverflowMenuButton extends AbstractButton<AbstractButtonProps> {
* @returns {void}
*/
_handleClick() {
// @ts-ignore
this.props.dispatch(openSheet(OverflowMenu));
}
}

View File

@@ -1,55 +1,29 @@
import React from 'react';
import { View, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import ColorSchemeRegistry from '../../../base/color-scheme/ColorSchemeRegistry';
import Platform from '../../../base/react/Platform.native';
import ChatButton from '../../../chat/components/native/ChatButton';
import ReactionsMenuButton from '../../../reactions/components/native/ReactionsMenuButton';
import { shouldDisplayReactionsButtons } from '../../../reactions/functions.any';
import TileViewButton from '../../../video-layout/components/TileViewButton';
import { iAmVisitor } from '../../../visitors/functions';
import { customButtonPressed } from '../../actions.native';
import { getMovableButtons, isToolboxVisible } from '../../functions.native';
import HangupButton from '../HangupButton';
import { getVisibleNativeButtons, isToolboxVisible } from '../../functions.native';
import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import AudioMuteButton from './AudioMuteButton';
import CustomOptionButton from './CustomOptionButton';
import HangupMenuButton from './HangupMenuButton';
import OverflowMenuButton from './OverflowMenuButton';
import RaiseHandButton from './RaiseHandButton';
import ScreenSharingButton from './ScreenSharingButton';
import VideoMuteButton from './VideoMuteButton';
import styles from './styles';
/**
* The type of {@link Toolbox}'s React {@code Component} props.
*/
interface IProps {
/**
* Custom Toolbar buttons.
*/
_customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>;
/**
* Whether the end conference feature is supported.
*/
_endConferenceSupported: boolean;
/**
* Whether we are in visitors mode.
*/
_iAmVisitor: boolean;
/**
* Whether or not any reactions buttons should be visible.
*/
_shouldDisplayReactionsButtons: boolean;
/**
* The color-schemed stylesheet of the feature.
*/
@@ -60,11 +34,6 @@ interface IProps {
*/
_visible: boolean;
/**
* The width of the screen.
*/
_width: number;
/**
* Redux store dispatch method.
*/
@@ -79,13 +48,9 @@ interface IProps {
*/
function Toolbox(props: IProps) {
const {
_customToolbarButtons,
_endConferenceSupported,
_iAmVisitor,
_shouldDisplayReactionsButtons,
_styles,
_visible,
_width,
dispatch
} = props;
@@ -93,41 +58,47 @@ function Toolbox(props: IProps) {
return null;
}
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const {
mainToolbarButtonsThresholds,
toolbarButtons
} = useSelector((state: IReduxState) => state['features/toolbox']);
const allButtons = useNativeToolboxButtons(customToolbarButtons);
const { mainMenuButtons } = getVisibleNativeButtons({
allButtons,
clientWidth,
mainToolbarButtonsThresholds,
toolbarButtons
});
const bottomEdge = Platform.OS === 'ios' && _visible;
const { buttonStylesBorderless, hangupButtonStyles, toggledButtonStyles } = _styles;
const additionalButtons = getMovableButtons(_width);
const backgroundToggledStyle = {
...toggledButtonStyles,
style: [
toggledButtonStyles.style,
_styles.backgroundToggle
]
};
const { buttonStylesBorderless, hangupButtonStyles } = _styles;
const style = { ...styles.toolbox };
// we have only hangup and raisehand button in _iAmVisitor mode
// We have only hangup and raisehand button in _iAmVisitor mode
if (_iAmVisitor) {
additionalButtons.add('raisehand');
style.justifyContent = 'center';
}
const renderCustomToolboxButtons = () => {
if (!_customToolbarButtons?.length) {
const renderToolboxButtons = () => {
if (!mainMenuButtons?.length) {
return;
}
return (
<>
{
_customToolbarButtons.map(({ backgroundColor, id, text, icon }) => (
<CustomOptionButton
backgroundColor = { backgroundColor }
mainMenuButtons?.map(({ Content, key, text, ...rest }: IToolboxNativeButton) => (
<Content
{ ...rest }
/* eslint-disable react/jsx-no-bind */
handleClick = { () => dispatch(customButtonPressed(id, text)) }
icon = { icon }
handleClick = { () => dispatch(customButtonPressed(key, text)) }
isToolboxButton = { true }
key = { id } />
key = { key }
styles = { key === 'hangup' ? hangupButtonStyles : buttonStylesBorderless } />
))
}
</>
@@ -144,44 +115,7 @@ function Toolbox(props: IProps) {
edges = { [ bottomEdge && 'bottom' ].filter(Boolean) }
pointerEvents = 'box-none'
style = { style as ViewStyle }>
{
_customToolbarButtons
? <>
{ renderCustomToolboxButtons() }
{ !_iAmVisitor && <OverflowMenuButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } /> }
</>
: <>
{!_iAmVisitor && <AudioMuteButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } />}
{!_iAmVisitor && <VideoMuteButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } />}
{additionalButtons.has('chat')
&& <ChatButton
styles = { buttonStylesBorderless }
toggledStyles = { backgroundToggledStyle } />}
{!_iAmVisitor && additionalButtons.has('screensharing')
&& <ScreenSharingButton styles = { buttonStylesBorderless } />}
{additionalButtons.has('raisehand') && (_shouldDisplayReactionsButtons
? <ReactionsMenuButton
styles = { buttonStylesBorderless }
toggledStyles = { backgroundToggledStyle } />
: <RaiseHandButton
styles = { buttonStylesBorderless }
toggledStyles = { backgroundToggledStyle } />)}
{additionalButtons.has('tileview')
&& <TileViewButton styles = { buttonStylesBorderless } />}
{!_iAmVisitor && <OverflowMenuButton
styles = { buttonStylesBorderless }
toggledStyles = { toggledButtonStyles } />}
{ _endConferenceSupported
? <HangupMenuButton />
: <HangupButton styles = { hangupButtonStyles } />}
</>
}
{ renderToolboxButtons() }
</SafeAreaView>
</View>
);
@@ -197,17 +131,10 @@ function Toolbox(props: IProps) {
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
const endConferenceSupported = conference?.isEndConferenceSupported();
return {
_customToolbarButtons: state['features/base/config']?.customToolbarButtons,
_endConferenceSupported: Boolean(endConferenceSupported),
_iAmVisitor: iAmVisitor(state),
_styles: ColorSchemeRegistry.get(state, 'Toolbox'),
_visible: isToolboxVisible(state),
_iAmVisitor: iAmVisitor(state),
_width: state['features/base/responsive-ui'].clientWidth,
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state)
};
}

View File

@@ -1,4 +1,4 @@
import { ToolbarButton } from './types';
import { NativeToolbarButton, ToolbarButton } from './types';
/**
* Thresholds for displaying toolbox buttons.
@@ -34,6 +34,32 @@ export const THRESHOLDS = [
}
];
/**
* Thresholds for displaying native toolbox buttons.
*/
export const NATIVE_THRESHOLDS = [
{
width: 560,
order: [ 'microphone', 'camera', 'chat', 'screensharing', 'raisehand', 'tileview', 'overflowmenu', 'hangup' ]
},
{
width: 500,
order: [ 'microphone', 'camera', 'chat', 'raisehand', 'tileview', 'overflowmenu', 'hangup' ]
},
{
width: 440,
order: [ 'microphone', 'camera', 'chat', 'raisehand', 'overflowmenu', 'hangup' ]
},
{
width: 380,
order: [ 'microphone', 'camera', 'chat', 'overflowmenu', 'hangup' ]
},
{
width: 320,
order: [ 'microphone', 'camera', 'overflowmenu', 'hangup' ]
}
];
/**
* Main toolbar buttons priority used to determine which button should be picked to fill empty spaces for disabled
* buttons.
@@ -47,6 +73,8 @@ export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
'reactions',
'participants-pane',
'tileview',
'overflowmenu',
'hangup',
'invite',
'toggle-camera',
'videoquality',
@@ -128,17 +156,34 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'whiteboard'
];
/**
* The list of all possible native buttons.
*
* @protected
* @type Array<string>
*/
export const NATIVE_TOOLBAR_BUTTONS: NativeToolbarButton[] = [
'camera',
'chat',
'hangup',
'microphone',
'overflowmenu',
'raisehand',
'screensharing',
'tileview'
];
/**
* The toolbar buttons to show when in visitors mode.
*/
export const VISITORS_MODE_BUTTONS: ToolbarButton[] = [
'chat',
'closedcaptions',
'fullscreen',
'hangup',
'raisehand',
'settings',
'tileview',
'fullscreen',
'stats',
'tileview',
'videoquality'
];

View File

@@ -1,9 +1,13 @@
import { IReduxState } from '../app/types';
import { IStateful } from '../base/app/types';
import { isJwtFeatureEnabledStateless } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
import { IParticipantFeatures } from '../base/participants/types';
import { toState } from '../base/redux/functions';
import { iAmVisitor } from '../visitors/functions';
import { VISITORS_MODE_BUTTONS } from './constants';
/**
* Indicates if the audio mute button is disabled or not.
*
@@ -57,3 +61,41 @@ export function getJwtDisabledButtons(
return acc;
}
/**
* Returns the list of enabled toolbar buttons.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @param {string[]} definedToolbarButtons - The list of all possible buttons.
*
* @returns {Array<string>} - The list of enabled toolbar buttons.
*/
export function getToolbarButtons(stateful: IStateful, definedToolbarButtons: string[]): Array<string> {
const state = toState(stateful);
const { toolbarButtons, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => id);
let buttons = Array.isArray(toolbarButtons) ? toolbarButtons : definedToolbarButtons;
if (iAmVisitor(state)) {
buttons = VISITORS_MODE_BUTTONS.filter(button => buttons.indexOf(button) > -1);
}
if (customButtons) {
return [ ...buttons, ...customButtons ];
}
return buttons;
}
/**
* 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 isButtonEnabled(buttonName: string, state: IReduxState | Array<string>) {
const buttons = Array.isArray(state) ? state : state['features/toolbox'].toolbarButtons || [];
return buttons.includes(buttonName);
}

View File

@@ -7,53 +7,12 @@ import { getParticipantCountWithFake } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { isLocalVideoTrackDesktop } from '../base/tracks/functions.native';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY } from './constants';
import { isButtonEnabled } from './functions.any';
import { IGetVisibleNativeButtonsParams, IToolboxNativeButton } from './types';
export * from './functions.any';
const WIDTH = {
FIT_9_ICONS: 560,
FIT_8_ICONS: 500,
FIT_7_ICONS: 440,
FIT_6_ICONS: 380
};
/**
* Returns a set of the buttons that are shown in the toolbar
* but removed from the overflow menu, based on the width of the screen.
*
* @param {number} width - The width of the screen.
* @returns {Set}
*/
export function getMovableButtons(width: number): Set<string> {
let buttons: string[] = [];
switch (true) {
case width >= WIDTH.FIT_9_ICONS: {
buttons = [ 'chat', 'togglecamera', 'screensharing', 'raisehand', 'tileview' ];
break;
}
case width >= WIDTH.FIT_8_ICONS: {
buttons = [ 'chat', 'togglecamera', 'raisehand', 'tileview' ];
break;
}
case width >= WIDTH.FIT_7_ICONS: {
buttons = [ 'chat', 'togglecamera', 'raisehand' ];
break;
}
case width >= WIDTH.FIT_6_ICONS: {
buttons = [ 'chat', 'togglecamera' ];
break;
}
default: {
buttons = [ 'chat' ];
}
}
return new Set(buttons);
}
/**
* Indicates if the desktop share button is disabled or not.
*
@@ -99,3 +58,64 @@ export function isVideoMuteButtonDisabled(state: IReduxState) {
return !hasAvailableDevices(state, 'videoInput')
|| (unmuteBlocked && Boolean(muted));
}
/**
* Returns all buttons that need to be rendered.
*
* @param {IGetVisibleButtonsParams} params - The parameters needed to extract the visible buttons.
* @returns {Object} - The visible buttons arrays .
*/
export function getVisibleNativeButtons({ allButtons, clientWidth, mainToolbarButtonsThresholds, toolbarButtons
}: IGetVisibleNativeButtonsParams) {
const filteredButtons = Object.keys(allButtons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be coming from config.mainToolbarButtons override
&& isButtonEnabled(key, toolbarButtons));
const { order } = mainToolbarButtonsThresholds.find(({ width }) => clientWidth > width)
|| mainToolbarButtonsThresholds[mainToolbarButtonsThresholds.length - 1];
const mainToolbarButtonKeysOrder = [
...order.filter(key => filteredButtons.includes(key)),
...MAIN_TOOLBAR_BUTTONS_PRIORITY.filter(key => !order.includes(key) && filteredButtons.includes(key)),
...filteredButtons.filter(key => !order.includes(key) && !MAIN_TOOLBAR_BUTTONS_PRIORITY.includes(key))
];
const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length);
const overflowMenuButtons = filteredButtons.reduce((acc, key) => {
if (!mainButtonsKeys.includes(key)) {
acc.push(allButtons[key]);
}
return acc;
}, [] as IToolboxNativeButton[]);
// if we have 1 button in the overflow menu it is better to directly display it in the main toolbar by replacing
// the "More" menu button with it.
if (overflowMenuButtons.length === 1) {
const button = overflowMenuButtons.shift()?.key;
button && mainButtonsKeys.push(button);
}
const mainMenuButtons
= mainButtonsKeys.map(key => allButtons[key]).sort((a, b) => {
// Native toolbox includes hangup and overflowmenu button keys, too
// hangup goes last, overflowmenu goes second-to-last
if (a.key === 'hangup' || a.key === 'overflowmenu') {
return 1;
}
if (b.key === 'hangup' || b.key === 'overflowmenu') {
return -1;
}
return 0; // other buttons are sorted by priority
});
return {
mainMenuButtons,
overflowMenuButtons
};
}

View File

@@ -1,5 +1,5 @@
import { IReduxState } from '../app/types';
import { hasAvailableDevices } from '../base/devices/functions';
import { hasAvailableDevices } from '../base/devices/functions.web';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
@@ -7,7 +7,8 @@ import { isScreenMediaShared } from '../screen-share/functions';
import { isWhiteboardVisible } from '../whiteboard/functions';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY, TOOLBAR_TIMEOUT } from './constants';
import { IMainToolbarButtonThresholds, IToolboxButton, NOTIFY_CLICK_MODE } from './types';
import { isButtonEnabled } from './functions.any';
import { IGetVisibleButtonsParams, IToolboxButton, NOTIFY_CLICK_MODE } from './types';
export * from './functions.any';
@@ -22,19 +23,6 @@ export function getToolboxHeight() {
return toolbox?.clientHeight || 0;
}
/**
* 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 isButtonEnabled(buttonName: string, state: IReduxState | Array<string>) {
const buttons = Array.isArray(state) ? state : state['features/toolbox'].toolbarButtons || [];
return buttons.includes(buttonName);
}
/**
* Indicates if the toolbox is visible or not.
*
@@ -125,26 +113,6 @@ export function showOverflowDrawer(state: IReduxState) {
return state['features/toolbox'].overflowDrawer;
}
/**
* Returns true if the overflow menu button is displayed and false otherwise.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean} - True if the overflow menu button is displayed and false otherwise.
*/
export function showOverflowMenu(state: IReduxState) {
return state['features/toolbox'].overflowMenuVisible;
}
/**
* Indicates whether the toolbox is enabled or not.
*
* @param {IReduxState} state - The state from the Redux store.
* @returns {boolean}
*/
export function isToolboxEnabled(state: IReduxState) {
return state['features/toolbox'].enabled;
}
/**
* Returns the toolbar timeout from config or the default value.
*
@@ -176,15 +144,6 @@ function setButtonsNotifyClickMode(buttons: Object, buttonsWithNotifyClick: Map<
});
}
interface IGetVisibleButtonsParams {
allButtons: { [key: string]: IToolboxButton; };
buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
clientWidth: number;
jwtDisabledButtons: string[];
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[];
}
/**
* Returns all buttons that need to be rendered.
*
@@ -234,8 +193,10 @@ export function getVisibleButtons({
button && mainButtonsKeys.push(button);
}
const mainMenuButtons = mainButtonsKeys.map(key => allButtons[key]);
return {
mainMenuButtons: mainButtonsKeys.map(key => allButtons[key]),
mainMenuButtons,
overflowMenuButtons
};
}

View File

@@ -0,0 +1,193 @@
import { useSelector } from 'react-redux';
import ChatButton from '../chat/components/native/ChatButton';
import RaiseHandContainerButtons from '../reactions/components/native/RaiseHandContainerButtons';
import TileViewButton from '../video-layout/components/TileViewButton';
import { iAmVisitor } from '../visitors/functions';
import AudioMuteButton from './components/native/AudioMuteButton';
import CustomOptionButton from './components/native/CustomOptionButton';
import HangupContainerButtons from './components/native/HangupContainerButtons';
import OverflowMenuButton from './components/native/OverflowMenuButton';
import ScreenSharingButton from './components/native/ScreenSharingButton';
import VideoMuteButton from './components/native/VideoMuteButton';
import { isDesktopShareButtonDisabled } from './functions.native';
import { ICustomToolbarButton, IToolboxNativeButton, NativeToolbarButton } from './types';
const microphone = {
key: 'microphone',
Content: AudioMuteButton,
group: 0
};
const camera = {
key: 'camera',
Content: VideoMuteButton,
group: 0
};
const chat = {
key: 'chat',
Content: ChatButton,
group: 1
};
const screensharing = {
key: 'screensharing',
Content: ScreenSharingButton,
group: 1
};
const raisehand = {
key: 'raisehand',
Content: RaiseHandContainerButtons,
group: 2
};
const tileview = {
key: 'tileview',
Content: TileViewButton,
group: 2
};
const overflowmenu = {
key: 'overflowmenu',
Content: OverflowMenuButton,
group: 3
};
const hangup = {
key: 'hangup',
Content: HangupContainerButtons,
group: 3
};
/**
* A hook that returns the audio mute button.
*
* @returns {Object | undefined}
*/
function getAudioMuteButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return microphone;
}
}
/**
* A hook that returns the video mute button.
*
* @returns {Object | undefined}
*/
function getVideoMuteButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return camera;
}
}
/**
* A hook that returns the chat button.
*
* @returns {Object | undefined}
*/
function getChatButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return chat;
}
}
/**
* A hook that returns the screen sharing button.
*
* @returns {Object | undefined}
*/
function getScreenSharingButton() {
const _iAmVisitor = useSelector(iAmVisitor);
const _isScreenShareButtonDisabled = useSelector(isDesktopShareButtonDisabled);
if (!_isScreenShareButtonDisabled && !_iAmVisitor) {
return screensharing;
}
}
/**
* A hook that returns the tile view button.
*
* @returns {Object | undefined}
*/
function getTileViewButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return tileview;
}
}
/**
* A hook that returns the overflow menu button.
*
* @returns {Object | undefined}
*/
function getOverflowMenuButton() {
const _iAmVisitor = useSelector(iAmVisitor);
if (!_iAmVisitor) {
return overflowmenu;
}
}
/**
* Returns all buttons that could be rendered.
*
* @param {Object} _customToolbarButtons - An array containing custom buttons objects.
* @returns {Object} The button maps mainMenuButtons and overflowMenuButtons.
*/
export function useNativeToolboxButtons(
_customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxNativeButton; } {
const audioMuteButton = getAudioMuteButton();
const videoMuteButton = getVideoMuteButton();
const chatButton = getChatButton();
const screenSharingButton = getScreenSharingButton();
const tileViewButton = getTileViewButton();
const overflowMenuButton = getOverflowMenuButton();
const buttons: { [key in NativeToolbarButton]?: IToolboxNativeButton; } = {
microphone: audioMuteButton,
camera: videoMuteButton,
chat: chatButton,
screensharing: screenSharingButton,
raisehand,
tileview: tileViewButton,
overflowmenu: overflowMenuButton,
hangup
};
const buttonKeys = Object.keys(buttons) as NativeToolbarButton[];
buttonKeys.forEach(
key => typeof buttons[key] === 'undefined' && delete buttons[key]);
const customButtons = _customToolbarButtons?.reduce((prev, { backgroundColor, icon, id, text }) => {
prev[id] = {
backgroundColor,
key: id,
id,
Content: CustomOptionButton,
group: 4,
icon,
text
};
return prev;
}, {} as { [key: string]: ICustomToolbarButton; });
return {
...buttons,
...customButtons
};
}

View File

@@ -270,7 +270,7 @@ function useHelpButton() {
*/
export function useToolboxButtons(
_customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxButton; } {
const dekstopSharing = getDesktopSharingButton();
const desktopSharing = getDesktopSharingButton();
const toggleCameraButton = useToggleCameraButton();
const _fullscreen = getFullscreenButton();
const security = useSecurityDialogButton();
@@ -297,7 +297,7 @@ export function useToolboxButtons(
microphone,
camera,
profile,
desktop: dekstopSharing,
desktop: desktopSharing,
chat,
raisehand,
reactions,

View File

@@ -0,0 +1,46 @@
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 { SET_TOOLBAR_BUTTONS } from './actionTypes';
import { setMainToolbarThresholds } from './actions.native';
import { NATIVE_THRESHOLDS, NATIVE_TOOLBAR_BUTTONS } from './constants';
import { getToolbarButtons } from './functions.native';
/**
* Middleware which intercepts Toolbox actions to handle changes to the
* visibility timeout of the Toolbox.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case UPDATE_CONFIG:
case OVERWRITE_CONFIG:
case I_AM_VISITOR_MODE:
case SET_CONFIG: {
const result = next(action);
const { dispatch } = store;
const state = store.getState();
const toolbarButtons = getToolbarButtons(state, NATIVE_TOOLBAR_BUTTONS);
if (action.type !== I_AM_VISITOR_MODE) {
dispatch(setMainToolbarThresholds(NATIVE_THRESHOLDS));
}
dispatch({
type: SET_TOOLBAR_BUTTONS,
toolbarButtons
});
return result;
}
}
return next(action);
});

View File

@@ -1,12 +1,10 @@
import { batch } from 'react-redux';
import { AnyAction } from 'redux';
import { IReduxState } from '../app/types';
import { OVERWRITE_CONFIG, SET_CONFIG, UPDATE_CONFIG } from '../base/config/actionTypes';
import { NotifyClickButton } from '../base/config/configType';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
import { iAmVisitor } from '../visitors/functions';
import {
CLEAR_TOOLBOX_TIMEOUT,
@@ -17,7 +15,8 @@ import {
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import { setMainToolbarThresholds } from './actions.web';
import { TOOLBAR_BUTTONS, VISITORS_MODE_BUTTONS } from './constants';
import { THRESHOLDS, TOOLBAR_BUTTONS } from './constants';
import { getToolbarButtons } from './functions.web';
import { NOTIFY_CLICK_MODE } from './types';
import './subscriber.web';
@@ -55,7 +54,7 @@ MiddlewareRegistry.register(store => next => action => {
batch(() => {
if (action.type !== I_AM_VISITOR_MODE) {
dispatch(setMainToolbarThresholds());
dispatch(setMainToolbarThresholds(THRESHOLDS));
}
dispatch({
type: SET_BUTTONS_WITH_NOTIFY_CLICK,
@@ -69,7 +68,7 @@ MiddlewareRegistry.register(store => next => action => {
});
}
const toolbarButtons = _getToolbarButtons(state);
const toolbarButtons = getToolbarButtons(state, TOOLBAR_BUTTONS);
dispatch({
type: SET_TOOLBAR_BUTTONS,
@@ -171,25 +170,3 @@ function _buildButtonsArray(
return new Map([ ...customButtonsWithNotifyClick, ...buttons ]);
}
/**
* 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

@@ -18,7 +18,7 @@ import {
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
import { THRESHOLDS } from './constants';
import { NATIVE_THRESHOLDS, THRESHOLDS } from './constants';
import { IMainToolbarButtonThresholds, NOTIFY_CLICK_MODE } from './types';
/**
@@ -52,7 +52,7 @@ const INITIAL_STATE = {
/**
* The thresholds for screen size and visible main toolbar buttons.
*/
mainToolbarButtonsThresholds: THRESHOLDS,
mainToolbarButtonsThresholds: navigator.product === 'ReactNative' ? NATIVE_THRESHOLDS : THRESHOLDS,
participantMenuButtonsWithNotifyClick: new Map(),

View File

@@ -1,13 +1,21 @@
import { ComponentType } from 'react';
import { CustomOptionButton } from './components';
export interface IToolboxButton {
Content: ComponentType<any>;
group: number;
key: string;
}
export interface IToolboxNativeButton {
Content: ComponentType<any>;
backgroundColor?: string;
group: number;
icon?: string;
id?: string;
key: string;
text?: string;
}
export type ToolbarButton = 'camera' |
'chat' |
'closedcaptions' |
@@ -28,6 +36,7 @@ export type ToolbarButton = 'camera' |
'mute-everyone' |
'mute-video-everyone' |
'noisesuppression' |
'overflowmenu' |
'participants-pane' |
'profile' |
'raisehand' |
@@ -52,12 +61,12 @@ export enum NOTIFY_CLICK_MODE {
}
export type IMainToolbarButtonThresholds = Array<{
order: Array<ToolbarButton | string>;
order: Array<ToolbarButton | NativeToolbarButton | string>;
width: number;
}>;
export interface ICustomToolbarButton {
Content?: typeof CustomOptionButton;
Content?: ComponentType<any>;
backgroundColor?: string;
group?: number;
icon: string;
@@ -65,3 +74,28 @@ export interface ICustomToolbarButton {
key?: string;
text: string;
}
export type NativeToolbarButton = 'camera' |
'chat' |
'microphone' |
'raisehand' |
'screensharing' |
'tileview' |
'overflowmenu' |
'hangup';
export interface IGetVisibleNativeButtonsParams {
allButtons: { [key: string]: IToolboxNativeButton; };
clientWidth: number;
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[];
}
export interface IGetVisibleButtonsParams {
allButtons: { [key: string]: IToolboxButton; };
buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
clientWidth: number;
jwtDisabledButtons: string[];
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[];
}

View File

@@ -1,6 +1,5 @@
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { TRANSCRIBER_LEFT } from './actionTypes';
import './subscriber';
@@ -17,7 +16,7 @@ MiddlewareRegistry.register(({ dispatch }) => next => action => {
if (action.abruptly) {
dispatch(showErrorNotification({
titleKey: 'transcribing.failed'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
}
break;
}

View File

@@ -27,8 +27,6 @@ class GrantModeratorDialog extends AbstractGrantModeratorDialog {
</Dialog>
);
}
_onSubmit: () => boolean;
}
export default translate(connect(abstractMapStateToProps)(GrantModeratorDialog));

View File

@@ -105,7 +105,7 @@ function _inviteRooms(rooms: ISipRoom[], conference: IJitsiConference, dispatch:
dispatch(showErrorNotification({
descriptionKey: 'videoSIPGW.errorInvite',
titleKey: 'videoSIPGW.errorInviteTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}));
return;
}
@@ -159,14 +159,14 @@ function _sessionStateChanged(
displayName: event.displayName
},
descriptionKey: 'videoSIPGW.errorInviteFailed'
}, NOTIFICATION_TIMEOUT_TYPE.LONG);
});
}
case JitsiSIPVideoGWStatus.STATE_OFF: {
if (event.failureReason === JitsiSIPVideoGWStatus.STATUS_BUSY) {
return showErrorNotification({
descriptionKey: 'videoSIPGW.busy',
titleKey: 'videoSIPGW.busyTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG);
});
} else if (event.failureReason) {
logger.error(`Unknown sip videogw error ${event.newState} ${
event.failureReason}`);

View File

@@ -1,7 +1,10 @@
local jid = require 'util.jid';
local json = require 'cjson.safe';
local queue = require "util.queue";
local uuid_gen = require "util.uuid".generate;
local main_util = module:require "util";
local ends_with = main_util.ends_with;
local get_room_from_jid = main_util.get_room_from_jid;
local is_healthcheck_room = main_util.is_healthcheck_room;
local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite;
local presence_check_status = main_util.presence_check_status;
@@ -13,9 +16,15 @@ end
local QUEUE_MAX_SIZE = 500;
-- Module that generates a unique meetingId, attaches it to the room
-- and adds it to all disco info form data (when room is queried or in the
-- initial room owner config)
-- Common module for all logic that can be loaded under the conference muc component.
--
-- This module:
-- a) Generates a unique meetingId, attaches it to the room and adds it to all disco info form data
-- (when room is queried or in the initial room owner config).
-- b) Updates user region (obtain it from the incoming http headers) in the occupant's presence on pre-join.
-- c) Avoids any participant joining the room in the interval between creating the room and jicofo entering the room.
-- d) Removes any nick that maybe set to messages being sent to the room.
-- e) Fires event for received endpoint messages (optimization to decode them once).
-- Hook to assign meetingId for new rooms
module:hook("muc-room-created", function(event)
@@ -149,3 +158,82 @@ module:hook('jicofo-unlock-room', handle_jicofo_unlock);
module:hook("muc-occupant-groupchat", function(event)
event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
end, 45); -- prosody check is prio 50, we want to run after it
module:hook('message/bare', function(event)
local stanza = event.stanza;
if stanza.attr.type ~= 'groupchat' then
return nil;
end
-- we are interested in all messages without a body
local body = stanza:get_child('body')
if body then
return;
end
local room = get_room_from_jid(stanza.attr.to);
if not room then
module:log('warn', 'No room found found for %s', stanza.attr.to);
return;
end
local occupant_jid = stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant sending msg %s was not found in room %s", occupant_jid, room.jid)
return;
end
local json_message = stanza:get_child_text('json-message', 'http://jitsi.org/jitmeet');
if not json_message then
return;
end
-- TODO: add optimization by moving type and certain fields like is_interim as attribute on 'json-message'
-- using string find is roughly 70x faster than json decode for checking the value
if string.find(json_message, '"is_interim":true', 1, true) then
return;
end
local msg_obj, error = json.decode(json_message);
if error then
module:log('error', 'Error decoding data error:%s Sender: %s to:%s', error, stanza.attr.from, stanza.attr.to);
return true;
end
if msg_obj.transcript ~= nil then
local transcription = msg_obj;
-- in case of the string matching optimization above failed
if transcription.is_interim then
return;
end
-- TODO what if we have multiple alternative transcriptions not just 1
local text_message = transcription.transcript[1].text;
--do not send empty messages
if text_message == '' then
return;
end
local user_id = transcription.participant.id;
local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id);
transcription.jid = who and who.jid;
transcription.session_id = room._data.meetingId;
local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
transcription.fqn = tenant..'/'..conference_name;
transcription.customer_id = id;
return module:fire_event('jitsi-transcript-received', {
room = room, occupant = occupant, transcription = transcription, stanza = stanza });
end
return module:fire_event('jitsi-endpoint-message-received', {
room = room, occupant = occupant, message = msg_obj,
origin = event.origin,
stanza = stanza, raw_message = json_message });
end);

View File

@@ -14,31 +14,6 @@ local is_healthcheck_room = util.is_healthcheck_room;
local POLLS_LIMIT = 128;
local POLL_PAYLOAD_LIMIT = 1024;
-- Checks if the given stanza contains a JSON message,
-- and that the message type pertains to the polls feature.
-- If yes, returns the parsed message. Otherwise, returns nil.
local function get_poll_message(stanza)
if stanza.attr.type ~= "groupchat" then
return nil;
end
local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet");
if json_data == nil then
return nil;
end
if string.len(json_data) >= POLL_PAYLOAD_LIMIT then
module:log('error', 'Poll payload too large, discarding. Sender: %s to:%s', stanza.attr.from, stanza.attr.to);
return nil;
end
local data, error = json.decode(json_data);
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
if error then
module:log('error', 'Error decoding data error:%s', error);
end
return nil;
end
return data;
end
-- Logs a warning and returns true if a room does not
-- have poll data associated with it.
local function check_polls(room)
@@ -87,21 +62,22 @@ end);
-- by listening to "new-poll" and "answer-poll" messages,
-- and updating the room poll data accordingly.
-- This mirrors the client-side poll update logic.
module:hook("message/bare", function(event)
local data = get_poll_message(event.stanza);
if data == nil then return end
module:hook('jitsi-endpoint-message-received', function(event)
local data, error, occupant, room, origin, stanza
= event.message, event.error, event.occupant, event.room, event.origin, event.stanza;
local room = muc.get_room_from_jid(event.stanza.attr.to);
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
return;
end
if string.len(event.raw_message) >= POLL_PAYLOAD_LIMIT then
module:log('error', 'Poll payload too large, discarding. Sender: %s to:%s', stanza.attr.from, stanza.attr.to);
return nil;
end
if data.type == "new-poll" then
if check_polls(room) then return end
local occupant_jid = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid)
return
end
local poll_creator = get_occupant_details(occupant)
if not poll_creator then
module:log("error", "Cannot retrieve poll creator id and name for %s from %s", occupant.jid, room.jid)
@@ -115,7 +91,7 @@ module:hook("message/bare", function(event)
if room.polls.by_id[data.pollId] ~= nil then
module:log("error", "Poll already exists: %s", data.pollId);
event.origin.send(st.error_reply(event.stanza, 'cancel', 'not-allowed', 'Poll already exists'));
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Poll already exists'));
return true;
end
@@ -150,16 +126,9 @@ module:hook("message/bare", function(event)
}
}
module:fire_event("poll-created", pollData);
elseif data.type == "answer-poll" then
if check_polls(room) then return end
local occupant_jid = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant %s does not exists for room %s", occupant_jid, room.jid)
return
end
local poll = room.polls.by_id[data.pollId];
if poll == nil then
module:log("warn", "answering inexistent poll");

View File

@@ -5,16 +5,13 @@ local update_presence_identity = module:require "util".update_presence_identity;
-- values are set in the session, then insert them into the presence messages
-- for that session.
function on_message(event)
if event and event["stanza"] then
if event.origin and event.origin.jitsi_meet_context_user then
local stanza, session = event.stanza, event.origin;
if stanza and session then
update_presence_identity(
event.stanza,
event.origin.jitsi_meet_context_user,
event.origin.jitsi_meet_context_group
stanza,
session.jitsi_meet_context_user,
session.jitsi_meet_context_group
);
end
end
end

View File

@@ -535,17 +535,11 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
end
end
end);
host_module:hook("message/bare", function(event)
local stanza = event.stanza;
if stanza.attr.type ~= "groupchat" then
return;
end
local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet");
if json_data == nil then
return;
end
local data, error = json.decode(json_data);
host_module:hook('jitsi-endpoint-message-received', function(event)
local data, error, occupant, room, stanza
= event.message, event.error, event.occupant, event.room, event.stanza;
if not data or data.type ~= 'visitors'
or (data.action ~= "promotion-response" and data.action ~= "demote-request") then
if error then
@@ -554,17 +548,9 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
return;
end
local room = get_room_from_jid(event.stanza.attr.to);
local occupant_jid = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid)
return
end
if occupant.role ~= 'moderator' then
module:log('error', 'Occupant %s sending response message but not moderator in room %s',
occupant_jid, room.jid);
occupant.jid, room.jid);
return false;
end
@@ -590,7 +576,6 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
end
end
end
else
if data.id then
process_promotion_response(room, data.id, data.approved and 'true' or 'false');
@@ -604,6 +589,7 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
return true; -- halt processing, but return true that we handled it
end);
if visitors_queue_service then
host_module:hook('muc-room-created', function (event)
local room = event.room;

View File

@@ -5,6 +5,7 @@ local timer = require "util.timer";
local http = require "net.http";
local cache = require "util.cache";
local array = require "util.array";
local is_set = require 'util.set'.is_set;
local http_timeout = 30;
local have_async, async = pcall(require, "util.async");
@@ -32,6 +33,7 @@ local roomless_iqs = {};
local OUTBOUND_SIP_JIBRI_PREFIXES = { 'outbound-sip-jibri@', 'sipjibriouta@', 'sipjibrioutb@' };
local INBOUND_SIP_JIBRI_PREFIXES = { 'inbound-sip-jibri@', 'sipjibriina@', 'sipjibriina@' };
local RECORDER_PREFIXES = module:get_option_inherited_set('recorder_prefixes', { 'recorder@recorder.', 'jibria@recorder.', 'jibrib@recorder.' });
local TRANSCRIBER_PREFIXES = module:get_option_inherited_set('transcriber_prefixes', { 'transcriber@recorder.', 'transcribera@recorder.', 'transcriberb@recorder.' });
local split_subdomain_cache = cache.new(1000);
local extract_subdomain_cache = cache.new(1000);
@@ -206,8 +208,7 @@ end
-- @param creator_group the group of the user who created the user which
-- presence we are updating (this is the poltergeist case, where a user creates
-- a poltergeist), optional.
function update_presence_identity(
stanza, user, group, creator_user, creator_group)
function update_presence_identity(stanza, user, group, creator_user, creator_group)
-- First remove any 'identity' element if it already
-- exists, so it cannot be spoofed by a client
@@ -220,7 +221,11 @@ function update_presence_identity(
end
return tag
end
)
);
if not user then
return;
end
stanza:tag("identity"):tag("user");
for k, v in pairs(user) do
@@ -267,17 +272,18 @@ function is_feature_allowed(ft, features, granted_features, is_moderator)
end
--- Extracts the subdomain and room name from internal jid node [foo]room1
-- @return subdomain(optional, if extracted or nil), the room name
-- @return subdomain(optional, if extracted or nil), the room name, the customer_id in case of vpaas
function extract_subdomain(room_node)
local ret = extract_subdomain_cache:get(room_node);
if ret then
return ret.subdomain, ret.room;
return ret.subdomain, ret.room, ret.customer_id;
end
local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$");
local cache_value = {subdomain=subdomain, room=room_name};
local _, customer_id = subdomain and subdomain:match("^(vpaas%-magic%-cookie%-)(.*)$") or nil, nil;
local cache_value = { subdomain=subdomain, room=room_name, customer_id=customer_id };
extract_subdomain_cache:set(room_node, cache_value);
return subdomain, room_name;
return subdomain, room_name, customer_id;
end
function starts_with(str, start)
@@ -288,14 +294,25 @@ function starts_with(str, start)
end
function starts_with_one_of(str, prefixes)
if not str then
if not str or not prefixes then
return false;
end
for i=1,#prefixes do
if starts_with(str, prefixes[i]) then
return prefixes[i];
if is_set(prefixes) then
-- set is a table with keys and value of true
for k, _ in prefixes:items() do
if starts_with(str, k) then
return k;
end
end
else
for _, v in pairs(prefixes) do
if starts_with(str, v) then
return v;
end
end
end
return false
end
@@ -481,6 +498,7 @@ function is_sip_jigasi(stanza)
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
end
-- This requires presence stanza being passed
function is_transcriber_jigasi(stanza)
if not stanza then
return false;
@@ -501,6 +519,9 @@ function is_transcriber_jigasi(stanza)
return false;
end
function is_transcriber(jid)
return starts_with_one_of(jid, TRANSCRIBER_PREFIXES);
end
function get_sip_jibri_email_prefix(email)
if not email then
@@ -616,6 +637,7 @@ return {
is_moderated = is_moderated;
is_sip_jibri_join = is_sip_jibri_join;
is_sip_jigasi = is_sip_jigasi;
is_transcriber = is_transcriber;
is_transcriber_jigasi = is_transcriber_jigasi;
is_vpaas = is_vpaas;
get_focus_occupant = get_focus_occupant;

View File

@@ -1,6 +1,7 @@
/* global APP $ */
import { multiremotebrowser } from '@wdio/globals';
import assert from 'assert';
import { Key } from 'webdriverio';
import { IConfig } from '../../react/features/base/config/configType';
@@ -10,9 +11,11 @@ import ChatPanel from '../pageobjects/ChatPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import InviteDialog from '../pageobjects/InviteDialog';
import LargeVideo from '../pageobjects/LargeVideo';
import LobbyScreen from '../pageobjects/LobbyScreen';
import Notifications from '../pageobjects/Notifications';
import ParticipantsPane from '../pageobjects/ParticipantsPane';
import PasswordDialog from '../pageobjects/PasswordDialog';
import PreJoinScreen from '../pageobjects/PreJoinScreen';
import SecurityDialog from '../pageobjects/SecurityDialog';
import SettingsDialog from '../pageobjects/SettingsDialog';
@@ -27,6 +30,13 @@ export const P2_DISPLAY_NAME = 'p2';
export const P3_DISPLAY_NAME = 'p3';
export const P4_DISPLAY_NAME = 'p4';
interface IWaitForSendReceiveDataOptions {
checkReceive?: boolean;
checkSend?: boolean;
msg?: string;
timeout?: number;
}
/**
* Participant.
*/
@@ -83,6 +93,24 @@ export class Participant {
this._jwt = jwt;
}
/**
* A wrapper for <tt>this.driver.execute</tt> that would catch errors, print them and throw them again.
*
* @param {string | ((...innerArgs: InnerArguments) => ReturnValue)} script - The script that will be executed.
* @param {any[]} args - The rest of the arguments.
* @returns {ReturnValue} - The result of the script.
*/
async execute<ReturnValue, InnerArguments extends any[]>(
script: string | ((...innerArgs: InnerArguments) => ReturnValue),
...args: InnerArguments): Promise<ReturnValue> {
try {
return await this.driver.execute(script, ...args);
} catch (error) {
console.error('An error occured while trying to execute a script: ', error);
throw error;
}
}
/**
* Returns participant endpoint ID.
*
@@ -90,8 +118,8 @@ export class Participant {
*/
async getEndpointId(): Promise<string> {
if (!this._endpointId) {
this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
return APP.conference.getMyUserId();
this._endpointId = await this.execute(() => { // eslint-disable-line arrow-body-style
return APP?.conference?.getMyUserId();
});
}
@@ -208,8 +236,8 @@ export class Participant {
const parallel = [];
parallel.push(driver.execute((name, sessionId, prefix) => {
APP.UI.dockToolbar(true);
parallel.push(this.execute((name, sessionId, prefix) => {
APP?.UI?.dockToolbar(true);
// disable keyframe animations (.fadeIn and .fadeOut classes)
$('<style>.notransition * { '
@@ -252,7 +280,7 @@ export class Participant {
*/
async waitForPageToLoad(): Promise<void> {
return this.driver.waitUntil(
() => this.driver.execute(() => document.readyState === 'complete'),
() => this.execute(() => document.readyState === 'complete'),
{
timeout: 30_000, // 30 seconds
timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
@@ -274,15 +302,15 @@ export class Participant {
/**
* Checks if the participant is in the meeting.
*/
async isInMuc() {
return await this.driver.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
isInMuc() {
return this.execute(() => typeof APP !== 'undefined' && APP.conference?.isJoined());
}
/**
* Checks if the participant is a moderator in the meeting.
*/
async isModerator() {
return await this.driver.execute(() => typeof APP !== 'undefined'
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
}
@@ -290,7 +318,7 @@ export class Participant {
* Checks if the meeting supports breakout rooms.
*/
async isBreakoutRoomsSupported() {
return await this.driver.execute(() => typeof APP !== 'undefined'
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
}
@@ -298,7 +326,7 @@ export class Participant {
* Checks if the participant is in breakout room.
*/
async isInBreakoutRoom() {
return await this.driver.execute(() => typeof APP !== 'undefined'
return await this.execute(() => typeof APP !== 'undefined'
&& APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
}
@@ -322,60 +350,59 @@ export class Participant {
*
* @returns {Promise<void>}
*/
async waitForIceConnected(): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() =>
driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
waitForIceConnected(): Promise<void> {
return this.driver.waitUntil(() =>
this.execute(() => APP?.conference?.getConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
});
}
/**
* Waits for send and receive data.
* Waits for ICE to get connected on the p2p connection.
*
* @returns {Promise<void>}
*/
async waitForSendReceiveData(
timeout = 15_000, msg = `expected to receive/send data in 15s for ${this.name}`): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() => driver.execute(() => {
const stats = APP.conference.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
uploadBitrate: bitrateMap.upload || 0,
downloadBitrate: bitrateMap.download || 0
};
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
}), {
timeout,
timeoutMsg: msg
waitForP2PIceConnected(): Promise<void> {
return this.driver.waitUntil(() =>
this.execute(() => APP?.conference?.getP2PConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: `expected P2P ICE to be connected for 15s for ${this.name}`
});
}
/**
* Waits for send and receive data.
*
* @param {Object} options
* @param {boolean} options.checkSend - If true we will chec
* @returns {Promise<void>}
*/
async waitForSendData(
timeout = 15_000, msg = `expected to send data in 15s for ${this.name}`): Promise<void> {
const driver = this.driver;
waitForSendReceiveData({
checkSend = true,
checkReceive = true,
timeout = 15_000,
msg
} = {} as IWaitForSendReceiveDataOptions): Promise<void> {
if (!checkSend && !checkReceive) {
return Promise.resolve();
}
return driver.waitUntil(() => driver.execute(() => {
const stats = APP.conference.getStats();
const lMsg = msg ?? `expected to ${
checkSend && checkReceive ? 'receive/send' : checkSend ? 'send' : 'receive'} data in 15s for ${this.name}`;
return this.driver.waitUntil(() => this.execute((pCheckSend: boolean, pCheckReceive: boolean) => {
const stats = APP?.conference?.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
uploadBitrate: bitrateMap.upload || 0
uploadBitrate: bitrateMap.upload || 0,
downloadBitrate: bitrateMap.download || 0
};
return rtpStats.uploadBitrate > 0;
}), {
return (rtpStats.uploadBitrate > 0 || !pCheckSend) && (rtpStats.downloadBitrate > 0 || !pCheckReceive);
}, checkSend, checkReceive), {
timeout,
timeoutMsg: msg
timeoutMsg: lMsg
});
}
@@ -385,11 +412,11 @@ export class Participant {
* @param {number} number - The number of remote streams to wait for.
* @returns {Promise<void>}
*/
waitForRemoteStreams(number: number): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() =>
driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
async waitForRemoteStreams(number: number): Promise<void> {
return await this.driver.waitUntil(async () => await this.execute(
count => (APP?.conference?.getNumberOfParticipantsWithTracks() ?? -1) >= count,
number
), {
timeout: 15_000,
timeoutMsg: `expected number of remote streams:${number} in 15s for ${this.name}`
});
@@ -403,12 +430,12 @@ export class Participant {
* @returns {Promise<void>}
*/
waitForParticipants(number: number, msg?: string): Promise<void> {
const driver = this.driver;
return driver.waitUntil(() => driver.execute(count => APP.conference.listMembers().length === count, number), {
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
});
return this.driver.waitUntil(
() => this.execute(count => (APP?.conference?.listMembers()?.length ?? -1) === count, number),
{
timeout: 15_000,
timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
});
}
/**
@@ -470,6 +497,15 @@ export class Participant {
return new ParticipantsPane(this);
}
/**
* Returns the large video page object.
*
* @returns {LargeVideo}
*/
getLargeVideo(): LargeVideo {
return new LargeVideo(this);
}
/**
* Returns the videoQuality Dialog.
*
@@ -497,6 +533,13 @@ export class Participant {
return new SettingsDialog(this);
}
/**
* Returns the password dialog.
*/
getPasswordDialog(): PasswordDialog {
return new PasswordDialog(this);
}
/**
* Returns the prejoin screen.
*/
@@ -546,7 +589,7 @@ export class Participant {
}
// do a hangup, to make sure unavailable presence is sent
await this.driver.execute(() => typeof APP !== 'undefined' && APP?.conference?.hangup());
await this.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
@@ -608,29 +651,6 @@ export class Participant {
return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
}
/**
* Gets avatar SRC attribute for the one displayed on large video.
*/
async getLargeVideoAvatar() {
const avatar = this.driver.$('//img[@id="dominantSpeakerAvatar"]');
return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
}
/**
* Returns resource part of the JID of the user who is currently displayed in the large video area.
*/
async getLargeVideoResource() {
return await this.driver.execute(() => APP.UI.getLargeVideoID());
}
/**
* Returns the source of the large video currently shown.
*/
async getLargeVideoId() {
return this.driver.execute('return document.getElementById("largeVideo").srcObject.id');
}
/**
* Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
* There are 3 options for avatar:
@@ -700,29 +720,136 @@ export class Participant {
return this.driver.$('div[data-testid="dialog.leaveReason"]').isDisplayed();
}
/**
* Returns the audio level for a participant.
*
* @param observer
* @param participant
* @return
*/
async getRemoteAudioLevel(p: Participant) {
const jid = await p.getEndpointId();
return await this.execute(id => {
const level = APP?.conference?.getPeerSSRCAudioLevel(id);
return level ? level.toFixed(2) : null;
}, jid);
}
/**
* For the participant to have his audio muted/unmuted from given observer's
* perspective. The method will fail the test if something goes wrong or
* the audio muted status is different than the expected one. We wait up to
* 3 seconds for the expected status to appear.
*
* @param testee - instance of the participant for whom we're checking the audio muted status.
* @param muted - <tt>true</tt> to wait for audio muted status or <tt>false</tt> to wait for the participant to
* unmute.
*/
async waitForAudioMuted(testee: Participant, muted: boolean): Promise<void> {
// Waits for the correct icon
await this.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, !muted);
// Extended timeout for 'unmuted' to make tests more resilient to
// unexpected glitches.
const timeout = muted ? 3_000 : 6_000;
// Give it 3 seconds to not get any audio or to receive some
// depending on "muted" argument
try {
await this.driver.waitUntil(async () => {
const audioLevel = await this.getRemoteAudioLevel(testee);
if (muted) {
if (audioLevel !== null && audioLevel > 0.1) {
console.log(`muted exiting on: ${audioLevel}`);
return true;
}
return false;
}
// When testing for unmuted we wait for first sound
if (audioLevel !== null && audioLevel > 0.1) {
console.log(`unmuted exiting on: ${audioLevel}`);
return true;
}
return false;
},
{ timeout });
// When testing for muted we don't want to have
// the condition succeeded
if (muted) {
const name = await testee.displayName;
assert.fail(`There was some sound coming from muted: '${name}'`);
} // else we're good for unmuted participant
} catch (_timeoutE) {
if (!muted) {
const name = await testee.displayName;
assert.fail(`There was no sound from unmuted: '${name}'`);
} // else we're good for muted participant
}
}
/**
* Waits for remote video state - receiving and displayed.
* @param endpointId
* @param reverse
*/
async waitForRemoteVideo(endpointId: string) {
await this.driver.waitUntil(async () =>
await this.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
});
async waitForRemoteVideo(endpointId: string, reverse = false) {
if (reverse) {
await this.driver.waitUntil(async () =>
!await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && !await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.displayName}`
});
} else {
await this.driver.waitUntil(async () =>
await this.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`),
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.displayName}`
});
}
}
/**
* Waits for ninja icon to be displayed.
* @param endpointId
* @param endpointId When no endpoint id is passed we check for any ninja icon.
*/
async waitForNinjaIcon(endpointId: string) {
await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000,
timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
});
async waitForNinjaIcon(endpointId?: string) {
if (endpointId) {
await this.driver.$(`//span[@id='participant_${endpointId}']//span[@class='connection_ninja']`)
.waitForDisplayed({
timeout: 15_000,
timeoutMsg: `expected ninja icon for ${endpointId} to be displayed in 15s by ${this.name}`
});
} else {
await this.driver.$('//span[contains(@class,"videocontainer")]//span[contains(@class,"connection_ninja")]')
.waitForDisplayed({
timeout: 5_000,
timeoutMsg: `expected ninja icon to be displayed in 5s by ${this.displayName}`
});
}
}
/**
* Waits for dominant speaker icon to appear in remote video of a participant.
* @param endpointId the endpoint ID of the participant whose dominant speaker icon status will be checked.
*/
waitForDominantSpeaker(endpointId: string) {
return this.driver.$(`//span[@id="participant_${endpointId}" and contains(@class, "dominant-speaker")]`)
.waitForDisplayed({ timeout: 5_000 });
}
}

View File

@@ -53,6 +53,49 @@ export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptio
]);
}
/**
* Creates the first participant instance or prepares one for re-joining.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export function joinFirstParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
return joinTheModeratorAsP1(ctx, options);
}
/**
* Creates the second participant instance or prepares one for re-joining.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export function joinSecondParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
return _joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
});
}
/**
* Creates the third participant instance or prepares one for re-joining.
*
* @param {Object} ctx - The context.
* @param {IJoinOptions} options - The options to use when joining the participant.
* @returns {Promise<void>}
*/
export function joinThirdParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
return _joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: P3_DISPLAY_NAME,
...options
});
}
/**
* Ensure that there are four participants.
*
@@ -131,13 +174,14 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
const { skipInMeetingChecks } = options;
await _joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
});
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
}),
skipInMeetingChecks ? Promise.resolve() : ctx.p1.waitForRemoteStreams(1),
skipInMeetingChecks ? Promise.resolve() : ctx.p2.waitForRemoteStreams(1)
]);
@@ -215,7 +259,7 @@ export async function unmuteAudioAndCheck(testee: Participant, observer: Partici
}
/**
* Starts the video on testee and check on observer.
* Stop the video on testee and check on observer.
* @param testee
* @param observer
*/
@@ -226,6 +270,18 @@ export async function unmuteVideoAndCheck(testee: Participant, observer: Partici
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
}
/**
* Starts the video on testee and check on observer.
* @param testee
* @param observer
*/
export async function muteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickVideoMuteButton();
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
await observer.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee);
}
/**
* Get a JWT token for a moderator.
*/
@@ -302,3 +358,23 @@ export async function checkSubject(participant: Participant, subject: string) {
expect(txt.startsWith(subject)).toBe(true);
}
/**
* Check if a screensharing tile is displayed on the observer.
* Expects there was already a video by this participant and screen sharing will be the second video `-v1`.
*/
export async function checkForScreensharingTile(sharer: Participant, observer: Participant, reverse = false) {
await observer.driver.$(`//span[@id='participant_${await sharer.getEndpointId()}-v1']`).waitForDisplayed({
timeout: 3_000,
reverse
});
}
/**
* Hangs up all participants (p1, p2, p3 and p4)
* @returns {Promise<void>}
*/
export function hangupAllParticipants() {
return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup() ]
.map(p => p ?? Promise.resolve()));
}

View File

@@ -13,6 +13,7 @@ export type IContext = {
p2: Participant;
p3: Participant;
p4: Participant;
roomKey: string;
roomName: string;
skipSuiteTests: boolean;
times: any;

View File

@@ -36,7 +36,7 @@ export default class Filmstrip extends BasePageObject {
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
reverse,
timeout: 2000,
timeout: 5_000,
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
});
}
@@ -62,7 +62,7 @@ export default class Filmstrip extends BasePageObject {
await remoteDisplayName.moveTo();
return await this.participant.driver.execute(eId =>
return await this.participant.execute(eId =>
document.evaluate(`//span[@id="participant_${eId}"]//video`,
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue?.srcObject?.id, endpointId);
}
@@ -71,7 +71,7 @@ export default class Filmstrip extends BasePageObject {
* Returns the local video id.
*/
getLocalVideoId() {
return this.participant.driver.execute(
return this.participant.execute(
'return document.getElementById("localVideo_container").srcObject.id');
}
@@ -80,10 +80,49 @@ export default class Filmstrip extends BasePageObject {
* @param participant The participant.
*/
async pinParticipant(participant: Participant) {
const id = participant === this.participant
? 'localVideoContainer' : `participant_${await participant.getEndpointId()}`;
let videoIdToSwitchTo;
await this.participant.driver.$(`//span[@id="${id}"]`).click();
if (participant === this.participant) {
videoIdToSwitchTo = await this.getLocalVideoId();
// when looking up the element and clicking it, it doesn't work if we do it twice in a row (oneOnOne.spec)
await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
} else {
const epId = await participant.getEndpointId();
videoIdToSwitchTo = await this.getRemoteVideoId(epId);
await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
}
await this.participant.driver.waitUntil(
async () => await this.participant.getLargeVideo().getId() === videoIdToSwitchTo,
{
timeout: 3_000,
timeoutMsg: `${this.participant.displayName} did not switch the large video to ${
participant.displayName}`
}
);
}
/**
* Unpins a participant by clicking on their thumbnail.
* @param participant
*/
async unpinParticipant(participant: Participant) {
const epId = await participant.getEndpointId();
if (participant === this.participant) {
await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
} else {
await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
}
await this.participant.driver.$(`//div[ @id="pin-indicator-${epId}" ]`).waitForDisplayed({
timeout: 2_000,
timeoutMsg: `${this.participant.displayName} did not unpin ${participant.displayName}`,
reverse: true
});
}
/**
@@ -136,11 +175,7 @@ export default class Filmstrip extends BasePageObject {
* @param participant
*/
async muteAudio(participant: Participant) {
const participantId = await participant.getEndpointId();
await this.participant.driver.$(`#participant-item-${participantId}`).moveTo();
await this.participant.driver.$(`button[data-testid="mute-audio-${participantId}"]`).click();
await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutelink', false);
}
/**
@@ -159,12 +194,19 @@ export default class Filmstrip extends BasePageObject {
return this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
}
/**
* Hover over local video.
*/
hoverOverLocalVideo() {
return this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
}
/**
* Clicks on the hide self view button from local video.
*/
async hideSelfView() {
// open local video menu
await this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
await this.hoverOverLocalVideo();
await this.participant.driver.$(LOCAL_USER_CONTROLS).moveTo();
// click Hide self view button
@@ -219,4 +261,16 @@ export default class Filmstrip extends BasePageObject {
return (await this.participant.driver.$$('//div[@id="remoteVideos"]//span[contains(@class,"videocontainer")]')
.filter(thumbnail => thumbnail.isDisplayed())).length;
}
/**
* Check if remote videos in filmstrip are visible.
*
* @param isDisplayed whether or not filmstrip remote videos should be visible
*/
verifyRemoteVideosDisplay(isDisplayed: boolean) {
return this.participant.driver.$('//div[contains(@class, "remote-videos")]/div').waitForDisplayed({
timeout: 5_000,
reverse: !isDisplayed,
});
}
}

View File

@@ -11,7 +11,7 @@ export default class IframeAPI extends BasePageObject {
* @param event
*/
getEventResult(event: string): Promise<any> {
return this.participant.driver.execute(
return this.participant.execute(
eventName => {
const result = window.jitsiAPI.test[eventName];
@@ -28,7 +28,7 @@ export default class IframeAPI extends BasePageObject {
* @param eventName The event name.
*/
addEventListener(eventName: string) {
return this.participant.driver.execute(
return this.participant.execute(
(event, prefix) => {
console.log(`${new Date().toISOString()} ${prefix} Adding listener for event: ${event}`);
window.jitsiAPI.addListener(event, evt => {
@@ -43,14 +43,14 @@ export default class IframeAPI extends BasePageObject {
* Returns an array of available rooms and details of it.
*/
getRoomsInfo() {
return this.participant.driver.execute(() => window.jitsiAPI.getRoomsInfo());
return this.participant.execute(() => window.jitsiAPI.getRoomsInfo());
}
/**
* Returns the number of participants in the conference.
*/
getNumberOfParticipants() {
return this.participant.driver.execute(() => window.jitsiAPI.getNumberOfParticipants());
return this.participant.execute(() => window.jitsiAPI.getNumberOfParticipants());
}
/**
@@ -59,7 +59,7 @@ export default class IframeAPI extends BasePageObject {
* @param args The arguments.
*/
executeCommand(command: string, ...args: any[]) {
return this.participant.driver.execute(
return this.participant.execute(
(commandName, commandArgs) =>
window.jitsiAPI.executeCommand(commandName, ...commandArgs)
, command, args);
@@ -69,13 +69,13 @@ export default class IframeAPI extends BasePageObject {
* Returns the current state of the participant's pane.
*/
isParticipantsPaneOpen() {
return this.participant.driver.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
return this.participant.execute(() => window.jitsiAPI.isParticipantsPaneOpen());
}
/**
* Removes the embedded Jitsi Meet conference.
*/
dispose() {
return this.participant.driver.execute(() => window.jitsiAPI.dispose());
return this.participant.execute(() => window.jitsiAPI.dispose());
}
}

View File

@@ -0,0 +1,81 @@
import BasePageObject from './BasePageObject';
/**
* The large video.
*/
export default class LargeVideo extends BasePageObject {
/**
* Returns the elapsed time at which video has been playing.
*
* @return {number} - The current play time of the video element.
*/
async getPlaytime() {
return this.participant.driver.$('#largeVideo').getProperty('currentTime');
}
/**
* Waits 5s for the large video to switch to passed endpoint id.
*
* @param {string} endpointId - The endpoint.
* @returns {Promise<void>}
*/
waitForSwitchTo(endpointId: string): Promise<void> {
return this.participant.driver.waitUntil(async () => endpointId === await this.getResource(), {
timeout: 5_000,
timeoutMsg: `expected large video to switch to ${endpointId} for 5s`
});
}
/**
* Gets avatar SRC attribute for the one displayed on large video.
*/
async getAvatar() {
const avatar = this.participant.driver.$('//img[@id="dominantSpeakerAvatar"]');
return await avatar.isExisting() ? await avatar.getAttribute('src') : null;
}
/**
* Returns resource part of the JID of the user who is currently displayed in the large video area.
*/
getResource() {
return this.participant.execute(() => APP?.UI?.getLargeVideoID());
}
/**
* Returns the source of the large video currently shown.
*/
getId() {
return this.participant.execute('return document.getElementById("largeVideo").srcObject.id');
}
/**
* Checks if the large video is playing or not.
*
* @returns {Promise<void>}
*/
assertPlaying() {
let lastTime: number;
return this.participant.driver.waitUntil(async () => {
const currentTime = parseFloat(await this.getPlaytime());
if (typeof lastTime === 'undefined') {
lastTime = currentTime;
}
if (currentTime > lastTime) {
return true;
}
lastTime = currentTime;
return false;
}, {
timeout: 5_500,
interval: 500,
timeoutMsg:
`Expected large video for participant ${this.participant.name} to play but it didn't for more than 5s`
});
}
}

View File

@@ -153,7 +153,7 @@ export default class Notifications extends BasePageObject {
private async getNotificationText(testId: string) {
const notificationElement = this.participant.driver.$(`[data-testid="${testId}"]`);
await notificationElement.waitForExist();
await notificationElement.waitForExist({ timeout: 2_000 });
return await notificationElement.getText();
}

View File

@@ -239,4 +239,16 @@ export default class ParticipantsPane extends BasePageObject {
await rejectButton.waitForExist();
await rejectButton.click();
}
/**
* Mutes the audio of a participant.
* @param participant
*/
async muteAudio(participant: Participant) {
const participantId = await participant.getEndpointId();
await this.participant.driver.$(`#participant-item-${participantId}`).moveTo();
await this.participant.driver.$(`button[data-testid="mute-audio-${participantId}"]`).click();
}
}

View File

@@ -0,0 +1,37 @@
import BaseDialog from './BaseDialog';
const INPUT_KEY_XPATH = '//input[@name="lockKey"]';
/**
* Represents the password dialog in a particular participant.
*/
export default class PasswordDialog extends BaseDialog {
/**
* Waiting for the dialog to appear.
*/
async waitForDialog() {
const input = this.participant.driver.$(INPUT_KEY_XPATH);
await input.waitForExist({
timeout: 5000,
timeoutMsg: 'Password dialog not found'
});
await input.waitForDisplayed();
}
/**
* Sets a password and submits the dialog.
* @param password
*/
async submitPassword(password: string) {
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
await passwordInput.waitForExist();
await passwordInput.click();
await passwordInput.clearValue();
await this.participant.driver.keys(password);
await this.clickOkButton();
}
}

View File

@@ -1,7 +1,10 @@
import PreMeetingScreen from './PreMeetingScreen';
const DISPLAY_NAME_ID = 'premeeting-name-input';
const ERROR_ON_JOIN = 'prejoin.errorMessage';
const JOIN_BUTTON_TEST_ID = 'prejoin.joinMeeting';
const JOIN_WITHOUT_AUDIO = 'prejoin.joinWithoutAudio';
const OPTIONS_BUTTON = 'prejoin.joinOptions';
/**
* Page object for the PreJoin screen.
@@ -28,4 +31,25 @@ export default class PreJoinScreen extends PreMeetingScreen {
return this.participant.driver.$('[data-testid="prejoin.screen"]')
.waitForDisplayed({ timeout: 3000 });
}
/**
* Returns the error message displayed on the prejoin screen.
*/
getErrorOnJoin() {
return this.participant.driver.$(`[data-testid="${ERROR_ON_JOIN}"]`);
}
/**
* Returns the join without audio button element.
*/
getJoinWithoutAudioButton() {
return this.participant.driver.$(`[data-testid="${JOIN_WITHOUT_AUDIO}"]`);
}
/**
* Returns the join options button element.
*/
getJoinOptions() {
return this.participant.driver.$(`[data-testid="${OPTIONS_BUTTON}"]`);
}
}

View File

@@ -54,8 +54,8 @@ export default abstract class PreMeetingScreen extends BasePageObject {
* Checks internally whether lobby room is joined.
*/
isLobbyRoomJoined() {
return this.participant.driver.execute(
() => APP.conference._room?.room?.getLobby()?.lobbyRoom?.joined === true);
return this.participant.execute(
() => APP?.conference?._room?.room?.getLobby()?.lobbyRoom?.joined === true);
}
/**

View File

@@ -7,6 +7,7 @@ const ADD_PASSWORD_FIELD = 'info-password-input';
const DIALOG_CONTAINER = 'security-dialog';
const LOCAL_LOCK = 'info-password-local';
const REMOTE_LOCK = 'info-password-remote';
const REMOVE_PASSWORD = 'remove-password';
/**
* Page object for the security dialog.
@@ -133,4 +134,18 @@ export default class SecurityDialog extends BaseDialog {
expect(validationMessage).toBe('');
}
}
/**
* Removes the password from the current conference through the security dialog, if a password is set.
*/
async removePassword() {
if (!await this.isLocked()) {
return;
}
const removePassword = this.participant.driver.$(`.${REMOVE_PASSWORD}`);
await removePassword.waitForClickable();
await removePassword.click();
}
}

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