mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-07 07:12:28 +00:00
Compare commits
35 Commits
fix-p2p-co
...
8079
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3c3b38993 | ||
|
|
94b6808ec6 | ||
|
|
1376f5909c | ||
|
|
74b02af318 | ||
|
|
de1e470c68 | ||
|
|
4ee613ed1f | ||
|
|
fb6a44a39b | ||
|
|
bde28105f4 | ||
|
|
782d46b4a6 | ||
|
|
160d6a4c52 | ||
|
|
767101497c | ||
|
|
889b37cedc | ||
|
|
4e727e9093 | ||
|
|
7fbf47c6f3 | ||
|
|
2d61c68615 | ||
|
|
d2ad3473a1 | ||
|
|
67f49815c4 | ||
|
|
2697eb1273 | ||
|
|
491f793530 | ||
|
|
eb0317fb8d | ||
|
|
59da1537be | ||
|
|
9e1e6237ce | ||
|
|
5c0b8467d5 | ||
|
|
b4a5e63d1d | ||
|
|
3ae50b6c4c | ||
|
|
bc9525a908 | ||
|
|
c6dcac47a8 | ||
|
|
f9f5cf87b9 | ||
|
|
b969fba433 | ||
|
|
f0fc63f573 | ||
|
|
d618175074 | ||
|
|
9ebe2c4395 | ||
|
|
e5189a5c1c | ||
|
|
f96592b4dc | ||
|
|
a11a281bf7 |
173
CONTRIBUTING.md
173
CONTRIBUTING.md
@@ -1,171 +1,14 @@
|
||||
# How to contribute
|
||||
We would love to have your help. Before you start working however, please read
|
||||
and follow this short guide.
|
||||
# Follow Our Updated Guide to See How You Can Contribute
|
||||
|
||||
# Reporting Issues
|
||||
Provide as much information as possible. Mention the version of Jitsi Meet,
|
||||
Jicofo and JVB you are using, and explain (as detailed as you can) how the
|
||||
problem can be reproduced.
|
||||
Hello there! 👋
|
||||
|
||||
# Code contributions
|
||||
Found a bug and know how to fix it? Great! Please read on.
|
||||
We're thrilled that you're eager to contribute to Jitsi Meet! ❤️
|
||||
|
||||
## Contributor License Agreement
|
||||
While the Jitsi projects are released under the
|
||||
[Apache License 2.0](https://github.com/jitsi/jitsi-meet/blob/master/LICENSE), the copyright
|
||||
holder and principal creator is [8x8](https://www.8x8.com/). To
|
||||
ensure that we can continue making these projects available under an Open Source license,
|
||||
we need you to sign our Apache-based contributor
|
||||
license agreement as either a [corporation](https://jitsi.org/ccla) or an
|
||||
[individual](https://jitsi.org/icla). If you cannot accept the terms laid out
|
||||
in the agreement, unfortunately, we cannot accept your contribution.
|
||||
Your interest in improving our platform means a lot to us. To ensure your contributions align seamlessly with our goals and processes, we've recently updated our guide. This guide will provide you with clear instructions on how to get involved effectively.
|
||||
|
||||
## Creating Pull Requests
|
||||
- Make sure your code passes the linter rules beforehand. The linter is executed
|
||||
automatically when committing code.
|
||||
- Perform **one** logical change per pull request.
|
||||
- Maintain a clean list of commits, squash them if necessary.
|
||||
- Rebase your topic branch on top of the master branch before creating the pull
|
||||
request.
|
||||
Ready to get started? Head over to our [Jitsi Meet Handbook](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-contributing/) and let's make Jitsi Meet even better together!
|
||||
|
||||
## Coding style
|
||||
### ❗️Additional Note
|
||||
Before sending us your code, double-check that it meets our coding standards. You can do this by running a command: `npm run lint`. If there are any issues, don't worry! You can fix them by running: `npm run lint-fix`. Once your code passes these checks, feel free to submit your pull request.
|
||||
|
||||
### Comments
|
||||
|
||||
* Comments documenting the source code are required.
|
||||
|
||||
* Comments from which documentation is automatically generated are **not**
|
||||
subject to case-by-case decisions. Such comments are used, for example, on
|
||||
types and their members. Examples of tools which automatically generate
|
||||
documentation from such comments include JSDoc, Javadoc, Doxygen.
|
||||
|
||||
* Comments which are not automatically processed are strongly encouraged. They
|
||||
are subject to case-by-case decisions. Such comments are often observed in
|
||||
function bodies.
|
||||
|
||||
* Comments should be formatted as proper English sentences. Such formatting pays
|
||||
attention to, for example, capitalization and punctuation.
|
||||
|
||||
### Duplication
|
||||
|
||||
* Don't copy-paste source code. Reuse it.
|
||||
|
||||
### Formatting
|
||||
|
||||
* Line length is limited to 120 characters.
|
||||
|
||||
* Sort by alphabetical order in order to make the addition of new entities as
|
||||
easy as looking a word up in a dictionary. Otherwise, one risks duplicate
|
||||
entries (with conflicting values in the cases of key-value pairs). For
|
||||
example:
|
||||
|
||||
* Within an `import` of multiple names from a module, sort the names in
|
||||
alphabetical order. (Of course, the default name stays first as required by
|
||||
the `import` syntax.)
|
||||
|
||||
````javascript
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
JITSI_CLIENT_CONNECTED,
|
||||
JITSI_CLIENT_CREATED,
|
||||
JITSI_CLIENT_DISCONNECTED,
|
||||
JITSI_CLIENT_ERROR,
|
||||
JITSI_CONFERENCE_JOINED,
|
||||
MODERATOR_CHANGED,
|
||||
PEER_JOINED,
|
||||
PEER_LEFT,
|
||||
RTC_ERROR
|
||||
} from './actionTypes';
|
||||
````
|
||||
|
||||
* Within a group of imports (e.g. groups of imports delimited by an empty line
|
||||
may be: third-party modules, then project modules, and eventually the
|
||||
private files of a module), sort the module names in alphabetical order.
|
||||
|
||||
````javascript
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
````
|
||||
|
||||
### Indentation
|
||||
|
||||
* Align `switch` and `case`/`default`. Don't indent the `case`/`default` more
|
||||
than its `switch`.
|
||||
|
||||
````javascript
|
||||
switch (i) {
|
||||
case 0:
|
||||
...
|
||||
break;
|
||||
default:
|
||||
...
|
||||
}
|
||||
````
|
||||
|
||||
### Naming
|
||||
|
||||
* An abstraction should have one name within the project and across multiple
|
||||
projects. For example:
|
||||
|
||||
* The instance of lib-jitsi-meet's `JitsiConnection` type should be named
|
||||
`connection` or `jitsiConnection` in jitsi-meet, not `client`.
|
||||
|
||||
* The class `ReducerRegistry` should be defined in ReducerRegistry.js and its
|
||||
imports in other files should use the same name. Don't define the class
|
||||
`Registry` in ReducerRegistry.js and then import it as `Reducers` in other
|
||||
files.
|
||||
|
||||
* The names of global constants (including ES6 module-global constants) should
|
||||
be written in uppercase with underscores to separate words. For example,
|
||||
`BACKGROUND_COLOR`.
|
||||
|
||||
* The underscore character at the beginning of a name signals that the
|
||||
respective variable, function, property is non-public i.e. private, protected,
|
||||
or internal. In contrast, the lack of an underscore at the beginning of a name
|
||||
signals public API.
|
||||
|
||||
### Feature layout
|
||||
|
||||
When adding a new feature, this would be the usual layout.
|
||||
|
||||
```
|
||||
react/features/sample/
|
||||
├── actionTypes.ts
|
||||
├── actions.js
|
||||
├── components
|
||||
│ ├── AnotherComponent.js
|
||||
│ ├── OneComponent.js
|
||||
│ └── index.js
|
||||
├── middleware.js
|
||||
└── reducer.js
|
||||
```
|
||||
|
||||
The middleware must be imported in `react/features/app/` specifically
|
||||
in `middlewares.any.ts`, `middlewares.native.ts` or `middlewares.web.ts` where appropriate.
|
||||
Likewise for the reducer.
|
||||
|
||||
An `index.js` file must not be provided for exporting actions, action types and
|
||||
component. Features / files requiring those must import them explicitly.
|
||||
|
||||
This has not always been the case and the entire codebase hasn't been migrated to
|
||||
this model but new features should follow this new layout.
|
||||
|
||||
When working on an old feature, adding the necessary changes to migrate to the new
|
||||
model is encouraged.
|
||||
|
||||
|
||||
### Avoiding bundle bloat
|
||||
|
||||
When adding a new feature it's possible that it triggers a build failure due to the increased bundle size. We have safeguards inplace to avoid bundles growing disproportionatelly. While there are legit reasons for increasing the limits, please analyze the bundle first to make sure no unintended dependencies have been included, causing the increase in size.
|
||||
|
||||
First, make a production build with bundle-analysis enabled:
|
||||
|
||||
```
|
||||
npx webpack -p --analyze-bundle
|
||||
```
|
||||
|
||||
Then open the interactive bundle analyzer tool:
|
||||
|
||||
```
|
||||
npx webpack-bundle-analyzer build/app-stats.json
|
||||
```
|
||||
Happy coding!
|
||||
|
||||
@@ -44,7 +44,7 @@ ext {
|
||||
googleServicesEnabled = project.file('app/google-services.json').exists() && !libreBuild
|
||||
|
||||
//React Native Version
|
||||
rnVersion = "0.72.9"
|
||||
rnVersion = "0.73.8"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
||||
@@ -294,8 +294,8 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onUserLeaveHint() {
|
||||
if (this.jitsiView != null) {
|
||||
this.jitsiView .enterPictureInPicture();
|
||||
if (this.jitsiView != null) {
|
||||
this.jitsiView.enterPictureInPicture();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import static android.Manifest.permission.POST_NOTIFICATIONS;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
@@ -38,7 +39,9 @@ import com.facebook.react.modules.core.PermissionListener;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
@@ -57,7 +60,7 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
|
||||
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver();
|
||||
|
||||
private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = (int) (Math.random() * Short.MAX_VALUE);
|
||||
private static final int PERMISSIONS_REQUEST_CODE = (int) (Math.random() * Short.MAX_VALUE);
|
||||
|
||||
private boolean isAudioMuted;
|
||||
|
||||
@@ -95,26 +98,50 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
|
||||
|
||||
public static void launch(Context context, HashMap<String, Object> extraData) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
PermissionListener listener = new PermissionListener() {
|
||||
@Override
|
||||
public boolean onRequestPermissionsResult(int i, String[] strings, int[] results) {
|
||||
if (results.length > 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
doLaunch(context, extraData);
|
||||
List<String> permissionsList = new ArrayList<>();
|
||||
|
||||
PermissionListener listener = new PermissionListener() {
|
||||
@Override
|
||||
public boolean onRequestPermissionsResult(int i, String[] strings, int[] results) {
|
||||
int counter = 0;
|
||||
|
||||
if (results.length > 0) {
|
||||
for (int result : results) {
|
||||
if (result == PackageManager.PERMISSION_GRANTED) {
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
if (counter == results.length){
|
||||
doLaunch(context, extraData);
|
||||
JitsiMeetLogger.w(TAG + " Service launched, permissions were granted");
|
||||
} else {
|
||||
JitsiMeetLogger.w(TAG + " Couldn't launch service, permissions were not granted");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionsList.add(POST_NOTIFICATIONS);
|
||||
permissionsList.add(RECORD_AUDIO);
|
||||
}
|
||||
|
||||
String[] permissionsArray = new String[ permissionsList.size() ];
|
||||
permissionsArray = permissionsList.toArray( permissionsArray );
|
||||
|
||||
if (permissionsArray.length > 0) {
|
||||
JitsiMeetActivityDelegate.requestPermissions(
|
||||
(Activity) context,
|
||||
new String[]{POST_NOTIFICATIONS},
|
||||
POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE,
|
||||
permissionsArray,
|
||||
PERMISSIONS_REQUEST_CODE,
|
||||
listener
|
||||
);
|
||||
} else {
|
||||
doLaunch(context, extraData);
|
||||
JitsiMeetLogger.w(TAG + " Service launched");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +159,10 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
stopSelf();
|
||||
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ project(':react-native-background-timer').projectDir = new File(rootProject.proj
|
||||
include ':react-native-calendar-events'
|
||||
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
|
||||
include ':react-native-community_clipboard'
|
||||
project(':react-native-community_clipboard').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/clipboard/android')
|
||||
project(':react-native-community_clipboard').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-clipboard/clipboard/android')
|
||||
include ':react-native-community_netinfo'
|
||||
project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
|
||||
include ':react-native-default-preference'
|
||||
|
||||
@@ -1676,6 +1676,18 @@ export default {
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.SILENT_STATUS_CHANGED,
|
||||
(id, isSilent) => {
|
||||
APP.store.dispatch(participantUpdated({
|
||||
conference: room,
|
||||
id,
|
||||
isSilent
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.BOT_TYPE_CHANGED,
|
||||
(id, botType) => {
|
||||
|
||||
15
config.js
15
config.js
@@ -88,8 +88,8 @@ var config = {
|
||||
// issues related to insertable streams.
|
||||
// disableE2EE: false,
|
||||
|
||||
// Enables supports for AV1 codec.
|
||||
// enableAv1Support: false,
|
||||
// Enables the use of the codec selection API supported by the browsers .
|
||||
// enableCodecSelectionAPI: false,
|
||||
|
||||
// P2P test mode disables automatic switching to P2P when there are 2
|
||||
// participants in the conference.
|
||||
@@ -121,6 +121,9 @@ var config = {
|
||||
// Disables polls feature.
|
||||
// disablePolls: false,
|
||||
|
||||
// Disables demote button from self-view
|
||||
// disableSelfDemote: false,
|
||||
|
||||
// Disables self-view tile. (hides it from tile view and from filmstrip)
|
||||
// disableSelfView: false,
|
||||
|
||||
@@ -458,6 +461,10 @@ var config = {
|
||||
// // Provides a way to set the codec preference on desktop based endpoints.
|
||||
// codecPreferenceOrder: [ 'VP9', 'VP8', 'H264' ],
|
||||
//
|
||||
// // Provides a way to set the codec for screenshare.
|
||||
// screenshareCodec: 'AV1',
|
||||
// mobileScreenshareCodec: 'VP8',
|
||||
//
|
||||
// // Codec specific settings for scalability modes and max bitrates.
|
||||
// av1: {
|
||||
// maxBitratesVideo: {
|
||||
@@ -1047,6 +1054,10 @@ var config = {
|
||||
// Provides a way to set the codec preference on desktop based endpoints.
|
||||
// codecPreferenceOrder: [ 'VP9', 'VP8', 'H264 ],
|
||||
|
||||
// Provides a way to set the codec for screenshare.
|
||||
// screenshareCodec: 'AV1',
|
||||
// mobileScreenshareCodec: 'VP8',
|
||||
|
||||
// How long we're going to wait, before going back to P2P after the 3rd
|
||||
// participant has left the conference (to filter out page reload).
|
||||
// backToP2PDelay: 5,
|
||||
|
||||
@@ -10,6 +10,10 @@ workspace 'jitsi-meet'
|
||||
|
||||
install! 'cocoapods', :deterministic_uuids => false
|
||||
|
||||
def cocoa_utilities
|
||||
pod 'CocoaLumberjack', '3.7.4'
|
||||
end
|
||||
|
||||
target 'JitsiMeet' do
|
||||
project 'app/app.xcodeproj'
|
||||
|
||||
@@ -45,7 +49,7 @@ target 'JitsiMeetSDK' do
|
||||
# Native pod dependencies
|
||||
#
|
||||
|
||||
pod 'CocoaLumberjack', '3.7.4'
|
||||
cocoa_utilities
|
||||
pod 'ObjectiveDropboxOfficial', '6.2.3'
|
||||
end
|
||||
|
||||
@@ -70,7 +74,7 @@ target 'JitsiMeetSDKLite' do
|
||||
# Native pod dependencies
|
||||
#
|
||||
|
||||
pod 'CocoaLumberjack', '3.7.4'
|
||||
cocoa_utilities
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
@@ -79,7 +83,6 @@ post_install do |installer|
|
||||
use_native_modules![:reactNativePath],
|
||||
:mac_catalyst_enabled => false
|
||||
)
|
||||
__apply_Xcode_12_5_M1_post_install_workaround(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
# https://github.com/CocoaPods/CocoaPods/issues/11402
|
||||
if target.respond_to?(:product_type) and target.product_type == "com.apple.product-type.bundle"
|
||||
|
||||
1259
ios/Podfile.lock
1259
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
@@ -375,6 +375,7 @@
|
||||
DE11877A21EE09640078D059 /* Setup Google reverse URL handler */,
|
||||
0BB7DA181EC9E695007AAE98 /* Adjust ATS */,
|
||||
DE4F6D6E22005C0400DE699E /* Setup Dropbox */,
|
||||
E9D850368D253EFA8AB3B8D1 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -611,6 +612,23 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "INFO_PLIST=\"$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH\"\nDROPBOX_KEY_FILE=\"$PROJECT_DIR/dropbox.key\"\n\nif [[ -f $DROPBOX_KEY_FILE ]]; then\n /usr/libexec/PlistBuddy -c \"Delete :LSApplicationQueriesSchemes\" $INFO_PLIST\n /usr/libexec/PlistBuddy -c \"Add :LSApplicationQueriesSchemes array\" $INFO_PLIST\n /usr/libexec/PlistBuddy -c \"Add :LSApplicationQueriesSchemes:0 string 'dbapi-2'\" $INFO_PLIST\n /usr/libexec/PlistBuddy -c \"Add :LSApplicationQueriesSchemes:1 string 'dbapi-8-emm'\" $INFO_PLIST\n\n DROPBOX_KEY=$(head -n 1 $DROPBOX_KEY_FILE)\n /usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:2:CFBundleURLName string dropbox\" $INFO_PLIST\n /usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:2:CFBundleURLSchemes array\" $INFO_PLIST\n /usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:2:CFBundleURLSchemes:0 string $DROPBOX_KEY\" $INFO_PLIST\nfi\n";
|
||||
};
|
||||
E9D850368D253EFA8AB3B8D1 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -968,7 +986,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
@@ -1025,6 +1043,7 @@
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
USE_HERMES = false;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1033,7 +1052,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
@@ -1086,6 +1105,7 @@
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
USE_HERMES = false;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -730,7 +730,7 @@
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
@@ -785,6 +785,7 @@
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
USE_HERMES = false;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
@@ -797,7 +798,7 @@
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++17";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
@@ -849,6 +850,7 @@
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
USE_HERMES = false;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
"privateNotice": "Message privé à {{recipient}}",
|
||||
"sendButton": "Envoyer",
|
||||
"smileysPanel": "Panneaux des Émojis",
|
||||
"systemDisplayName": "Système",
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"polls": "Sondages"
|
||||
@@ -219,7 +220,9 @@
|
||||
"joinInBrowser": "Rejoindre depuis le navigateur",
|
||||
"launchMeetingLabel": "Comment voulez-vous rejoindre la réunion ?",
|
||||
"launchWebButton": "Lancer dans le navigateur",
|
||||
"noDesktopApp": "Vous n'avez pas l'application ?",
|
||||
"noMobileApp": "Vous n’avez pas l’application ?",
|
||||
"or": "OU",
|
||||
"termsAndConditions": "En continuant, vous acceptez nos <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>conditions générales d’utilisation.</a>",
|
||||
"title": "Lancement de votre réunion dans {{app}} en cours ...",
|
||||
"titleNew": "Lancement de votre réunion ...",
|
||||
@@ -261,6 +264,7 @@
|
||||
"Share": "Partager",
|
||||
"Submit": "Soumettre",
|
||||
"WaitForHostMsg": "La conférence n'a pas encore commencé. Si vous en êtes l'hôte, veuillez vous authentifier. Sinon, veuillez attendre son arrivée.",
|
||||
"WaitForHostNoAuthMsg": "La conférence n'a pas encore commencé car aucun modérateur n'est encore arrivé. Veuillez patienter.",
|
||||
"WaitingForHostButton": "Attendre l'hôte",
|
||||
"WaitingForHostTitle": "En attente de l'hôte ...",
|
||||
"Yes": "Oui",
|
||||
@@ -303,6 +307,8 @@
|
||||
"contactSupport": "Contacter le support",
|
||||
"copied": "Copié",
|
||||
"copy": "Copier",
|
||||
"demoteParticipantDialog": "Êtes-vous sûr de vouloir déplacer ce participant en visiteur ?",
|
||||
"demoteParticipantTitle": "Déplacer en visiteur",
|
||||
"dismiss": "Rejeter",
|
||||
"displayNameRequired": "Bonjour ! Quel est votre nom ?",
|
||||
"done": "Terminé",
|
||||
@@ -314,6 +320,7 @@
|
||||
"embedMeeting": "Intégrer la réunion",
|
||||
"enterDisplayName": "Merci de saisir votre nom ici",
|
||||
"error": "Erreur",
|
||||
"errorRoomCreationRestriction": "Vous avez essayé de rejoindre trop rapidement, veuillez revenir dans un moment.",
|
||||
"gracefulShutdown": "Notre service est actuellement en maintenance. Veuillez réessayer plus tard.",
|
||||
"grantModeratorDialog": "Êtes-vous sûr de vouloir rendre ce participant modérateur ?",
|
||||
"grantModeratorTitle": "Nommer modérateur",
|
||||
@@ -558,6 +565,7 @@
|
||||
"noNumbers": "Numéros non trouvés",
|
||||
"noPassword": "Aucun",
|
||||
"noRoom": "Aucune réunion n'a été spécifiée pour l'appel entrant.",
|
||||
"noWhiteboard": "Impossible de charger le tableau blanc.",
|
||||
"numbers": "Numéros d'appel",
|
||||
"password": "$t(lockRoomPasswordUppercase) :",
|
||||
"reachedLimit": "Vous avez atteint la limite de votre abonnement.",
|
||||
@@ -565,7 +573,8 @@
|
||||
"sipAudioOnly": "Adresse SIP en audio uniquement",
|
||||
"title": "Partager",
|
||||
"tooltip": "Partager le lien et les informations de connexion pour cette conférence",
|
||||
"upgradeOptions": "Veuillez vérifier les options de mise à niveau"
|
||||
"upgradeOptions": "Veuillez vérifier les options de mise à niveau",
|
||||
"whiteboardError": "Erreur de chargement du tableau blanc. Veuillez réessayer plus tard."
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "Il y a eu un petit problème.",
|
||||
@@ -729,6 +738,8 @@
|
||||
"connectedTwoMembers": "{{first}} et {{second}} ont rejoint la réunion",
|
||||
"dataChannelClosed": "Qualité vidéo dégradée",
|
||||
"dataChannelClosedDescription": "Le canal de communication avec le Bridge a été interrompu, la qualité vidéo se trouve limitée à sa valeur la plus faible.",
|
||||
"dataChannelClosedDescriptionWithAudio": "Le canal de pont est fermé, ce qui peut entraîner des perturbations de l'audio et de la vidéo.",
|
||||
"dataChannelClosedWithAudio": "La qualité de l'audio et de la vidéo peut être altérée",
|
||||
"disabledIframe": "L'intégration Iframe est uniquement destinée à des démos, cet appel se terminera dans {{timeout}} minutes.",
|
||||
"disabledIframeSecondary": "L'intégration Iframe de {{domaine}} est uniquement destinée à des démos, cet appel se terminera dans {{timeout}} minutes.",
|
||||
"disconnected": "déconnecté",
|
||||
@@ -800,13 +811,19 @@
|
||||
"startSilentTitle": "Vous avez rejoint sans sortie audio !",
|
||||
"suboptimalBrowserWarning": "Nous craignons que votre expérience de réunion en ligne ne soit pas idéale ici. Nous cherchons des moyens d'améliorer cela, mais d'ici-là, essayez d'utiliser l'un des <a href='{{recommendedBrowserPageLink}}' target='_blank'>navigateurs supportés</a>.",
|
||||
"suboptimalExperienceTitle": "Avertissement du navigateur",
|
||||
"suggestRecordingAction": "Démarrer",
|
||||
"suggestRecordingDescription": "Souhaitez-vous démarrer un enregistrement ?",
|
||||
"suggestRecordingTitle": "Enregistrer cette réunion",
|
||||
"unmute": "Rétablir le son",
|
||||
"videoMutedRemotelyDescription": "Vous pouvez toujours la réactiver.",
|
||||
"videoMutedRemotelyTitle": "Votre caméra a été coupée par {{participantDisplayName}}!",
|
||||
"videoUnmuteBlockedDescription": "Le rétablissement de la vidéo a été bloqué temporairement en raison de limites système.",
|
||||
"videoUnmuteBlockedTitle": "Rétablissement de la caméra bloqué !",
|
||||
"viewLobby": "Voir la salle d'attente",
|
||||
"viewVisitors": "Voir les visiteurs",
|
||||
"waitingParticipants": "{{waitingParticipants}} personnes",
|
||||
"waitingVisitors": "Visiteurs en attente dans la file : {{waitingVisitors}}",
|
||||
"waitingVisitorsTitle": "La réunion n'est pas encore en direct !",
|
||||
"whiteboardLimitDescription": "Veuillez sauvegarder votre progression, car la limite d’utilisation du tableau blanc sera bientôt atteinte et celui-ci sera fermé.",
|
||||
"whiteboardLimitTitle": "Utiilisation du tableau blanc"
|
||||
},
|
||||
@@ -820,6 +837,7 @@
|
||||
"audioModeration": "Rouvrir leur micro",
|
||||
"blockEveryoneMicCamera": "Bloquer tous les micros et caméras",
|
||||
"breakoutRooms": "Salles annexes",
|
||||
"goLive": "Passer en direct",
|
||||
"invite": "Inviter quelqu'un",
|
||||
"moreModerationActions": "Options de modération supplémentaires",
|
||||
"moreModerationControls": "Options de modération supplémentaires",
|
||||
@@ -837,6 +855,7 @@
|
||||
"headings": {
|
||||
"lobby": "Salle d'attente ({{count}})",
|
||||
"participantsList": "Participants de la réunion ({{count}})",
|
||||
"visitorInQueue": " (en attente {{count}})",
|
||||
"visitorRequests": "(Demande {{count}} )",
|
||||
"visitors": "Visiteurs {{count}}",
|
||||
"waitingLobby": "Dans la salle d'attente ({{count}})"
|
||||
@@ -850,6 +869,8 @@
|
||||
"pinnedParticipant": "Participant toujours affiché",
|
||||
"polls": {
|
||||
"answer": {
|
||||
"edit": "Modifier",
|
||||
"send": "Envoyer",
|
||||
"skip": "Passer",
|
||||
"submit": "Envoyer"
|
||||
},
|
||||
@@ -863,6 +884,7 @@
|
||||
"pollQuestion": "Question du sondage",
|
||||
"questionPlaceholder": "Poser une question",
|
||||
"removeOption": "Supprimer l'option",
|
||||
"save": "Enregistrer",
|
||||
"send": "Envoyer"
|
||||
},
|
||||
"errors": {
|
||||
@@ -935,6 +957,7 @@
|
||||
"or": "ou",
|
||||
"premeeting": "Pré-séance",
|
||||
"proceedAnyway": "Continuer quand même",
|
||||
"recordingWarning": "D'autres participants peuvent enregistrer cet appel",
|
||||
"screenSharingError": "Erreur de partage d'écran:",
|
||||
"showScreen": "Activer l'écran de pré-séance",
|
||||
"startWithPhone": "Commencez avec l'audio du téléphone",
|
||||
@@ -1356,13 +1379,9 @@
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Activer / Désactiver les sous-titres",
|
||||
"error": "Échec de la transcription. Veuillez réessayer.",
|
||||
"expandedLabel": "La transcription est actuellement activée",
|
||||
"failedToStart": "Échec de démarrage de la transcription",
|
||||
"labelToolTip": "La transcription de la réunion est en cours",
|
||||
"off": "La transcription est désactivée",
|
||||
"on": "La transcription est activée",
|
||||
"pending": "Préparation de la transcription de la réunion ...",
|
||||
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
|
||||
"sourceLanguageHere": "ici",
|
||||
"start": "Activer les sous-titres",
|
||||
@@ -1418,6 +1437,7 @@
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Informations de la connexion",
|
||||
"demote": "Déplacer en visiteur",
|
||||
"domute": "Couper le micro",
|
||||
"domuteOthers": "Couper le micro de tous les autres",
|
||||
"domuteVideo": "Couper la caméra",
|
||||
@@ -1472,9 +1492,15 @@
|
||||
"chatIndicator": "(visiteur)",
|
||||
"labelTooltip": "Nombre de Visiteurs",
|
||||
"notification": {
|
||||
"demoteDescription": "Envoyé ici par {{actor}}, levez la main pour participer",
|
||||
"description": "Pour participer lever la main.",
|
||||
"noMainParticipantsDescription": "Un participant doit démarrer la réunion. Veuillez réessayer dans un moment.",
|
||||
"noMainParticipantsTitle": "Cette réunion n'a pas encore commencé.",
|
||||
"noVisitorLobby": "Vous ne pouvez pas rejoindre tant qu'une salle d'attente est activée pour la réunion.",
|
||||
"notAllowedPromotion": "Un participant doit d'abord autoriser votre demande.",
|
||||
"title": "Vous êtes visiteur dans cette réunion"
|
||||
}
|
||||
},
|
||||
"waitingMessage": "Vous rejoindrez la réunion dès qu'elle sera en direct !"
|
||||
},
|
||||
"volumeSlider": "Curseur de volume",
|
||||
"welcomepage": {
|
||||
@@ -1532,6 +1558,7 @@
|
||||
"whiteboard": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Tableau blanc"
|
||||
}
|
||||
},
|
||||
"screenTitle": "Tableau blanc"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,8 @@
|
||||
"Remove": "Noņemt",
|
||||
"Share": "Kopīgot",
|
||||
"Submit": "Iesniegt",
|
||||
"WaitForHostMsg": "Sapulce vēl nav sākusies. Ja esat sapulces rīkotājs, lūdzu autorizējaties. Pretējā gadījumā, lūdzu, uzgaidiet.",
|
||||
"WaitForHostMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, autorizējieties, lai kļūtu par moderatoru. Pretējā gadījumā, lūdzu, uzgaidiet.",
|
||||
"WaitForHostNoAuthMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, uzgaidiet.",
|
||||
"WaitingForHostButton": "Gaidīt rīkotāju",
|
||||
"WaitingForHostTitle": "Gaida rīkotāju...",
|
||||
"Yes": "Jā",
|
||||
@@ -864,6 +865,8 @@
|
||||
"pinnedParticipant": "Dalībnieks ir piesprausts",
|
||||
"polls": {
|
||||
"answer": {
|
||||
"edit": "Labot",
|
||||
"send": "Nosūtīt",
|
||||
"skip": "Izlaist",
|
||||
"submit": "Iesniegt"
|
||||
},
|
||||
@@ -877,6 +880,7 @@
|
||||
"pollQuestion": "Aptaujas Jautājums",
|
||||
"questionPlaceholder": "Uzdod jautājumu",
|
||||
"removeOption": "Noņemt opciju",
|
||||
"save": "Saglabāt",
|
||||
"send": "Nosūtīt"
|
||||
},
|
||||
"errors": {
|
||||
|
||||
@@ -263,7 +263,8 @@
|
||||
"Remove": "Kaldır",
|
||||
"Share": "Paylaş",
|
||||
"Submit": "Gönder",
|
||||
"WaitForHostMsg": "Toplantısı henüz başlamadı. Toplantı sahibi sizseniz, lütfen kimlik doğrulaması yapın. Değilseniz lütfen toplantı sahibinin gelmesini bekleyin.",
|
||||
"WaitForHostMsg": "Toplantı sahibi gelmediğinden toplantı henüz başlamadı. Toplantı sahibi sizseniz, lütfen kimlik doğrulaması yapın. Değilseniz lütfen toplantı sahibinin gelmesini bekleyin.",
|
||||
"WaitForHostNoAuthMsg": "Toplantı sahibi gelmediğinden toplantı henüz başlamadı. Lütfen bekleyin.",
|
||||
"WaitingForHostButton": "Toplantı sahibini bekle",
|
||||
"WaitingForHostTitle": "Toplantı sahibi bekleniyor ...",
|
||||
"Yes": "Evet",
|
||||
@@ -319,6 +320,7 @@
|
||||
"embedMeeting": "Toplantıyı yerleştir",
|
||||
"enterDisplayName": "Lütfen adınızı buraya girin...",
|
||||
"error": "Hata",
|
||||
"errorRoomCreationRestriction": "Çok hızlı katılmaya çalıştınız, lütfen biraz sonra tekrar gelin.",
|
||||
"gracefulShutdown": "Hizmetimiz şu anda bakım için devre dışı. Lütfen daha sonra tekrar deneyiniz.",
|
||||
"grantModeratorDialog": "{{participantName}} için moderatör hakları vermek istediğinize emin misiniz?",
|
||||
"grantModeratorTitle": "Moderatör hakları ver",
|
||||
@@ -820,6 +822,8 @@
|
||||
"viewLobby": "Lobiyi göster",
|
||||
"viewVisitors": "Ziyaretçileri görüntüle",
|
||||
"waitingParticipants": "{{waitingParticipants}} kişi",
|
||||
"waitingVisitors": "Sırada bekleyen ziyaretçiler: {{waitingVisitors}}",
|
||||
"waitingVisitorsTitle": "Toplantı henüz canlı değil!",
|
||||
"whiteboardLimitDescription": "Kullanıcı sınırına yakında ulaşılacağından ve beyaz tahta kapanacağından lütfen ilerlemenizi kaydedin.",
|
||||
"whiteboardLimitTitle": "Beyaz tahta kullanımı"
|
||||
},
|
||||
@@ -833,6 +837,7 @@
|
||||
"audioModeration": "Seslerini aç",
|
||||
"blockEveryoneMicCamera": "Herkesin mikrofonunu ve kamerasını blokla",
|
||||
"breakoutRooms": "Alt odalar",
|
||||
"goLive": "Canlı yayına geç",
|
||||
"invite": "Birini davet et",
|
||||
"moreModerationActions": "Daha fazla denetleme seçeneği",
|
||||
"moreModerationControls": "Daha fazla denetleme kontrolü",
|
||||
@@ -850,6 +855,7 @@
|
||||
"headings": {
|
||||
"lobby": "Lobi ({{count}})",
|
||||
"participantsList": "Toplantı Katılımcıları ({{count}})",
|
||||
"visitorInQueue": "(waiting {{count}})",
|
||||
"visitorRequests": "(requests {{count}})",
|
||||
"visitors": "Ziyaretçiler {{count}}",
|
||||
"waitingLobby": "Lobide bekleyen ({{count}})"
|
||||
@@ -863,6 +869,8 @@
|
||||
"pinnedParticipant": "Katılımcı sabitlendi",
|
||||
"polls": {
|
||||
"answer": {
|
||||
"edit": "Düzenle",
|
||||
"send": "Gönder",
|
||||
"skip": "Geç",
|
||||
"submit": "Gönder"
|
||||
},
|
||||
@@ -876,6 +884,7 @@
|
||||
"pollQuestion": "Anket Sorusu",
|
||||
"questionPlaceholder": "Soru sor",
|
||||
"removeOption": "Seçeneği sil",
|
||||
"save": "Kaydet",
|
||||
"send": "Gönder"
|
||||
},
|
||||
"errors": {
|
||||
@@ -1485,8 +1494,13 @@
|
||||
"notification": {
|
||||
"demoteDescription": "Buraya {{actor}} tarafından gönderildi, katılmak için elinizi kaldırın",
|
||||
"description": "Katılmak için elinizi kaldırın",
|
||||
"noMainParticipantsDescription": "Bir katılımcının toplantıyı başlatması gerekiyor. Lütfen biraz sonra tekrar deneyin.",
|
||||
"noMainParticipantsTitle": "Bu toplantı henüz başlamadı.",
|
||||
"noVisitorLobby": "Toplantı için etkinleştirilmiş bir lobi varken katılamazsınız.",
|
||||
"notAllowedPromotion": "Bir katılımcının öncelikle isteğinize izin vermesi gerekiyor.",
|
||||
"title": "Toplantıda ziyaretçisiniz"
|
||||
}
|
||||
},
|
||||
"waitingMessage": "Toplantı canlı yayınlanır yayınlanmaz katılacaksınız!"
|
||||
},
|
||||
"volumeSlider": "Ses kaydırıcısı",
|
||||
"welcomepage": {
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
"Remove": "Remove",
|
||||
"Share": "Share",
|
||||
"Submit": "Submit",
|
||||
"Understood": "Understood",
|
||||
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
|
||||
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
|
||||
"WaitingForHostButton": "Wait for moderator",
|
||||
@@ -822,6 +823,8 @@
|
||||
"viewLobby": "View lobby",
|
||||
"viewVisitors": "View visitors",
|
||||
"waitingParticipants": "{{waitingParticipants}} people",
|
||||
"waitingVisitors": "Visitors waiting in queue: {{waitingVisitors}}",
|
||||
"waitingVisitorsTitle": "The meeting is not live yet!",
|
||||
"whiteboardLimitDescription": "Please save your progress, as the user limit will soon be reached and the whiteboard will close.",
|
||||
"whiteboardLimitTitle": "Whiteboard usage"
|
||||
},
|
||||
@@ -835,7 +838,10 @@
|
||||
"audioModeration": "Unmute themselves",
|
||||
"blockEveryoneMicCamera": "Block everyone's mic and camera",
|
||||
"breakoutRooms": "Breakout rooms",
|
||||
"goLive": "Go live",
|
||||
"invite": "Invite Someone",
|
||||
"lowerAllHands": "Lower all hands",
|
||||
"lowerHand": "Lower the hand",
|
||||
"moreModerationActions": "More moderation options",
|
||||
"moreModerationControls": "More moderation controls",
|
||||
"moreParticipantOptions": "More participant options",
|
||||
@@ -852,6 +858,7 @@
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
"visitorInQueue": " (waiting {{count}})",
|
||||
"visitorRequests": " (requests {{count}})",
|
||||
"visitors": "Visitors {{count}}",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
@@ -1486,6 +1493,12 @@
|
||||
},
|
||||
"visitors": {
|
||||
"chatIndicator": "(visitor)",
|
||||
"joinMeeting": {
|
||||
"description": "You're currently an observer in this conference.",
|
||||
"raiseHand": "Raise your hand",
|
||||
"title": "Joining meeting",
|
||||
"wishToSpeak": "If you wish to speak, please raise your hand below and wait for the moderator's approval."
|
||||
},
|
||||
"labelTooltip": "Number of visitors: {{count}}",
|
||||
"notification": {
|
||||
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
|
||||
@@ -1495,7 +1508,8 @@
|
||||
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
|
||||
"notAllowedPromotion": "A participant needs to allow your request first.",
|
||||
"title": "You are a visitor in the meeting"
|
||||
}
|
||||
},
|
||||
"waitingMessage": "You'll join the meeting as soon as it is live!"
|
||||
},
|
||||
"volumeSlider": "Volume slider",
|
||||
"welcomepage": {
|
||||
|
||||
6998
package-lock.json
generated
6998
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -16,7 +16,7 @@
|
||||
"author": "",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"@amplitude/react-native": "2.7.0",
|
||||
"@amplitude/react-native": "2.17.3",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@emotion/react": "11.10.6",
|
||||
"@emotion/styled": "11.10.6",
|
||||
@@ -31,16 +31,17 @@
|
||||
"@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",
|
||||
"@mui/material": "5.12.1",
|
||||
"@react-native-async-storage/async-storage": "1.19.4",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-clipboard/clipboard": "1.14.1",
|
||||
"@react-native-community/netinfo": "11.1.0",
|
||||
"@react-native-community/slider": "4.4.3",
|
||||
"@react-native-google-signin/google-signin": "10.1.0",
|
||||
"@react-navigation/bottom-tabs": "6.5.8",
|
||||
"@react-navigation/elements": "1.3.18",
|
||||
"@react-navigation/material-top-tabs": "6.6.3",
|
||||
"@react-navigation/native": "6.1.7",
|
||||
"@react-navigation/stack": "6.3.17",
|
||||
"@react-navigation/bottom-tabs": "6.6.0",
|
||||
"@react-navigation/elements": "1.3.30",
|
||||
"@react-navigation/material-top-tabs": "6.6.13",
|
||||
"@react-navigation/native": "6.1.17",
|
||||
"@react-navigation/stack": "6.4.0",
|
||||
"@stomp/stompjs": "7.0.0",
|
||||
"@svgr/webpack": "6.3.1",
|
||||
"@tensorflow/tfjs-backend-wasm": "3.13.0",
|
||||
"@tensorflow/tfjs-core": "3.13.0",
|
||||
@@ -66,7 +67,7 @@
|
||||
"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/v1837.0.0+6bcc577a/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1844.0.0+a9b6dd7e/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -77,17 +78,17 @@
|
||||
"punycode": "2.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-emoji-render": "1.2.4",
|
||||
"react-emoji-render": "2.0.1",
|
||||
"react-focus-on": "3.8.1",
|
||||
"react-i18next": "10.11.4",
|
||||
"react-linkify": "1.0.0-alpha",
|
||||
"react-native": "0.72.14",
|
||||
"react-native": "0.73.8",
|
||||
"react-native-background-timer": "2.4.1",
|
||||
"react-native-calendar-events": "2.2.0",
|
||||
"react-native-default-preference": "1.4.4",
|
||||
"react-native-device-info": "10.9.0",
|
||||
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
|
||||
"react-native-gesture-handler": "2.9.0",
|
||||
"react-native-gesture-handler": "2.17.1",
|
||||
"react-native-get-random-values": "1.9.0",
|
||||
"react-native-immersive-mode": "2.0.1",
|
||||
"react-native-keep-awake": "4.0.0",
|
||||
@@ -96,16 +97,16 @@
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.0.0",
|
||||
"react-native-safe-area-context": "4.7.1",
|
||||
"react-native-screens": "3.24.0",
|
||||
"react-native-screens": "3.32.0",
|
||||
"react-native-sound": "0.11.2",
|
||||
"react-native-splash-screen": "3.3.0",
|
||||
"react-native-svg": "13.13.0",
|
||||
"react-native-svg-transformer": "1.1.0",
|
||||
"react-native-svg-transformer": "1.2.0",
|
||||
"react-native-tab-view": "3.5.2",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-video": "6.0.0-alpha.11",
|
||||
"react-native-watch-connectivity": "1.1.0",
|
||||
"react-native-webrtc": "124.0.1",
|
||||
"react-native-webrtc": "124.0.3",
|
||||
"react-native-webview": "13.8.7",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
@@ -126,13 +127,13 @@
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.5",
|
||||
"@babel/eslint-parser": "7.21.8",
|
||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/preset-react": "7.16.0",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/eslint-parser": "7.24.7",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.7",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-react": "7.24.7",
|
||||
"@jitsi/eslint-config": "4.1.10",
|
||||
"@react-native/metro-config": "0.72.12",
|
||||
"@react-native/metro-config": "0.73.5",
|
||||
"@types/amplitude-js": "8.16.5",
|
||||
"@types/audioworklet": "0.0.29",
|
||||
"@types/dom-screen-wake-lock": "1.0.1",
|
||||
@@ -145,7 +146,6 @@
|
||||
"@types/react": "17.0.14",
|
||||
"@types/react-dom": "17.0.14",
|
||||
"@types/react-linkify": "1.0.1",
|
||||
"@types/react-native": "0.69.22",
|
||||
"@types/react-native-keep-awake": "2.0.3",
|
||||
"@types/react-native-video": "5.0.14",
|
||||
"@types/react-redux": "7.1.24",
|
||||
@@ -157,7 +157,7 @@
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-optional-require": "0.3.1",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
"clean-css-cli": "4.3.0",
|
||||
@@ -169,7 +169,7 @@
|
||||
"eslint-plugin-react-native": "4.0.0",
|
||||
"eslint-plugin-typescript-sort-keys": "2.3.0",
|
||||
"jetifier": "1.6.4",
|
||||
"metro-react-native-babel-preset": "0.75.1",
|
||||
"metro-react-native-babel-preset": "0.77.0",
|
||||
"patch-package": "6.4.7",
|
||||
"process": "0.11.10",
|
||||
"sass": "1.26.8",
|
||||
|
||||
@@ -18,7 +18,7 @@ index e4f7e15..6f05fb3 100644
|
||||
+ = [UIApplication sharedApplication].applicationState == UIApplicationStateBackground
|
||||
+ || [UIDevice currentDevice].proximityState;
|
||||
});
|
||||
|
||||
|
||||
+ _inBackground = initialInBackground;
|
||||
+
|
||||
for (NSString *name in @[
|
||||
@@ -34,12 +34,12 @@ index e4f7e15..6f05fb3 100644
|
||||
+ name:UIDeviceProximityStateDidChangeNotification
|
||||
+ object:nil];
|
||||
}
|
||||
|
||||
|
||||
- (void)dealloc
|
||||
@@ -187,6 +195,16 @@ RCT_EXPORT_MODULE()
|
||||
[self startTimers];
|
||||
}
|
||||
|
||||
|
||||
+- (void)proximityChanged
|
||||
+{
|
||||
+ BOOL isClose = [UIDevice currentDevice].proximityState;
|
||||
@@ -30,6 +30,7 @@ interface IEventListeners {
|
||||
onConferenceLeft?: Function;
|
||||
onConferenceWillJoin?: Function;
|
||||
onEnterPictureInPicture?: Function;
|
||||
onEndpointMessageReceived?: Function;
|
||||
onParticipantJoined?: Function;
|
||||
onParticipantLeft?: ({ id }: { id: string }) => void;
|
||||
onReadyToClose?: Function;
|
||||
@@ -133,6 +134,7 @@ export const JitsiMeeting = forwardRef<JitsiRefProps, IAppProps>((props, ref) =>
|
||||
onConferenceWillJoin: eventListeners?.onConferenceWillJoin,
|
||||
onConferenceLeft: eventListeners?.onConferenceLeft,
|
||||
onEnterPictureInPicture: eventListeners?.onEnterPictureInPicture,
|
||||
onEndpointMessageReceived: eventListeners?.onEndpointMessageReceived,
|
||||
onParticipantJoined: eventListeners?.onParticipantJoined,
|
||||
onParticipantLeft: eventListeners?.onParticipantLeft,
|
||||
onReadyToClose: eventListeners?.onReadyToClose
|
||||
|
||||
@@ -57,10 +57,11 @@
|
||||
"@giphy/react-native-sdk": "0.0.0",
|
||||
"@react-native/metro-config": "*",
|
||||
"@react-native-async-storage/async-storage": "0.0.0",
|
||||
"@react-native-community/clipboard": "0.0.0",
|
||||
"@react-native-clipboard/clipboard": "0.0.0",
|
||||
"@react-native-community/netinfo": "0.0.0",
|
||||
"@react-native-community/slider": "0.0.0",
|
||||
"@react-native-google-signin/google-signin": "0.0.0",
|
||||
"@stomp/stompjs": "0.0.0",
|
||||
"react-native": "*",
|
||||
"react": "*",
|
||||
"react-native-background-timer": "0.0.0",
|
||||
|
||||
5
react-native-sdk/update_dependencies.js
vendored
5
react-native-sdk/update_dependencies.js
vendored
@@ -21,11 +21,6 @@ function updateDependencies() {
|
||||
|
||||
for (const key in RNSDKpackageJSON.peerDependencies) {
|
||||
if (!packageJSON.dependencies.hasOwnProperty(key)) {
|
||||
|
||||
if (packageJSON.devDependencies.hasOwnProperty('@react-native/metro-config')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
packageJSON.dependencies[key] = RNSDKpackageJSON.peerDependencies[key];
|
||||
updated = true;
|
||||
}
|
||||
|
||||
@@ -228,6 +228,14 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
|
||||
name: getNormalizedDisplayName(displayName)
|
||||
})));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.SILENT_STATUS_CHANGED,
|
||||
(id: string, isSilent: boolean) => dispatch(participantUpdated({
|
||||
conference,
|
||||
id,
|
||||
isSilent
|
||||
})));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||
(dominant: string, previous: string[], silence: boolean | string) => {
|
||||
|
||||
@@ -706,6 +706,10 @@ function _updateLocalParticipantInConference({ dispatch, getState }: IStore, nex
|
||||
conference.setDisplayName(participant.name);
|
||||
}
|
||||
|
||||
if ('isSilent' in participant) {
|
||||
conference.setIsSilent(participant.isSilent);
|
||||
}
|
||||
|
||||
if ('role' in participant && participant.role === PARTICIPANT_ROLE.MODERATOR) {
|
||||
const { pendingSubjectChange, subject } = getState()['features/base/conference'];
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ export interface IConferenceMetadata {
|
||||
recording?: {
|
||||
isTranscribingEnabled: boolean;
|
||||
};
|
||||
visitors?: {
|
||||
live: boolean;
|
||||
};
|
||||
whiteboard?: {
|
||||
collabDetails: {
|
||||
roomId: string;
|
||||
@@ -132,6 +135,7 @@ export interface IJitsiConference {
|
||||
setAssumedBandwidthBps: (value: number) => void;
|
||||
setDesktopSharingFrameRate: Function;
|
||||
setDisplayName: Function;
|
||||
setIsSilent: Function;
|
||||
setLocalParticipantProperty: Function;
|
||||
setMediaEncryptionKey: Function;
|
||||
setReceiverConstraints: Function;
|
||||
|
||||
@@ -287,6 +287,7 @@ export interface IConfig {
|
||||
disableRemoveRaisedHandOnFocus?: boolean;
|
||||
disableResponsiveTiles?: boolean;
|
||||
disableRtx?: boolean;
|
||||
disableSelfDemote?: boolean;
|
||||
disableSelfView?: boolean;
|
||||
disableSelfViewSettings?: boolean;
|
||||
disableShortcuts?: boolean;
|
||||
@@ -478,6 +479,7 @@ export interface IConfig {
|
||||
peopleSearchQueryTypes?: string[];
|
||||
peopleSearchUrl?: string;
|
||||
preferBosh?: boolean;
|
||||
preferVisitor?: boolean;
|
||||
preferredTranscribeLanguage?: string;
|
||||
prejoinConfig?: {
|
||||
enabled?: boolean;
|
||||
|
||||
@@ -116,6 +116,7 @@ export default [
|
||||
'disableRemoteMute',
|
||||
'disableResponsiveTiles',
|
||||
'disableRtx',
|
||||
'disableSelfDemote',
|
||||
'disableSelfView',
|
||||
'disableSelfViewSettings',
|
||||
'disableShortcuts',
|
||||
@@ -197,6 +198,7 @@ export default [
|
||||
'participantsPane',
|
||||
'pcStatsInterval',
|
||||
'preferBosh',
|
||||
'preferVisitor',
|
||||
'prejoinConfig',
|
||||
'prejoinPageEnabled',
|
||||
'recordingService',
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface IConfigState extends IConfig {
|
||||
audio?: boolean;
|
||||
video?: boolean;
|
||||
};
|
||||
queueService: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { appNavigate } from '../../app/actions.native';
|
||||
import { IStore } from '../../app/types';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
|
||||
import { _connectInternal } from './actions.any';
|
||||
|
||||
@@ -13,7 +16,12 @@ export * from './actions.any';
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function connect(id?: string, password?: string) {
|
||||
return (dispatch: IStore['dispatch']) => dispatch(_connectInternal(id, password));
|
||||
return (dispatch: IStore['dispatch']) => dispatch(_connectInternal(id, password))
|
||||
.catch(error => {
|
||||
if (error === JitsiConnectionErrors.NOT_LIVE_ERROR) {
|
||||
navigateRoot(screen.visitorsQueue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -147,6 +147,13 @@ function _connectionFailed(
|
||||
return state;
|
||||
}
|
||||
|
||||
let preferVisitor;
|
||||
|
||||
if (error.name === JitsiConnectionErrors.NOT_LIVE_ERROR) {
|
||||
// we want to keep the state for the moment when the meeting is live
|
||||
preferVisitor = state.preferVisitor;
|
||||
}
|
||||
|
||||
return assign(state, {
|
||||
connecting: undefined,
|
||||
connection: undefined,
|
||||
@@ -154,7 +161,7 @@ function _connectionFailed(
|
||||
passwordRequired:
|
||||
error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
|
||||
? connection : undefined,
|
||||
preferVisitor: undefined
|
||||
preferVisitor
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -176,10 +176,12 @@ export function validateJwt(jwt: string) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidUnixTimestamp(nbf)) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_INVALID });
|
||||
} else if (currentTimestamp < nbf * 1000) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_FUTURE });
|
||||
if (nbf) { // nbf value is optional
|
||||
if (!isValidUnixTimestamp(nbf)) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_INVALID });
|
||||
} else if (currentTimestamp < nbf * 1000) {
|
||||
errors.push({ key: JWT_VALIDATION_ERRORS.NBF_FUTURE });
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidUnixTimestamp(exp)) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { CALLING, INVITED } from '../../presence-status/constants';
|
||||
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
|
||||
import { CONFERENCE_WILL_JOIN } from '../conference/actionTypes';
|
||||
import { CONFERENCE_JOINED, CONFERENCE_WILL_JOIN } from '../conference/actionTypes';
|
||||
import { forEachConference, getCurrentConference } from '../conference/functions';
|
||||
import { IJitsiConference } from '../conference/reducer';
|
||||
import { SET_CONFIG } from '../config/actionTypes';
|
||||
@@ -201,6 +201,28 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return result;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
const result = next(action);
|
||||
|
||||
const state = store.getState();
|
||||
const { startSilent } = state['features/base/config'];
|
||||
|
||||
if (startSilent) {
|
||||
const localId = getLocalParticipant(store.getState())?.id;
|
||||
|
||||
if (localId) {
|
||||
store.dispatch(participantUpdated({
|
||||
id: localId,
|
||||
local: true,
|
||||
isSilent: startSilent
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
|
||||
const state = store.getState();
|
||||
const { recording, onlySelf } = action;
|
||||
@@ -236,10 +258,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
let queue = getRaiseHandsQueue(store.getState());
|
||||
|
||||
if (participant.raisedHandTimestamp) {
|
||||
queue.push({
|
||||
id: participant.id,
|
||||
raisedHandTimestamp: participant.raisedHandTimestamp
|
||||
});
|
||||
queue = [ ...queue, { id: participant.id,
|
||||
raisedHandTimestamp: participant.raisedHandTimestamp } ];
|
||||
|
||||
// sort the queue before adding to store.
|
||||
queue = queue.sort(({ raisedHandTimestamp: a }, { raisedHandTimestamp: b }) => a - b);
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface IParticipant {
|
||||
isJigasi?: boolean;
|
||||
isReplaced?: boolean;
|
||||
isReplacing?: number;
|
||||
isSilent?: boolean;
|
||||
jwtId?: string;
|
||||
loadableAvatarUrl?: string;
|
||||
loadableAvatarUrlUseCORS?: boolean;
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
* The payload name for remotely setting the camera facing mode message.
|
||||
*/
|
||||
export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message';
|
||||
export const LOWER_HAND_MESSAGE = 'lower-hand-message';
|
||||
|
||||
@@ -111,12 +111,9 @@ const Dialog = ({
|
||||
}, [ onCancel ]);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
if (onSubmit && (
|
||||
(document.activeElement && !operatesWithEnterKey(document.activeElement))
|
||||
|| !document.activeElement
|
||||
)) {
|
||||
if ((document.activeElement && !operatesWithEnterKey(document.activeElement)) || !document.activeElement) {
|
||||
!disableAutoHideOnSubmit && dispatch(hideDialog());
|
||||
onSubmit();
|
||||
onSubmit?.();
|
||||
}
|
||||
}, [ onSubmit ]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Clipboard from '@react-native-community/clipboard';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
|
||||
/**
|
||||
* Tries to copy a given text to the clipboard.
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function BreakoutRoomNamePrompt({ breakoutRoomJid, initialRoomNam
|
||||
const formattedRoomName = roomName?.trim();
|
||||
|
||||
if (formattedRoomName) {
|
||||
dispatch(renameBreakoutRoom(formattedRoomName, roomName));
|
||||
dispatch(renameBreakoutRoom(breakoutRoomJid, formattedRoomName));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
},
|
||||
|
||||
displayNameContainer: {
|
||||
margin: 10
|
||||
margin: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,8 @@ import JitsiPortal from '../../../toolbox/components/web/JitsiPortal';
|
||||
import Toolbox from '../../../toolbox/components/web/Toolbox';
|
||||
import { LAYOUT_CLASSNAMES } from '../../../video-layout/constants';
|
||||
import { getCurrentLayout } from '../../../video-layout/functions.any';
|
||||
import VisitorsQueue from '../../../visitors/components/web/VisitorsQueue';
|
||||
import { showVisitorsQueue } from '../../../visitors/functions';
|
||||
import { init } from '../../actions.web';
|
||||
import { maybeShowSuboptimalExperienceNotification } from '../../functions.web';
|
||||
import {
|
||||
@@ -100,6 +102,11 @@ interface IProps extends AbstractProps, WithTranslation {
|
||||
*/
|
||||
_showPrejoin: boolean;
|
||||
|
||||
/**
|
||||
* If visitors queue page is visible or not.
|
||||
*/
|
||||
_showVisitorsQueue: boolean;
|
||||
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
@@ -206,6 +213,7 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
_overflowDrawer,
|
||||
_showLobby,
|
||||
_showPrejoin,
|
||||
_showVisitorsQueue,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
@@ -257,8 +265,9 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
|
||||
<CalleeInfoContainer />
|
||||
|
||||
{ _showPrejoin && <Prejoin />}
|
||||
{ _showLobby && <LobbyScreen />}
|
||||
{ (_showPrejoin && !_showVisitorsQueue) && <Prejoin />}
|
||||
{ (_showLobby && !_showVisitorsQueue) && <LobbyScreen />}
|
||||
{ _showVisitorsQueue && <VisitorsQueue />}
|
||||
</div>
|
||||
<ParticipantsPane />
|
||||
<ReactionAnimations />
|
||||
@@ -402,7 +411,8 @@ function _mapStateToProps(state: IReduxState) {
|
||||
_overflowDrawer: overflowDrawer,
|
||||
_roomName: getConferenceNameForTitle(state),
|
||||
_showLobby: getIsLobbyVisible(state),
|
||||
_showPrejoin: isPrejoinPageVisible(state)
|
||||
_showPrejoin: isPrejoinPageVisible(state),
|
||||
_showVisitorsQueue: showVisitorsQueue(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import { IReduxState, IStore } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT
|
||||
CONFERENCE_LEFT,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getURLWithoutParamsNormalized } from '../base/connection/utils';
|
||||
@@ -19,10 +20,11 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
import { translateToHTML } from '../base/i18n/functions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
import { pinParticipant, raiseHandClear } from '../base/participants/actions';
|
||||
import { pinParticipant, raiseHand, raiseHandClear } from '../base/participants/actions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
|
||||
import { LOWER_HAND_MESSAGE } from '../base/tracks/constants';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
import { inIframe } from '../base/util/iframeUtils';
|
||||
import { isCalendarEnabled } from '../calendar-sync/functions';
|
||||
@@ -71,6 +73,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { participant, data } = action;
|
||||
const { dispatch } = store;
|
||||
|
||||
if (data.name === LOWER_HAND_MESSAGE && participant.isModerator()) {
|
||||
dispatch(raiseHand(false));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -41,9 +41,7 @@ const initGlobalKeyboardShortcuts = () =>
|
||||
helpCharacter: 'SPACE',
|
||||
helpDescription: 'keyboardShortcuts.pushToTalk',
|
||||
handler: () => {
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
|
||||
logger.log('Talk shortcut released');
|
||||
APP.conference.muteAudio(true);
|
||||
// Handled directly on the global handler.
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -82,7 +80,18 @@ export const initKeyboardShortcuts = () =>
|
||||
(dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
dispatch(initGlobalKeyboardShortcuts());
|
||||
|
||||
window.onkeyup = (e: KeyboardEvent) => {
|
||||
const pttDelay = 50;
|
||||
let pttTimeout: number | undefined;
|
||||
|
||||
// Used to chain the push to talk operations in order to fix an issue when on press we actually need to create
|
||||
// a new track and the release happens before the track is created. In this scenario the release is ignored.
|
||||
// The chaining would also prevent creating multiple new tracks if the space bar is pressed and released
|
||||
// multiple times before the new track creation finish.
|
||||
// TODO: Revisit the fix once we have better track management in LJM. It is possible that we would not need the
|
||||
// chaining at all.
|
||||
let mutePromise = Promise.resolve();
|
||||
|
||||
window.addEventListener('keyup', (e: KeyboardEvent) => {
|
||||
const state = getState();
|
||||
const enabled = areKeyboardShortcutsEnabled(state);
|
||||
const shortcuts = getKeyboardShortcuts(state);
|
||||
@@ -93,12 +102,21 @@ export const initKeyboardShortcuts = () =>
|
||||
|
||||
const key = getKeyboardKey(e).toUpperCase();
|
||||
|
||||
if (key === ' ') {
|
||||
clearTimeout(pttTimeout);
|
||||
pttTimeout = window.setTimeout(() => {
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED));
|
||||
logger.log('Talk shortcut released');
|
||||
mutePromise = mutePromise.then(() => APP.conference.muteAudio(true));
|
||||
}, pttDelay);
|
||||
}
|
||||
|
||||
if (shortcuts.has(key)) {
|
||||
shortcuts.get(key)?.handler(e);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
window.onkeydown = (e: KeyboardEvent) => {
|
||||
window.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const state = getState();
|
||||
const enabled = areKeyboardShortcutsEnabled(state);
|
||||
|
||||
@@ -110,11 +128,12 @@ export const initKeyboardShortcuts = () =>
|
||||
const key = getKeyboardKey(e).toUpperCase();
|
||||
|
||||
if (key === ' ' && !focusedElement) {
|
||||
clearTimeout(pttTimeout);
|
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED));
|
||||
logger.log('Talk shortcut pressed');
|
||||
APP.conference.muteAudio(false);
|
||||
mutePromise = mutePromise.then(() => APP.conference.muteAudio(false));
|
||||
} else if (key === 'ESCAPE') {
|
||||
focusedElement?.blur();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import DialInSummary from '../../../invite/components/dial-in-summary/native/Dia
|
||||
import Prejoin from '../../../prejoin/components/native/Prejoin';
|
||||
import UnsafeRoomWarning from '../../../prejoin/components/native/UnsafeRoomWarning';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
import VisitorsQueue from '../../../visitors/components/native/VisitorsQueue';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
import WelcomePage from '../../../welcome/components/WelcomePage';
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
navigationContainerTheme,
|
||||
preJoinScreenOptions,
|
||||
unsafeMeetingScreenOptions,
|
||||
visitorsScreenOptions,
|
||||
welcomeScreenOptions
|
||||
} from '../screenOptions';
|
||||
|
||||
@@ -105,6 +107,10 @@ const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWel
|
||||
name = { screen.unsafeRoomWarning }
|
||||
options = { unsafeMeetingScreenOptions } />
|
||||
}
|
||||
<RootStack.Screen
|
||||
component = { VisitorsQueue }
|
||||
name = { screen.visitorsQueue }
|
||||
options = { visitorsScreenOptions } />
|
||||
<RootStack.Screen
|
||||
component = { ConferenceNavigationContainer }
|
||||
name = { screen.conference.root }
|
||||
|
||||
@@ -44,6 +44,7 @@ export const screen = {
|
||||
profile: 'Profile'
|
||||
},
|
||||
unsafeRoomWarning: 'Unsafe Room Warning',
|
||||
visitorsQueue: 'Visitors Queue',
|
||||
welcome: {
|
||||
main: 'Welcome',
|
||||
tabs: {
|
||||
|
||||
@@ -55,6 +55,11 @@ export const welcomeScreenOptions = {
|
||||
*/
|
||||
export const conferenceScreenOptions = fullScreenOptions;
|
||||
|
||||
/**
|
||||
* Screen options for visitors queue.
|
||||
*/
|
||||
export const visitorsScreenOptions = fullScreenOptions;
|
||||
|
||||
/**
|
||||
* Tab bar options for chat screen.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
CONFERENCE_FOCUSED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN
|
||||
CONFERENCE_WILL_JOIN,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
} from '../../base/conference/actionTypes';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
|
||||
@@ -29,34 +30,43 @@ const externalAPIEnabled = isExternalAPIAvailable();
|
||||
|
||||
switch (type) {
|
||||
case SET_AUDIO_MUTED:
|
||||
rnSdkHandlers?.onAudioMutedChanged && rnSdkHandlers?.onAudioMutedChanged(action.muted);
|
||||
rnSdkHandlers?.onAudioMutedChanged?.(action.muted);
|
||||
break;
|
||||
case SET_VIDEO_MUTED:
|
||||
rnSdkHandlers?.onVideoMutedChanged && rnSdkHandlers?.onVideoMutedChanged(Boolean(action.muted));
|
||||
rnSdkHandlers?.onVideoMutedChanged?.(Boolean(action.muted));
|
||||
break;
|
||||
case CONFERENCE_BLURRED:
|
||||
rnSdkHandlers?.onConferenceBlurred && rnSdkHandlers?.onConferenceBlurred();
|
||||
rnSdkHandlers?.onConferenceBlurred?.();
|
||||
break;
|
||||
case CONFERENCE_FOCUSED:
|
||||
rnSdkHandlers?.onConferenceFocused && rnSdkHandlers?.onConferenceFocused();
|
||||
rnSdkHandlers?.onConferenceFocused?.();
|
||||
break;
|
||||
case CONFERENCE_JOINED:
|
||||
rnSdkHandlers?.onConferenceJoined && rnSdkHandlers?.onConferenceJoined();
|
||||
rnSdkHandlers?.onConferenceJoined?.();
|
||||
break;
|
||||
case CONFERENCE_LEFT:
|
||||
// Props are torn down at this point, perhaps need to leave this one out
|
||||
break;
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
rnSdkHandlers?.onConferenceWillJoin && rnSdkHandlers?.onConferenceWillJoin();
|
||||
rnSdkHandlers?.onConferenceWillJoin?.();
|
||||
break;
|
||||
case ENTER_PICTURE_IN_PICTURE:
|
||||
rnSdkHandlers?.onEnterPictureInPicture && rnSdkHandlers?.onEnterPictureInPicture();
|
||||
rnSdkHandlers?.onEnterPictureInPicture?.();
|
||||
break;
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { data, participant } = action;
|
||||
|
||||
rnSdkHandlers?.onEndpointMessageReceived?.({
|
||||
data,
|
||||
participant
|
||||
});
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
const { participant } = action;
|
||||
const participantInfo = participantToParticipantInfo(participant);
|
||||
|
||||
rnSdkHandlers?.onParticipantJoined && rnSdkHandlers?.onParticipantJoined(participantInfo);
|
||||
rnSdkHandlers?.onParticipantJoined?.(participantInfo);
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
@@ -64,11 +74,11 @@ const externalAPIEnabled = isExternalAPIAvailable();
|
||||
|
||||
const { id } = participant ?? {};
|
||||
|
||||
rnSdkHandlers?.onParticipantLeft && rnSdkHandlers?.onParticipantLeft({ id });
|
||||
rnSdkHandlers?.onParticipantLeft?.({ id });
|
||||
break;
|
||||
}
|
||||
case READY_TO_CLOSE:
|
||||
rnSdkHandlers?.onReadyToClose && rnSdkHandlers?.onReadyToClose();
|
||||
rnSdkHandlers?.onReadyToClose?.();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,9 @@ const notification = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: 'auto',
|
||||
paddingBottom: BaseTheme.spacing[2],
|
||||
paddingHorizontal: BaseTheme.spacing[2],
|
||||
maxWidth: 432,
|
||||
width: 'auto'
|
||||
marginVertical: BaseTheme.spacing[1],
|
||||
maxWidth: 416,
|
||||
width: '100%'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -178,6 +178,7 @@ const Notification = ({
|
||||
description,
|
||||
descriptionArguments,
|
||||
descriptionKey,
|
||||
disableClosing,
|
||||
hideErrorSupportLink,
|
||||
icon,
|
||||
onDismissed,
|
||||
@@ -336,14 +337,16 @@ const Notification = ({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
className = { classes.closeIcon }
|
||||
color = { theme.palette.icon04 }
|
||||
id = 'close-notification'
|
||||
onClick = { onDismiss }
|
||||
size = { 20 }
|
||||
src = { IconCloseLarge }
|
||||
testId = { `${titleKey || descriptionKey}-dismiss` } />
|
||||
{ !disableClosing && (
|
||||
<Icon
|
||||
className = { classes.closeIcon }
|
||||
color = { theme.palette.icon04 }
|
||||
id = 'close-notification'
|
||||
onClick = { onDismiss }
|
||||
size = { 20 }
|
||||
src = { IconCloseLarge }
|
||||
testId = { `${titleKey || descriptionKey}-dismiss` } />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -104,12 +104,19 @@ export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION';
|
||||
export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
|
||||
|
||||
/**
|
||||
* The identifier of the lobby notification.
|
||||
* The identifier of the visitors promotion notification.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const VISITORS_PROMOTION_NOTIFICATION_ID = 'VISITORS_PROMOTION_NOTIFICATION';
|
||||
|
||||
/**
|
||||
* The identifier of the visitors notification indicating the meeting is not live.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const VISITORS_NOT_LIVE_NOTIFICATION_ID = 'VISITORS_NOT_LIVE_NOTIFICATION_ID';
|
||||
|
||||
/**
|
||||
* Amount of participants beyond which no join notification will be emitted.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface INotificationProps {
|
||||
description?: string | React.ReactNode;
|
||||
descriptionArguments?: Object;
|
||||
descriptionKey?: string;
|
||||
disableClosing?: boolean;
|
||||
hideErrorSupportLink?: boolean;
|
||||
icon?: string;
|
||||
maxLines?: number;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
import { JitsiConferenceErrors, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
isFatalJitsiConferenceError,
|
||||
isFatalJitsiConnectionError
|
||||
@@ -8,7 +8,6 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { openPageReloadDialog } from './actions';
|
||||
|
||||
|
||||
/**
|
||||
* Error type. Basically like Error, but augmented with a recoverable property.
|
||||
*/
|
||||
@@ -34,6 +33,7 @@ type ErrorType = {
|
||||
* List of errors that are not fatal (or handled differently) so then the page reload dialog won't kick in.
|
||||
*/
|
||||
const RN_NO_RELOAD_DIALOG_ERRORS = [
|
||||
JitsiConnectionErrors.NOT_LIVE_ERROR,
|
||||
JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED,
|
||||
JitsiConferenceErrors.CONFERENCE_DESTROYED,
|
||||
JitsiConferenceErrors.CONNECTION_ERROR,
|
||||
|
||||
@@ -15,12 +15,16 @@ import {
|
||||
isEnabled as isAvModerationEnabled,
|
||||
isSupported as isAvModerationSupported
|
||||
} from '../../../av-moderation/functions';
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { hideSheet, openDialog } from '../../../base/dialog/actions';
|
||||
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCheck, IconVideoOff } from '../../../base/icons/svg';
|
||||
import { IconCheck, IconRaiseHand, IconVideoOff } from '../../../base/icons/svg';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { getParticipantCount, isEveryoneModerator } from '../../../base/participants/functions';
|
||||
import { raiseHand } from '../../../base/participants/actions';
|
||||
import { getParticipantCount, getRaiseHandsQueue, isEveryoneModerator, isLocalParticipantModerator }
|
||||
from '../../../base/participants/functions';
|
||||
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
|
||||
import MuteEveryonesVideoDialog
|
||||
from '../../../video-menu/components/native/MuteEveryonesVideoDialog';
|
||||
|
||||
@@ -32,6 +36,14 @@ export const ContextMenuMore = () => {
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog));
|
||||
dispatch(hideSheet());
|
||||
}, [ dispatch ]);
|
||||
const conference = useSelector(getCurrentConference);
|
||||
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
|
||||
const moderator = useSelector(isLocalParticipantModerator);
|
||||
const lowerAllHands = useCallback(() => {
|
||||
dispatch(raiseHand(false));
|
||||
conference?.sendEndpointMessage('', { name: LOWER_HAND_MESSAGE });
|
||||
dispatch(hideSheet());
|
||||
}, [ dispatch ]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
|
||||
@@ -59,6 +71,14 @@ export const ContextMenuMore = () => {
|
||||
src = { IconVideoOff } />
|
||||
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.stopEveryonesVideo')}</Text>
|
||||
</TouchableOpacity>
|
||||
{ moderator && raisedHandsQueue.length !== 0 && <TouchableOpacity
|
||||
onPress = { lowerAllHands }
|
||||
style = { styles.contextMenuItem as ViewStyle }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconRaiseHand } />
|
||||
<Text style = { styles.contextMenuItemText }>{t('participantsPane.actions.lowerAllHands')}</Text>
|
||||
</TouchableOpacity> }
|
||||
{isModerationSupported && ((participantCount === 1 || !allModerators)) && <>
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.divider } />
|
||||
|
||||
@@ -3,17 +3,21 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_MODES, BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { admitMultiple } from '../../../visitors/actions';
|
||||
import { getPromotionRequests } from '../../../visitors/functions';
|
||||
import { admitMultiple, goLive } from '../../../visitors/actions';
|
||||
import {
|
||||
getPromotionRequests,
|
||||
getVisitorsCount,
|
||||
getVisitorsInQueueCount,
|
||||
isVisitorsLive
|
||||
} from '../../../visitors/functions';
|
||||
|
||||
import { VisitorsItem } from './VisitorsItem';
|
||||
import styles from './styles';
|
||||
|
||||
const VisitorsList = () => {
|
||||
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -22,9 +26,16 @@ const VisitorsList = () => {
|
||||
const admitAll = useCallback(() => {
|
||||
dispatch(admitMultiple(requests));
|
||||
}, [ dispatch, requests ]);
|
||||
const goLiveCb = useCallback(() => {
|
||||
dispatch(goLive());
|
||||
}, [ dispatch ]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (visitorsCount <= 0) {
|
||||
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
|
||||
const isLive = useSelector(isVisitorsLive);
|
||||
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
|
||||
|
||||
if (visitorsCount <= 0 && !showVisitorsInQueue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -34,6 +45,10 @@ const VisitorsList = () => {
|
||||
title += t('participantsPane.headings.visitorRequests', { count: requests.length });
|
||||
}
|
||||
|
||||
if (showVisitorsInQueue) {
|
||||
title += t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { styles.listDetails as ViewStyle } >
|
||||
@@ -41,7 +56,7 @@ const VisitorsList = () => {
|
||||
{ title }
|
||||
</Text>
|
||||
{
|
||||
requests.length > 1 && (
|
||||
requests.length > 1 && !showVisitorsInQueue && (
|
||||
<Button
|
||||
accessibilityLabel = 'participantsPane.actions.admitAll'
|
||||
labelKey = 'participantsPane.actions.admitAll'
|
||||
@@ -50,6 +65,16 @@ const VisitorsList = () => {
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)
|
||||
}
|
||||
{
|
||||
showVisitorsInQueue && (
|
||||
<Button
|
||||
accessibilityLabel = 'participantsPane.actions.goLive'
|
||||
labelKey = 'participantsPane.actions.goLive'
|
||||
mode = { BUTTON_MODES.TEXT }
|
||||
onClick = { goLiveCb }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)
|
||||
}
|
||||
</View>
|
||||
{
|
||||
requests.map(r => (
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import {
|
||||
getParticipantCount,
|
||||
getRaiseHandsQueue,
|
||||
isEveryoneModerator
|
||||
} from '../../../base/participants/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
@@ -32,6 +33,7 @@ import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { openSettingsDialog } from '../../../settings/actions.web';
|
||||
import { SETTINGS_TABS } from '../../../settings/constants';
|
||||
import { shouldShowModeratorSettings } from '../../../settings/functions.web';
|
||||
import LowerHandButton from '../../../video-menu/components/web/LowerHandButton';
|
||||
import MuteEveryonesVideoDialog from '../../../video-menu/components/web/MuteEveryonesVideoDialog';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
@@ -85,6 +87,7 @@ interface IProps {
|
||||
export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
|
||||
const raisedHandsQueue = useSelector(getRaiseHandsQueue);
|
||||
const allModerators = useSelector(isEveryoneModerator);
|
||||
const isModeratorSettingsTabEnabled = useSelector(shouldShowModeratorSettings);
|
||||
const participantCount = useSelector(getParticipantCount);
|
||||
@@ -147,6 +150,7 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
|
||||
onClick: muteAllVideo,
|
||||
text: t('participantsPane.actions.stopEveryonesVideo')
|
||||
} ] } />
|
||||
{raisedHandsQueue.length !== 0 && <LowerHandButton />}
|
||||
{!isBreakoutRoom && isModerationSupported && (participantCount === 1 || !allModerators) && (
|
||||
<ContextMenuItemGroup actions = { actions }>
|
||||
<div className = { classes.text }>
|
||||
|
||||
@@ -3,10 +3,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { admitMultiple } from '../../../visitors/actions';
|
||||
import { getPromotionRequests } from '../../../visitors/functions';
|
||||
import { admitMultiple, goLive } from '../../../visitors/actions';
|
||||
import {
|
||||
getPromotionRequests,
|
||||
getVisitorsCount,
|
||||
getVisitorsInQueueCount,
|
||||
isVisitorsLive
|
||||
} from '../../../visitors/functions';
|
||||
|
||||
import { VisitorsItem } from './VisitorsItem';
|
||||
|
||||
@@ -66,7 +70,10 @@ const useStyles = makeStyles()(theme => {
|
||||
*/
|
||||
export default function VisitorsList() {
|
||||
const requests = useSelector(getPromotionRequests);
|
||||
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
|
||||
const isLive = useSelector(isVisitorsLive);
|
||||
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { classes, cx } = useStyles();
|
||||
@@ -76,7 +83,11 @@ export default function VisitorsList() {
|
||||
dispatch(admitMultiple(requests));
|
||||
}, [ dispatch, requests ]);
|
||||
|
||||
if (visitorsCount <= 0) {
|
||||
const goLiveCb = useCallback(() => {
|
||||
dispatch(goLive());
|
||||
}, [ dispatch ]);
|
||||
|
||||
if (visitorsCount <= 0 && !showVisitorsInQueue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -87,12 +98,20 @@ export default function VisitorsList() {
|
||||
{ t('participantsPane.headings.visitors', { count: visitorsCount })}
|
||||
{ requests.length > 0
|
||||
&& t('participantsPane.headings.visitorRequests', { count: requests.length }) }
|
||||
{ showVisitorsInQueue
|
||||
&& t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount }) }
|
||||
</div>
|
||||
{
|
||||
requests.length > 1
|
||||
requests.length > 1 && !showVisitorsInQueue // Go live button is with higher priority
|
||||
&& <div
|
||||
className = { classes.link }
|
||||
onClick = { admitAll }>{t('participantsPane.actions.admitAll')}</div>
|
||||
onClick = { admitAll }>{ t('participantsPane.actions.admitAll') }</div>
|
||||
}
|
||||
{
|
||||
showVisitorsInQueue
|
||||
&& <div
|
||||
className = { classes.link }
|
||||
onClick = { goLiveCb }>{ t('participantsPane.actions.goLive') }</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -63,6 +63,10 @@ export function getParticipantAudioMediaState(participant: IParticipant | undefi
|
||||
muted: Boolean, state: IReduxState) {
|
||||
const dominantSpeaker = getDominantSpeakerParticipant(state);
|
||||
|
||||
if (participant?.isSilent) {
|
||||
return MEDIA_STATE.NONE;
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||
return MEDIA_STATE.FORCE_MUTED;
|
||||
@@ -146,9 +150,10 @@ export function getQuickActionButtonType(
|
||||
state: IReduxState) {
|
||||
// handled only by moderators
|
||||
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||
const isParticipantSilent = participant?.isSilent || false;
|
||||
|
||||
if (isLocalParticipantModerator(state)) {
|
||||
if (!isAudioMuted) {
|
||||
if (!isAudioMuted && !isParticipantSilent) {
|
||||
return QUICK_ACTION_BUTTON.MUTE;
|
||||
}
|
||||
if (!isVideoMuted) {
|
||||
@@ -157,7 +162,7 @@ export function getQuickActionButtonType(
|
||||
if (isVideoForceMuted) {
|
||||
return QUICK_ACTION_BUTTON.ALLOW_VIDEO;
|
||||
}
|
||||
if (isSupported()(state)) {
|
||||
if (isSupported()(state) && !isParticipantSilent) {
|
||||
return QUICK_ACTION_BUTTON.ASK_TO_UNMUTE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FlatList, Platform, View, ViewStyle } from 'react-native';
|
||||
import { TextInput } from 'react-native-gesture-handler';
|
||||
import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { connect } from '../base/connection/actions.native';
|
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { showVisitorsQueue } from '../visitors/functions';
|
||||
|
||||
/**
|
||||
* Action used to start the conference.
|
||||
@@ -8,7 +12,12 @@ import { IStore } from '../app/types';
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function joinConference(options?: Object, _ignoreJoiningInProgress = false) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
return async function(_dispatch: IStore['dispatch'], _getState: IStore['getState']) {
|
||||
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const _showVisitorsQueue = showVisitorsQueue(getState);
|
||||
|
||||
if (_showVisitorsQueue) {
|
||||
dispatch(connect());
|
||||
navigateRoot(screen.conference.root);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ interface IProps extends AbstractButtonProps {
|
||||
*/
|
||||
_raisedHand: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the click is disabled.
|
||||
*/
|
||||
disableClick?: boolean;
|
||||
|
||||
/**
|
||||
* Used to close the overflow menu after raise hand is clicked.
|
||||
*/
|
||||
@@ -75,8 +80,14 @@ class RaiseHandButton extends Component<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
const { disableClick, onCancel } = this.props;
|
||||
|
||||
if (disableClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toggleRaisedHand();
|
||||
this.props.onCancel();
|
||||
onCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,4 +170,23 @@ function _mapStateToProps(state: IReduxState) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _standaloneMapStateToProps(state: IReduxState) {
|
||||
const _enabled = getFeatureFlag(state, RAISE_HAND_ENABLED, true);
|
||||
|
||||
return {
|
||||
_enabled
|
||||
};
|
||||
}
|
||||
|
||||
const StandaloneRaiseHandButton = translate(connect(_standaloneMapStateToProps)(RaiseHandButton));
|
||||
|
||||
export { StandaloneRaiseHandButton };
|
||||
|
||||
export default translate(connect(_mapStateToProps)(RaiseHandButton));
|
||||
|
||||
@@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import { raiseHand } from '../../../base/participants/actions';
|
||||
@@ -15,6 +15,16 @@ import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/too
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether or not the click is disabled.
|
||||
*/
|
||||
disableClick?: boolean;
|
||||
|
||||
/**
|
||||
* Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the hand is raised.
|
||||
*/
|
||||
@@ -51,7 +61,11 @@ class RaiseHandButton extends AbstractButton<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, raisedHand } = this.props;
|
||||
const { disableClick, dispatch, raisedHand } = this.props;
|
||||
|
||||
if (disableClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendAnalytics(createToolbarEvent(
|
||||
'raise.hand',
|
||||
@@ -76,4 +90,6 @@ const mapStateToProps = (state: IReduxState) => {
|
||||
};
|
||||
};
|
||||
|
||||
export { RaiseHandButton };
|
||||
|
||||
export default translate(connect(mapStateToProps)(RaiseHandButton));
|
||||
|
||||
@@ -32,7 +32,10 @@ import {
|
||||
START_LOCAL_RECORDING,
|
||||
STOP_LOCAL_RECORDING
|
||||
} from './actionTypes';
|
||||
import { START_RECORDING_NOTIFICATION_ID } from './constants';
|
||||
import {
|
||||
RECORDING_METADATA_ID,
|
||||
START_RECORDING_NOTIFICATION_ID
|
||||
} from './constants';
|
||||
import {
|
||||
getRecordButtonProps,
|
||||
getRecordingLink,
|
||||
@@ -451,6 +454,9 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
|
||||
});
|
||||
|
||||
if (autoTranscribeOnRecord) {
|
||||
conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
dispatch(setRequestingSubtitles(true, false, null));
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,7 @@ const useStyles = makeStyles()(theme => {
|
||||
},
|
||||
|
||||
'&::after': {
|
||||
content: '',
|
||||
content: '""',
|
||||
backgroundColor: theme.palette.ui01,
|
||||
marginBottom: 'env(safe-area-inset-bottom, 0)'
|
||||
}
|
||||
|
||||
@@ -141,12 +141,13 @@ class LocalVideoMenu extends PureComponent<IProps> {
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { disableSelfDemote } = state['features/base/config'];
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_participant: participant,
|
||||
_participantDisplayName: getParticipantDisplayName(state, participant?.id ?? ''),
|
||||
_showDemote: getParticipantCount(state) > 1
|
||||
_showDemote: !disableSelfDemote && getParticipantCount(state) > 1
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { IJitsiConference } from '../../../base/conference/reducer';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The current conference.
|
||||
*/
|
||||
_conference: IJitsiConference | undefined;
|
||||
|
||||
/**
|
||||
* The ID of the participant object that this button is supposed to
|
||||
* ask to lower the hand.
|
||||
*/
|
||||
participantId: String | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for lowering certain
|
||||
* participant raised hands.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
class LowerHandButton extends AbstractButton<IProps> {
|
||||
icon = IconRaiseHand;
|
||||
accessibilityLabel = 'participantsPane.actions.lowerHand';
|
||||
label = 'participantsPane.actions.lowerHand';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and asks the participant to lower hand.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { participantId, _conference } = this.props;
|
||||
|
||||
_conference?.sendEndpointMessage(
|
||||
participantId,
|
||||
{
|
||||
name: LOWER_HAND_MESSAGE
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { participantID } = ownProps;
|
||||
const currentConference = getCurrentConference(state);
|
||||
|
||||
return {
|
||||
_conference: currentConference,
|
||||
participantId: participantID
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(LowerHandButton));
|
||||
@@ -16,6 +16,7 @@ import { translate } from '../../../base/i18n/functions';
|
||||
import {
|
||||
getParticipantById,
|
||||
getParticipantDisplayName,
|
||||
hasRaisedHand,
|
||||
isLocalParticipantModerator
|
||||
} from '../../../base/participants/functions';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
@@ -27,6 +28,7 @@ import ConnectionStatusButton from './ConnectionStatusButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import LowerHandButton from './LowerHandButton';
|
||||
import MuteButton from './MuteButton';
|
||||
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
|
||||
import MuteVideoButton from './MuteVideoButton';
|
||||
@@ -78,6 +80,11 @@ interface IProps {
|
||||
*/
|
||||
_isParticipantAvailable?: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the targeted participant joined without audio.
|
||||
*/
|
||||
_isParticipantSilent: boolean;
|
||||
|
||||
/**
|
||||
* Whether the local participant is moderator or not.
|
||||
*/
|
||||
@@ -88,6 +95,11 @@ interface IProps {
|
||||
*/
|
||||
_participantDisplayName: string;
|
||||
|
||||
/**
|
||||
* Whether the targeted participant raised hand or not.
|
||||
*/
|
||||
_raisedHand: boolean;
|
||||
|
||||
/**
|
||||
* Array containing the breakout rooms.
|
||||
*/
|
||||
@@ -143,7 +155,9 @@ class RemoteVideoMenu extends PureComponent<IProps> {
|
||||
_disableGrantModerator,
|
||||
_isBreakoutRoom,
|
||||
_isParticipantAvailable,
|
||||
_isParticipantSilent,
|
||||
_moderator,
|
||||
_raisedHand,
|
||||
_rooms,
|
||||
_showDemote,
|
||||
_currentRoomId,
|
||||
@@ -166,10 +180,11 @@ class RemoteVideoMenu extends PureComponent<IProps> {
|
||||
<BottomSheet
|
||||
renderHeader = { this._renderMenuHeader }
|
||||
showSlidingView = { _isParticipantAvailable }>
|
||||
<AskUnmuteButton { ...buttonProps } />
|
||||
{!_isParticipantSilent && <AskUnmuteButton { ...buttonProps } />}
|
||||
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
|
||||
<MuteEveryoneElseButton { ...buttonProps } />
|
||||
{ !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }
|
||||
{ _moderator && _raisedHand && <LowerHandButton { ...buttonProps } /> }
|
||||
{ !_disableRemoteMute && !_isParticipantSilent && <MuteVideoButton { ...buttonProps } /> }
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.divider as ViewStyle } />
|
||||
{ !_disableKick && <KickButton { ...buttonProps } /> }
|
||||
@@ -242,7 +257,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const kickOutEnabled = getFeatureFlag(state, KICK_OUT_ENABLED, true);
|
||||
const { participantId } = ownProps;
|
||||
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
|
||||
const isParticipantAvailable = getParticipantById(state, participantId);
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const { disableKick, disablePrivateChat } = remoteVideoMenu;
|
||||
const _rooms = Object.values(getBreakoutRooms(state));
|
||||
const _currentRoomId = getCurrentRoomId(state);
|
||||
@@ -250,6 +265,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const moderator = isLocalParticipantModerator(state);
|
||||
const _iAmVisitor = state['features/visitors'].iAmVisitor;
|
||||
const _isBreakoutRoom = isInBreakoutRoom(state);
|
||||
const raisedHand = hasRaisedHand(participant);
|
||||
|
||||
return {
|
||||
_currentRoomId,
|
||||
@@ -257,9 +273,11 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_disableRemoteMute: Boolean(disableRemoteMute),
|
||||
_disablePrivateChat: Boolean(disablePrivateChat) || _iAmVisitor,
|
||||
_isBreakoutRoom,
|
||||
_isParticipantAvailable: Boolean(isParticipantAvailable),
|
||||
_isParticipantAvailable: Boolean(participant),
|
||||
_isParticipantSilent: Boolean(participant?.isSilent),
|
||||
_moderator: moderator,
|
||||
_participantDisplayName: getParticipantDisplayName(state, participantId),
|
||||
_raisedHand: raisedHand,
|
||||
_rooms,
|
||||
_showDemote: moderator
|
||||
};
|
||||
|
||||
@@ -268,7 +268,7 @@ const LocalVideoMenuTriggerButton = ({
|
||||
function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { thumbnailType } = ownProps;
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { disableLocalVideoFlip, disableSelfViewSettings } = state['features/base/config'];
|
||||
const { disableLocalVideoFlip, disableSelfDemote, disableSelfViewSettings } = state['features/base/config'];
|
||||
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
|
||||
const { overflowDrawer } = state['features/toolbox'];
|
||||
const { showConnectionInfo } = state['features/base/connection'];
|
||||
@@ -292,7 +292,7 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
|
||||
return {
|
||||
_menuPosition,
|
||||
_showDemote: getParticipantCount(state) > 1,
|
||||
_showDemote: !disableSelfDemote && getParticipantCount(state) > 1,
|
||||
_showLocalVideoFlipButton: !disableLocalVideoFlip && videoTrack?.videoType !== 'desktop',
|
||||
_showHideSelfViewButton: showHideSelfViewButton,
|
||||
_overflowDrawer: overflowDrawer,
|
||||
|
||||
56
react/features/video-menu/components/web/LowerHandButton.tsx
Normal file
56
react/features/video-menu/components/web/LowerHandButton.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { IconRaiseHand } from '../../../base/icons/svg';
|
||||
import { raiseHand } from '../../../base/participants/actions';
|
||||
import { LOWER_HAND_MESSAGE } from '../../../base/tracks/constants';
|
||||
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The ID of the participant that's linked to the button.
|
||||
*/
|
||||
participantID?: String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for notifying certain
|
||||
* participant who raised hand to lower hand.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const LowerHandButton = ({
|
||||
participantID = ''
|
||||
}: IProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const currentConference = useSelector(getCurrentConference);
|
||||
const accessibilityText = participantID
|
||||
? t('participantsPane.actions.lowerHand')
|
||||
: t('participantsPane.actions.lowerAllHands');
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!participantID) {
|
||||
dispatch(raiseHand(false));
|
||||
}
|
||||
currentConference?.sendEndpointMessage(
|
||||
participantID,
|
||||
{
|
||||
name: LOWER_HAND_MESSAGE
|
||||
}
|
||||
);
|
||||
}, [ participantID ]);
|
||||
|
||||
return (
|
||||
<ContextMenuItem
|
||||
accessibilityLabel = { accessibilityText }
|
||||
icon = { IconRaiseHand }
|
||||
onClick = { handleClick }
|
||||
text = { accessibilityText } />
|
||||
);
|
||||
};
|
||||
|
||||
export default LowerHandButton;
|
||||
@@ -9,7 +9,7 @@ import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
@@ -33,6 +33,7 @@ import CustomOptionButton from './CustomOptionButton';
|
||||
import DemoteToVisitorButton from './DemoteToVisitorButton';
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import LowerHandButton from './LowerHandButton';
|
||||
import MuteButton from './MuteButton';
|
||||
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
|
||||
import MuteEveryoneElsesVideoButton from './MuteEveryoneElsesVideoButton';
|
||||
@@ -148,6 +149,7 @@ const ParticipantContextMenu = ({
|
||||
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
|
||||
const isBreakoutRoom = useSelector(isInBreakoutRoom);
|
||||
const isModerationSupported = useSelector((state: IReduxState) => isAvModerationSupported()(state));
|
||||
const raisedHands = hasRaisedHand(participant);
|
||||
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
|
||||
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
@@ -214,7 +216,7 @@ const ParticipantContextMenu = ({
|
||||
|
||||
if (_isModerator) {
|
||||
if (isModerationSupported) {
|
||||
if (_isAudioMuted
|
||||
if (_isAudioMuted && !participant.isSilent
|
||||
&& !(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.ASK_TO_UNMUTE)) {
|
||||
buttons.push(<AskToUnmuteButton
|
||||
{ ...getButtonProps(BUTTONS.ASK_UNMUTE) }
|
||||
@@ -230,7 +232,7 @@ const ParticipantContextMenu = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!disableRemoteMute) {
|
||||
if (!disableRemoteMute && !participant.isSilent) {
|
||||
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) {
|
||||
buttons.push(<MuteButton { ...getButtonProps(BUTTONS.MUTE) } />);
|
||||
}
|
||||
@@ -241,6 +243,10 @@ const ParticipantContextMenu = ({
|
||||
buttons.push(<MuteEveryoneElsesVideoButton { ...getButtonProps(BUTTONS.MUTE_OTHERS_VIDEO) } />);
|
||||
}
|
||||
|
||||
if (raisedHands) {
|
||||
buttons2.push(<LowerHandButton { ...getButtonProps(BUTTONS.LOWER_PARTICIPANT_HAND) } />);
|
||||
}
|
||||
|
||||
if (!disableGrantModerator && !isBreakoutRoom) {
|
||||
buttons2.push(<GrantModeratorButton { ...getButtonProps(BUTTONS.GRANT_MODERATOR) } />);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ export const PARTICIPANT_MENU_BUTTONS = {
|
||||
GRANT_MODERATOR: 'grant-moderator',
|
||||
HIDE_SELF_VIEW: 'hide-self-view',
|
||||
KICK: 'kick',
|
||||
LOWER_PARTICIPANT_HAND: 'lower-participant-hand',
|
||||
MUTE: 'mute',
|
||||
MUTE_OTHERS: 'mute-others',
|
||||
MUTE_OTHERS_VIDEO: 'mute-others-video',
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
*/
|
||||
export const UPDATE_VISITORS_COUNT = 'UPDATE_VISITORS_COUNT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action to update visitors in queue count.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
* count: number
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_VISITORS_IN_QUEUE_COUNT = 'UPDATE_VISITORS_IN_QUEUE_COUNT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which enables/disables visitors UI mode.
|
||||
*
|
||||
@@ -39,6 +49,16 @@ export const VISITOR_PROMOTION_REQUEST = 'VISITOR_PROMOTION_REQUEST';
|
||||
*/
|
||||
export const CLEAR_VISITOR_PROMOTION_REQUEST = 'CLEAR_VISITOR_PROMOTION_REQUEST';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets in visitor's queue.
|
||||
*
|
||||
* {
|
||||
* type: SET_IN_VISITORS_QUEUE,
|
||||
* value: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_IN_VISITORS_QUEUE = 'SET_IN_VISITORS_QUEUE';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets visitor demote actor.
|
||||
*
|
||||
|
||||
@@ -8,9 +8,11 @@ import { getLocalParticipant } from '../base/participants/functions';
|
||||
import {
|
||||
CLEAR_VISITOR_PROMOTION_REQUEST,
|
||||
I_AM_VISITOR_MODE,
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
UPDATE_VISITORS_COUNT,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
@@ -150,6 +152,21 @@ export function setIAmVisitor(enabled: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets in visitor's queue.
|
||||
*
|
||||
* @param {boolean} value - The new value.
|
||||
* @returns {{
|
||||
* type: SET_IN_VISITORS_QUEUE,
|
||||
* }}
|
||||
*/
|
||||
export function setInVisitorsQueue(value: boolean) {
|
||||
return {
|
||||
type: SET_IN_VISITORS_QUEUE,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets visitor demote actor.
|
||||
*
|
||||
@@ -194,3 +211,34 @@ export function updateVisitorsCount(count: number) {
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitors in queue count has been updated.
|
||||
*
|
||||
* @param {number} count - The new visitors in queue count.
|
||||
* @returns {{
|
||||
* type: UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
* }}
|
||||
*/
|
||||
export function updateVisitorsInQueueCount(count: number) {
|
||||
return {
|
||||
type: UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the overflow menu if opened.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
export function goLive() {
|
||||
return (_: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('visitors', {
|
||||
live: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
1
react/features/visitors/components/index.native.ts
Normal file
1
react/features/visitors/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as JoinMeetingDialog } from './native/JoinMeetingDialog';
|
||||
1
react/features/visitors/components/index.web.ts
Normal file
1
react/features/visitors/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as JoinMeetingDialog } from './web/JoinMeetingDialog';
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import Dialog from 'react-native-dialog';
|
||||
|
||||
import { StandaloneRaiseHandButton as RaiseHandButton } from '../../../reactions/components/native/RaiseHandButton';
|
||||
import styles from '../../components/native/styles';
|
||||
|
||||
/**
|
||||
* Component that renders the join meeting dialog for visitors.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function JoinMeetingDialog() {
|
||||
const { t } = useTranslation();
|
||||
const [ visible, setVisible ] = useState(true);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
setVisible(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog.Container
|
||||
coverScreen = { false }
|
||||
visible = { visible }>
|
||||
<Dialog.Title>{ t('visitors.joinMeeting.title') }</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{ t('visitors.joinMeeting.description') }
|
||||
<View style = { styles.raiseHandButton as ViewStyle }>
|
||||
{/* @ts-ignore */}
|
||||
<RaiseHandButton disableClick = { true } />
|
||||
</View>
|
||||
</Dialog.Description>
|
||||
<Dialog.Description>{t('visitors.joinMeeting.wishToSpeak')}</Dialog.Description>
|
||||
<Dialog.Button
|
||||
label = { t('dialog.Understood') }
|
||||
onPress = { closeDialog } />
|
||||
</Dialog.Container>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/native/Label';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import { getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
import { getVisitorsCount, getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
|
||||
const styles = {
|
||||
raisedHandsCountLabel: {
|
||||
@@ -25,8 +25,7 @@ const styles = {
|
||||
|
||||
const VisitorsCountLabel = () => {
|
||||
const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
|
||||
const visitorsCount = useSelector((state: IReduxState) =>
|
||||
state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
|
||||
return !visitorsMode && visitorsCount > 0 ? (
|
||||
<Label
|
||||
|
||||
27
react/features/visitors/components/native/VisitorsQueue.tsx
Normal file
27
react/features/visitors/components/native/VisitorsQueue.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import styles from '../../../lobby/components/native/styles';
|
||||
|
||||
/**
|
||||
* The component that renders visitors queue UI.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function VisitorsQueue() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style = { styles.lobbyWaitingFragmentContainer }>
|
||||
<Text style = { styles.lobbyTitle }>
|
||||
{ t('visitors.waitingMessage') }
|
||||
</Text>
|
||||
<LoadingIndicator
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
style = { styles.loadingIndicator } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
12
react/features/visitors/components/native/styles.ts
Normal file
12
react/features/visitors/components/native/styles.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The styles of the feature visitors.
|
||||
*/
|
||||
export default {
|
||||
|
||||
raiseHandButton: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '100%'
|
||||
}
|
||||
};
|
||||
75
react/features/visitors/components/web/JoinMeetingDialog.tsx
Normal file
75
react/features/visitors/components/web/JoinMeetingDialog.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { noop } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IconArrowUp } from '../../../base/icons/svg';
|
||||
import ToolboxButtonWithPopup from '../../../base/toolbox/components/web/ToolboxButtonWithPopup';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { RaiseHandButton } from '../../../reactions/components/web/RaiseHandButton';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
raiseHand: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
pointerEvents: 'none'
|
||||
},
|
||||
raiseHandTooltip: {
|
||||
border: '1px solid #444',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
paddingBottom: theme.spacing(1),
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2)
|
||||
},
|
||||
raiseHandButton: {
|
||||
display: 'inline-block',
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
position: 'relative'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders the join meeting dialog for visitors.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function JoinMeetingDialog() {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ translationKey: 'dialog.Understood' }}
|
||||
titleKey = 'visitors.joinMeeting.title'>
|
||||
<div className = 'join-meeting-dialog'>
|
||||
<p>{t('visitors.joinMeeting.description')}</p>
|
||||
<div className = { classes.raiseHand }>
|
||||
<p className = { classes.raiseHandTooltip }>{t('visitors.joinMeeting.raiseHand')}</p>
|
||||
<div className = { classes.raiseHandButton }>
|
||||
<ToolboxButtonWithPopup
|
||||
icon = { IconArrowUp }
|
||||
iconDisabled = { false }
|
||||
onPopoverClose = { noop }
|
||||
onPopoverOpen = { noop }
|
||||
popoverContent = { null }
|
||||
visible = { false }>
|
||||
{/* @ts-ignore */}
|
||||
<RaiseHandButton
|
||||
disableClick = { true }
|
||||
raisedHand = { true } />
|
||||
</ToolboxButtonWithPopup>
|
||||
</div>
|
||||
</div>
|
||||
<p>{t('visitors.joinMeeting.wishToSpeak')}</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { IReduxState } from '../../../app/types';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
import { getVisitorsCount, getVisitorsShortText, iAmVisitor } from '../../functions';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
@@ -21,8 +21,7 @@ const useStyles = makeStyles()(theme => {
|
||||
const VisitorsCountLabel = () => {
|
||||
const { classes: styles, theme } = useStyles();
|
||||
const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
|
||||
const visitorsCount = useSelector((state: IReduxState) =>
|
||||
state['features/visitors'].count || 0);
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return !visitorsMode && visitorsCount > 0 ? (<Tooltip
|
||||
|
||||
93
react/features/visitors/components/web/VisitorsQueue.tsx
Normal file
93
react/features/visitors/components/web/VisitorsQueue.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions';
|
||||
import LoadingIndicator from '../../../base/ui/components/web/Spinner';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
inset: '0 0 0 0',
|
||||
display: 'flex',
|
||||
backgroundColor: theme.palette.ui01,
|
||||
zIndex: 252,
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
flexDirection: 'column-reverse'
|
||||
}
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 252,
|
||||
|
||||
'@media (max-width: 720px)': {
|
||||
height: 'auto',
|
||||
margin: '0 auto'
|
||||
},
|
||||
|
||||
// mobile phone landscape
|
||||
'@media (max-width: 420px)': {
|
||||
padding: '16px 16px 0 16px',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
'@media (max-width: 400px)': {
|
||||
padding: '16px'
|
||||
}
|
||||
},
|
||||
contentControls: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
margin: 'auto',
|
||||
width: '100%'
|
||||
},
|
||||
roomName: {
|
||||
...withPixelLineHeight(theme.typography.heading5),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(4),
|
||||
overflow: 'hidden',
|
||||
textAlign: 'center',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%'
|
||||
},
|
||||
spinner: {
|
||||
margin: '8px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The component that renders visitors queue UI.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default function VisitorsQueue() {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (<div className = { classes.container }>
|
||||
<div className = { classes.content }>
|
||||
<div className = { classes.contentControls }>
|
||||
<span className = { classes.roomName }>
|
||||
{ t('visitors.waitingMessage') }
|
||||
</span>
|
||||
<div className = { classes.spinner }>
|
||||
<LoadingIndicator size = 'large' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
@@ -45,3 +45,47 @@ export function iAmVisitor(stateful: IStateful) {
|
||||
export function getVisitorsCount(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].count ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of visitors that are waiting in queue.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {number} - The number of visitors in queue.
|
||||
*/
|
||||
export function getVisitorsInQueueCount(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueueCount ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is supported.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean} Whether visitor moder is supported.
|
||||
*/
|
||||
export function isVisitorsSupported(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].supported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is live.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean} Whether visitor moder is live.
|
||||
*/
|
||||
export function isVisitorsLive(stateful: IStateful) {
|
||||
return toState(stateful)['features/base/conference'].metadata?.visitors?.live;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to show visitor queue screen.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @returns {boolean} Whether current participant is visitor and is in queue.
|
||||
*/
|
||||
export function showVisitorsQueue(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueue;
|
||||
}
|
||||
|
||||
@@ -2,38 +2,57 @@ import i18n from 'i18next';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_JOIN_IN_PROGRESS,
|
||||
ENDPOINT_MESSAGE_RECEIVED
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from '../base/conference/actionTypes';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
|
||||
import { connect, setPreferVisitor } from '../base/connection/actions';
|
||||
import { disconnect } from '../base/connection/actions.any';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { JitsiConferenceEvents, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
||||
import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { raiseHand } from '../base/participants/actions';
|
||||
import { getLocalParticipant, getParticipantById } from '../base/participants/functions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
isLocalParticipantModerator
|
||||
} from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { BUTTON_TYPES } from '../base/ui/constants.any';
|
||||
import { hideNotification, showNotification } from '../notifications/actions';
|
||||
import {
|
||||
NOTIFICATION_ICON,
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
VISITORS_NOT_LIVE_NOTIFICATION_ID,
|
||||
VISITORS_PROMOTION_NOTIFICATION_ID
|
||||
} from '../notifications/constants';
|
||||
import { INotificationProps } from '../notifications/types';
|
||||
import { open as openParticipantsPane } from '../participants-pane/actions';
|
||||
import { joinConference } from '../prejoin/actions';
|
||||
|
||||
import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import {
|
||||
approveRequest,
|
||||
clearPromotionRequest,
|
||||
denyRequest,
|
||||
goLive,
|
||||
promotionRequestReceived,
|
||||
setInVisitorsQueue,
|
||||
setVisitorDemoteActor,
|
||||
setVisitorsSupported,
|
||||
updateVisitorsCount
|
||||
updateVisitorsCount,
|
||||
updateVisitorsInQueueCount
|
||||
} from './actions';
|
||||
import { getPromotionRequests } from './functions';
|
||||
import { JoinMeetingDialog } from './components';
|
||||
import { getPromotionRequests, getVisitorsCount, getVisitorsInQueueCount } from './functions';
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
switch (action.type) {
|
||||
@@ -43,7 +62,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
conference.on(JitsiConferenceEvents.PROPERTIES_CHANGED, (properties: { 'visitor-count': number; }) => {
|
||||
const visitorCount = Number(properties?.['visitor-count']);
|
||||
|
||||
if (!isNaN(visitorCount) && getState()['features/visitors'].count !== visitorCount) {
|
||||
if (!isNaN(visitorCount) && getVisitorsCount(getState) !== visitorCount) {
|
||||
dispatch(updateVisitorsCount(visitorCount));
|
||||
}
|
||||
});
|
||||
@@ -53,6 +72,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const { conference } = action;
|
||||
|
||||
if (getState()['features/visitors'].iAmVisitor) {
|
||||
dispatch(openDialog(JoinMeetingDialog));
|
||||
|
||||
const { demoteActorDisplayName } = getState()['features/visitors'];
|
||||
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
@@ -130,17 +151,179 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
if (data?.action === 'promotion-response' && data.approved) {
|
||||
const request = getPromotionRequests(getState())
|
||||
.find(r => r.from === data.id);
|
||||
.find((r: any) => r.from === data.id);
|
||||
|
||||
request && dispatch(clearPromotionRequest(request));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONNECTION_FAILED: {
|
||||
const { error } = action;
|
||||
|
||||
if (error?.name !== JitsiConnectionErrors.NOT_LIVE_ERROR) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { hosts, visitors: visitorsConfig } = getState()['features/base/config'];
|
||||
const { locationURL, preferVisitor } = getState()['features/base/connection'];
|
||||
|
||||
if (!visitorsConfig?.queueService || !locationURL || !preferVisitor) {
|
||||
break;
|
||||
}
|
||||
|
||||
// let's subscribe for visitor waiting queue
|
||||
const { room } = getState()['features/base/conference'];
|
||||
const conferenceJid = `${room}@${hosts?.muc}`;
|
||||
|
||||
WebsocketClient.getInstance()
|
||||
.connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
|
||||
`/secured/conference/visitor/topic.${conferenceJid}`,
|
||||
msg => {
|
||||
if ('status' in msg && msg.status === 'live') {
|
||||
logger.info('The conference is now live!');
|
||||
|
||||
WebsocketClient.getInstance().disconnect()
|
||||
.then(() => {
|
||||
let delay = 0;
|
||||
|
||||
// now let's connect to meeting
|
||||
if ('randomDelayMs' in msg) {
|
||||
delay = msg.randomDelayMs;
|
||||
}
|
||||
|
||||
if (WebsocketClient.getInstance().connectCount > 1) {
|
||||
// if we keep connecting/disconnecting, let's slow it down
|
||||
delay = 30 * 1000;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(joinConference());
|
||||
dispatch(setInVisitorsQueue(false));
|
||||
}, Math.random() * delay);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getState()['features/base/jwt'].jwt,
|
||||
() => {
|
||||
dispatch(setInVisitorsQueue(true));
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
|
||||
if (visitorsConfig?.queueService && isLocalParticipantModerator(getState)) {
|
||||
const { metadata } = getState()['features/base/conference'];
|
||||
|
||||
if (metadata?.visitors?.live === false && !WebsocketClient.getInstance().isActive()) {
|
||||
// when go live is available and false, we should subscribe
|
||||
// to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_CONFIG: {
|
||||
const result = next(action);
|
||||
const { preferVisitor } = action.config;
|
||||
|
||||
if (preferVisitor !== undefined) {
|
||||
setPreferVisitor(preferVisitor);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
const { visitors: visitorsConfig } = toState(getState)['features/base/config'];
|
||||
|
||||
if (!visitorsConfig?.queueService) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (isLocalParticipantModerator(getState)) {
|
||||
if (metadata?.visitors?.live === false) {
|
||||
if (!WebsocketClient.getInstance().isActive()) {
|
||||
// if metadata go live changes to goLive false and local is moderator
|
||||
// we should subscribe to the service if available to listen for waiting visitors
|
||||
_subscribeQueueStats(getState(), dispatch);
|
||||
}
|
||||
|
||||
_showNotLiveNotification(dispatch, getVisitorsInQueueCount(getState));
|
||||
} else if (metadata?.visitors?.live) {
|
||||
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case UPDATE_VISITORS_IN_QUEUE_COUNT: {
|
||||
_showNotLiveNotification(dispatch, action.count);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Shows a notification that the meeting is not live.
|
||||
*
|
||||
* @param {Dispatch} dispatch - The Redux dispatch function.
|
||||
* @param {number} count - The count of visitors waiting.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _showNotLiveNotification(dispatch: IStore['dispatch'], count: number): void {
|
||||
// let's show notification
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.waitingVisitorsTitle',
|
||||
descriptionKey: 'notify.waitingVisitors',
|
||||
descriptionArguments: {
|
||||
waitingVisitors: count
|
||||
},
|
||||
disableClosing: true,
|
||||
uid: VISITORS_NOT_LIVE_NOTIFICATION_ID,
|
||||
customActionNameKey: [ 'participantsPane.actions.goLive' ],
|
||||
customActionType: [ BUTTON_TYPES.PRIMARY ],
|
||||
customActionHandler: [ () => batch(() => {
|
||||
dispatch(hideNotification(VISITORS_NOT_LIVE_NOTIFICATION_ID));
|
||||
dispatch(goLive());
|
||||
}) ],
|
||||
icon: NOTIFICATION_ICON.PARTICIPANTS
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe for moderator stats.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @param {Dispatch} dispatch - The Redux dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch']) {
|
||||
const { hosts } = toState(stateful)['features/base/config'];
|
||||
const { room } = toState(stateful)['features/base/conference'];
|
||||
const conferenceJid = `${room}@${hosts?.muc}`;
|
||||
|
||||
const { visitors: visitorsConfig } = toState(stateful)['features/base/config'];
|
||||
|
||||
WebsocketClient.getInstance()
|
||||
.connect(`wss://${visitorsConfig?.queueService}/visitor/websocket`,
|
||||
`/secured/conference/state/topic.${conferenceJid}`,
|
||||
msg => {
|
||||
if ('visitorsWaiting' in msg) {
|
||||
dispatch(updateVisitorsInQueueCount(msg.visitorsWaiting));
|
||||
}
|
||||
},
|
||||
toState(stateful)['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle the promotion notification.
|
||||
*
|
||||
|
||||
@@ -4,16 +4,20 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import {
|
||||
CLEAR_VISITOR_PROMOTION_REQUEST,
|
||||
I_AM_VISITOR_MODE,
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
UPDATE_VISITORS_COUNT,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
count: -1,
|
||||
count: 0,
|
||||
iAmVisitor: false,
|
||||
inQueue: false,
|
||||
inQueueCount: 0,
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: []
|
||||
@@ -23,6 +27,8 @@ export interface IVisitorsState {
|
||||
count?: number;
|
||||
demoteActorDisplayName?: string;
|
||||
iAmVisitor: boolean;
|
||||
inQueue: boolean;
|
||||
inQueueCount?: number;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
}
|
||||
@@ -49,12 +55,28 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
count: action.count
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_IN_QUEUE_COUNT: {
|
||||
if (state.count === action.count) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
inQueueCount: action.count
|
||||
};
|
||||
}
|
||||
case I_AM_VISITOR_MODE: {
|
||||
return {
|
||||
...state,
|
||||
iAmVisitor: action.enabled
|
||||
};
|
||||
}
|
||||
case SET_IN_VISITORS_QUEUE: {
|
||||
return {
|
||||
...state,
|
||||
inQueue: action.value
|
||||
};
|
||||
}
|
||||
case SET_VISITOR_DEMOTE_ACTOR: {
|
||||
return {
|
||||
...state,
|
||||
|
||||
150
react/features/visitors/websocket-client.ts
Normal file
150
react/features/visitors/websocket-client.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Client } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
interface QueueServiceResponse {
|
||||
conference: string;
|
||||
}
|
||||
export interface StateResponse extends QueueServiceResponse {
|
||||
randomDelayMs: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface VisitorResponse extends QueueServiceResponse {
|
||||
visitorsWaiting: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Websocket client impl, used for visitors queue.
|
||||
* Uses STOMP for authenticating (https://stomp.github.io/).
|
||||
*/
|
||||
export class WebsocketClient {
|
||||
private stompClient: Client | undefined;
|
||||
|
||||
private static instance: WebsocketClient;
|
||||
|
||||
private retriesCount = 0;
|
||||
|
||||
private _connectCount = 0;
|
||||
|
||||
/**
|
||||
* WebsocketClient getInstance.
|
||||
*
|
||||
* @static
|
||||
* @returns {WebsocketClient} - WebsocketClient instance.
|
||||
*/
|
||||
static getInstance(): WebsocketClient {
|
||||
if (!this.instance) {
|
||||
this.instance = new WebsocketClient();
|
||||
}
|
||||
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to endpoint.
|
||||
*
|
||||
* @param {string} queueServiceURL - The service URL to use.
|
||||
* @param {string} endpoint - The endpoint to subscribe to.
|
||||
* @param {Function} callback - The callback to execute when we receive a message from the endpoint.
|
||||
* @param {string} token - The token, if any, to be used for authorization.
|
||||
* @param {Function?} connectCallback - The callback to execute when successfully connected.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
connect(queueServiceURL: string, // eslint-disable-line max-params
|
||||
endpoint: string,
|
||||
callback: (response: StateResponse | VisitorResponse) => void,
|
||||
token: string | undefined,
|
||||
connectCallback?: () => void): void {
|
||||
this.stompClient = new Client({
|
||||
brokerURL: queueServiceURL,
|
||||
forceBinaryWSFrames: true,
|
||||
appendMissingNULLonIncoming: true
|
||||
});
|
||||
|
||||
const errorConnecting = (error: any) => {
|
||||
if (this.retriesCount > 3) {
|
||||
this.stompClient?.deactivate();
|
||||
this.stompClient = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.retriesCount++;
|
||||
|
||||
logger.error(`Error connecting to ${queueServiceURL} ${JSON.stringify(error)}`);
|
||||
};
|
||||
|
||||
this.stompClient.onWebSocketError = errorConnecting;
|
||||
|
||||
this.stompClient.onStompError = frame => {
|
||||
errorConnecting(frame.headers.message);
|
||||
};
|
||||
|
||||
if (token) {
|
||||
this.stompClient.connectHeaders = {
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
this.stompClient.onConnect = () => {
|
||||
if (!this.stompClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.retriesCount = 0;
|
||||
|
||||
logger.info(`Connected to:${endpoint}`);
|
||||
this._connectCount++;
|
||||
connectCallback?.();
|
||||
|
||||
this.stompClient.subscribe(endpoint, message => {
|
||||
try {
|
||||
callback(JSON.parse(message.body));
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing response: ${message}`, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the current stomp client instance and clears it.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
disconnect(): Promise<any> {
|
||||
if (!this.stompClient) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const url = this.stompClient.brokerURL;
|
||||
|
||||
return this.stompClient.deactivate().then(() => {
|
||||
logger.info(`disconnected from: ${url}`);
|
||||
this.stompClient = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the instance is created and connected or in connecting state.
|
||||
*
|
||||
* @returns {boolean} Whether the connect method was executed.
|
||||
*/
|
||||
isActive() {
|
||||
return this.stompClient !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of connections.
|
||||
*
|
||||
* @returns {number} The number of connections for the life of the app.
|
||||
*/
|
||||
get connectCount(): number {
|
||||
return this._connectCount;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,29 @@ paths:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/PhoneNumberListAnswer"
|
||||
/waiting-queue/golive:
|
||||
post:
|
||||
tags:
|
||||
- waitingQueueGoLive
|
||||
summary: Post a go live request
|
||||
description: Mark the conference as live, notify all visitors waiting.
|
||||
operationId: goLive
|
||||
consumes:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Go Live Request"
|
||||
required: true
|
||||
schema:
|
||||
"$ref": "#/definitions/GoLiveRequest"
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
'404':
|
||||
description: Missing conference
|
||||
'500':
|
||||
description: Failed operation
|
||||
securityDefinitions:
|
||||
token:
|
||||
type: "apiKey"
|
||||
@@ -143,6 +166,12 @@ definitions:
|
||||
- {"countryCode":"US","tollFree":false,"formattedNumber":"+1 123-456-7890"}
|
||||
- {"countryCode":"UK","tollFree":true,"formattedNumber":"+44 123 456 7890"}
|
||||
|
||||
GoLiveRequest:
|
||||
type: object
|
||||
properties:
|
||||
conference:
|
||||
type: string
|
||||
externalDocs:
|
||||
description: "Find out more about the Jitsi Cloud API"
|
||||
url: "https://jitsi.org/CloudAPI"
|
||||
|
||||
@@ -36,6 +36,7 @@ local breakout_rooms_component_host = module:get_option_string('breakout_rooms_c
|
||||
|
||||
module:log("info", "Starting room metadata for %s", muc_component_host);
|
||||
|
||||
local main_muc_module;
|
||||
|
||||
-- Utility functions
|
||||
|
||||
@@ -148,6 +149,9 @@ function on_message(event)
|
||||
|
||||
broadcastMetadata(room);
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
@@ -158,6 +162,8 @@ module:hook("message/host", on_message);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
main_muc_module = host_module;
|
||||
|
||||
module:log('debug', 'Main muc loaded');
|
||||
module:log("info", "Hook to muc events on %s", muc_component_host);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module:log('info', 'Starting visitors_component at %s', module.host);
|
||||
|
||||
local http = require 'net.http';
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local util = module:require 'util';
|
||||
@@ -18,6 +19,9 @@ local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local json = require 'cjson.safe';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
@@ -36,6 +40,13 @@ local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promo
|
||||
-- can be set to off and being controlled by another module, turning it on and off for rooms
|
||||
local always_visitors_enabled = module:get_option_boolean('always_visitors_enabled', true);
|
||||
|
||||
local visitors_queue_service = module:get_option_string('visitors_queue_service');
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody (" .. prosody.version .. "; " .. prosody.platform .. ")",
|
||||
["Content-Type"] = "application/json",
|
||||
["Accept"] = "application/json"
|
||||
};
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
@@ -294,8 +305,61 @@ local function process_promotion_response(room, id, approved)
|
||||
allow = approved }):up());
|
||||
end
|
||||
|
||||
-- if room metadata does not have visitors.live set to `true` and there are no occupants in the meeting
|
||||
-- it will skip calling goLive endpoint
|
||||
local function go_live(room)
|
||||
if room._jitsi_go_live_sent then
|
||||
return;
|
||||
end
|
||||
|
||||
if not (room.jitsiMetadata and room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live) then
|
||||
return;
|
||||
end
|
||||
|
||||
local has_occupant = false;
|
||||
for _, occupant in room:each_occupant() do
|
||||
if not is_admin(occupant.bare_jid) then
|
||||
has_occupant = true;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
-- when there is an occupant then go live
|
||||
if not has_occupant then
|
||||
return;
|
||||
end
|
||||
|
||||
-- let's inform the queue service
|
||||
local function cb(content_, code_, response_, request_)
|
||||
local room = room;
|
||||
if code_ ~= 200 then
|
||||
module:log('warn', 'External call to visitors_queue_service/golive failed. Code %s, Content %s',
|
||||
code_, content_)
|
||||
end
|
||||
end
|
||||
|
||||
local headers = http_headers or {};
|
||||
headers['Authorization'] = token_util:generateAsapToken();
|
||||
|
||||
local ev = {
|
||||
conference = internal_room_jid_match_rewrite(room.jid)
|
||||
};
|
||||
|
||||
room._jitsi_go_live_sent = true;
|
||||
|
||||
http.request(visitors_queue_service..'/golive', {
|
||||
headers = headers,
|
||||
method = 'POST',
|
||||
body = json.encode(ev);
|
||||
}, cb);
|
||||
end
|
||||
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
process_host_module(muc_domain_base, function(host_module, host)
|
||||
token_util = module:require "token/util".new(host_module);
|
||||
end);
|
||||
|
||||
process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_module, host)
|
||||
-- if visitor mode is started, then you are not allowed to join without request/response exchange of iqs -> deny access
|
||||
-- check list of allowed jids for the room
|
||||
@@ -451,6 +515,29 @@ 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;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
go_live(room);
|
||||
end, -2); -- metadata hook on -1
|
||||
host_module:hook('jitsi-metadata-updated', function (event)
|
||||
if event.key == 'visitors' then
|
||||
go_live(event.room);
|
||||
end
|
||||
end);
|
||||
-- when metadata changed internally from another module
|
||||
host_module:hook('room-metadata-changed', function (event)
|
||||
go_live(event.room);
|
||||
end);
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
go_live(event.room);
|
||||
end);
|
||||
end
|
||||
|
||||
if always_visitors_enabled then
|
||||
local visitorsEnabledField = {
|
||||
|
||||
@@ -26,6 +26,24 @@ local ssl = require "ssl";
|
||||
-- TODO: Figure out a less arbitrary default cache size.
|
||||
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
|
||||
|
||||
-- the cache for generated asap jwt tokens
|
||||
local jwtKeyCache = require 'util.cache'.new(cacheSize);
|
||||
|
||||
local ASAPTTL_THRESHOLD = module:get_option_number('asap_ttl_threshold', 600);
|
||||
local ASAPTTL = module:get_option_number('asap_ttl', 3600);
|
||||
local ASAPIssuer = module:get_option_string('asap_issuer', 'jitsi');
|
||||
local ASAPAudience = module:get_option_string('asap_audience', 'jitsi');
|
||||
local ASAPKeyId = module:get_option_string('asap_key_id', 'jitsi');
|
||||
local ASAPKeyPath = module:get_option_string('asap_key_path', '/etc/prosody/certs/asap.key');
|
||||
|
||||
local ASAPKey;
|
||||
local f = io.open(ASAPKeyPath, 'r');
|
||||
|
||||
if f then
|
||||
ASAPKey = f:read('*all');
|
||||
f:close();
|
||||
end
|
||||
|
||||
local Util = {}
|
||||
Util.__index = Util
|
||||
|
||||
@@ -229,13 +247,8 @@ end
|
||||
-- session.jitsi_meet_context_group - the group value from the token
|
||||
-- session.jitsi_meet_context_features - the features value from the token
|
||||
-- @param session the current session
|
||||
-- @param acceptedIssuers optional list of accepted issuers to check
|
||||
-- @return false and error
|
||||
function Util:process_and_verify_token(session, acceptedIssuers)
|
||||
if not acceptedIssuers then
|
||||
acceptedIssuers = self.acceptedIssuers;
|
||||
end
|
||||
|
||||
function Util:process_and_verify_token(session)
|
||||
if session.auth_token == nil then
|
||||
if self.allowEmptyToken then
|
||||
return true;
|
||||
@@ -292,7 +305,7 @@ function Util:process_and_verify_token(session, acceptedIssuers)
|
||||
session.auth_token,
|
||||
self.signatureAlgorithm,
|
||||
key,
|
||||
acceptedIssuers,
|
||||
self.acceptedIssuers,
|
||||
self.acceptedAudiences
|
||||
)
|
||||
if claims ~= nil then
|
||||
@@ -479,4 +492,48 @@ function Util:verify_room(session, room_address)
|
||||
end
|
||||
end
|
||||
|
||||
function Util:generateAsapToken(audience)
|
||||
if not ASAPKey then
|
||||
module:log('warn', 'No ASAP Key read, asap key generation is disabled');
|
||||
return ''
|
||||
end
|
||||
|
||||
audience = audience or ASAPAudience
|
||||
local t = os.time()
|
||||
local err
|
||||
local exp_key = 'asap_exp.'..audience
|
||||
local token_key = 'asap_token.'..audience
|
||||
local exp = jwtKeyCache:get(exp_key)
|
||||
local token = jwtKeyCache:get(token_key)
|
||||
|
||||
--if we find a token and it isn't too far from expiry, then use it
|
||||
if token ~= nil and exp ~= nil then
|
||||
exp = tonumber(exp)
|
||||
if (exp - t) > ASAPTTL_THRESHOLD then
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
--expiry is the current time plus TTL
|
||||
exp = t + ASAPTTL
|
||||
local payload = {
|
||||
iss = ASAPIssuer,
|
||||
aud = audience,
|
||||
nbf = t,
|
||||
exp = exp,
|
||||
}
|
||||
|
||||
-- encode
|
||||
local alg = 'RS256'
|
||||
token, err = jwt.encode(payload, ASAPKey, alg, { kid = ASAPKeyId })
|
||||
if not err then
|
||||
token = 'Bearer '..token
|
||||
jwtKeyCache:set(exp_key, exp)
|
||||
jwtKeyCache:set(token_key, token)
|
||||
return token
|
||||
else
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
return Util;
|
||||
|
||||
0
resources/waiting-queue/examples/main.css
Normal file
0
resources/waiting-queue/examples/main.css
Normal file
45
resources/waiting-queue/examples/visitor.html
Normal file
45
resources/waiting-queue/examples/visitor.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Conference WebSocket</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<link href="./main.css" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js"></script>
|
||||
<script src="./visitor.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
|
||||
enabled. Please enable
|
||||
Javascript and reload this page!</h2></noscript>
|
||||
<div id="main-content" class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form class="form-inline">
|
||||
<div class="form-group">
|
||||
<label for="connect">WebSocket connection:</label>
|
||||
<button id="connect" class="btn btn-default" type="submit">Connect</button>
|
||||
<label for="conference">What is your conference?</label>
|
||||
<input type="text" id="conference" class="form-control" placeholder="Your conference here...">
|
||||
<button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table id="conference" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Messages</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="messages">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
63
resources/waiting-queue/examples/visitor.js
Normal file
63
resources/waiting-queue/examples/visitor.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const token = 'JWT_TOKEN_GOES_HERE'
|
||||
|
||||
const stompClient = new StompJs.Client({
|
||||
brokerURL: 'ws://localhost:8060/waiting-queue/visitor/websocket',
|
||||
});
|
||||
|
||||
stompClient.onWebSocketError = (error) => {
|
||||
console.error('Error with websocket', error);
|
||||
};
|
||||
|
||||
stompClient.onStompError = (frame) => {
|
||||
console.error('Broker reported error: ' + frame.headers['message']);
|
||||
console.error('Additional details: ' + frame.body);
|
||||
};
|
||||
|
||||
function setConnected(connected) {
|
||||
$("#connect").prop("disabled", connected);
|
||||
$("#disconnect").prop("disabled", !connected);
|
||||
if (connected) {
|
||||
$("#conversation").show();
|
||||
}
|
||||
else {
|
||||
$("#conversation").hide();
|
||||
}
|
||||
$("#messages").html("");
|
||||
}
|
||||
|
||||
function connect(conference) {
|
||||
console.log("Connecting to conference " + conference);
|
||||
|
||||
headers = {
|
||||
Authorization: 'Bearer ' + token
|
||||
};
|
||||
|
||||
stompClient.connectHeaders = headers;
|
||||
|
||||
stompClient.onConnect = (frame) => {
|
||||
setConnected(true);
|
||||
console.log('Connected: ' + frame);
|
||||
|
||||
stompClient.subscribe('/secured/conference/visitor/topic.' + conference, (message) => {
|
||||
showMessage(message.body);
|
||||
}, headers);
|
||||
};
|
||||
|
||||
stompClient.activate();
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
stompClient.deactivate();
|
||||
setConnected(false);
|
||||
console.log("Disconnected");
|
||||
}
|
||||
|
||||
function showMessage(message) {
|
||||
$("#messages").append("<tr><td>" + message + "</td></tr>");
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$("form").on('submit', (e) => e.preventDefault());
|
||||
$( "#connect" ).click(() => connect($("#conference").val()));
|
||||
$( "#disconnect" ).click(() => disconnect());
|
||||
});
|
||||
BIN
resources/waiting-queue/img/waiting-queue-ds.png
Normal file
BIN
resources/waiting-queue/img/waiting-queue-ds.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
BIN
resources/waiting-queue/img/waiting-queue-topics.png
Normal file
BIN
resources/waiting-queue/img/waiting-queue-topics.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
79
resources/waiting-queue/waiting-queue.md
Normal file
79
resources/waiting-queue/waiting-queue.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Waiting queue
|
||||
|
||||
Visitors queue service is used for managing the visitors queue for the 8x8 video meetings by keeping visitors websocket connections opened and when a moderator opens the meeting, the visitors are notified and allowed to join the meeting.
|
||||
The moderators should be able to see the visitors count.
|
||||
|
||||
## Authentication
|
||||
|
||||
The JWT token is sent at least in the CONNECT STOMP message as connect header - see sample code:
|
||||
|
||||
```
|
||||
headers = {
|
||||
Authorization: 'Bearer ' + token
|
||||
};
|
||||
|
||||
stompClient.connectHeaders = headers;
|
||||
|
||||
stompClient.onConnect = (frame) => {
|
||||
setConnected(true);
|
||||
console.log('Connected: ' + frame);
|
||||
|
||||
stompClient.subscribe('/secured/conference/visitor/topic.' + conference, (message) => {
|
||||
showMessage(message.body);
|
||||
}, headers);
|
||||
};
|
||||
```
|
||||
|
||||
### Visitors
|
||||
|
||||
This endpoint should accept only visitor's jaas tokens for a conference specified as param to the endpoint and the token to be valid for that room. The token for visitors contains:
|
||||
```
|
||||
context: {
|
||||
user: {
|
||||
role: ‘visitor'
|
||||
}
|
||||
}
|
||||
```
|
||||
It allows visitors to connect to the /visitors websocket and wait for the start message to be published on /secured/conference/visitor/topic.{conference} topic.
|
||||
|
||||
### Moderators
|
||||
|
||||
This endpoint should accept only moderator's jaas tokens for a conference specified as param to the endpoint and the token to be valid for that room. The token for moderator contains:
|
||||
```
|
||||
context: {
|
||||
user: {
|
||||
moderator: true
|
||||
}
|
||||
}
|
||||
```
|
||||
It allows moderators to connect to the /moderator websocket and wait for the status message to be published on /secured/conference/state/topic.{conference} topic (triggered every 15 seconds).
|
||||
|
||||
## Flow
|
||||
|
||||
The flow is depicted below:
|
||||
|
||||

|
||||
|
||||
## Topics
|
||||
|
||||
The topics used:
|
||||
|
||||

|
||||
|
||||
## API
|
||||
|
||||
| Endpoint | Type | Auth | Use |
|
||||
|----------|:-------------:|------:|------:|
|
||||
| WS /visitor | WebSocket/STOMP | require client token for conference | Visitors open a websocket and wait to receive a message. Message format is not very important, since we’re starting with a single message – “ready to join”. But keep it extensible. If a conference is already live when a visitor opens the ws, immediately send a notification |
|
||||
| WS /state | WebSocket/STOMP | require client token for conference | Moderators use it to get the number of visitors waiting. Service sends updates for the number of visitors. To reduce traffic send updates at a minimum period and only if the count changed |
|
||||
| POST /golive | REST | require a server-to-server token for conference | Our backend calls it anytime the visitorsLive state for a conference changes from “false” to “true”, including when a conference is created with visitorsLive=true |
|
||||
|
||||
>
|
||||
> Note: CONNECT and MESSAGE STOMP frames expect an additional header for Authorization
|
||||
>
|
||||
|
||||
More on [STOMP](https://stomp.github.io/stomp-specification-1.2.html).
|
||||
|
||||
## Sample code
|
||||
|
||||
There is sample code showing how to handle the visitor case [here](./examples/visitor.js).
|
||||
Reference in New Issue
Block a user