Compare commits

...

15 Commits

Author SHA1 Message Date
Calin-Teodor
67f49815c4 feat(android): update rnVersion to 0.73.8 2024-07-04 18:18:51 +03:00
Calinteodor
2697eb1273 deps(react-native): update to 0.73 (#14886)
* deps(react-native): updates regarding to 0.73.8
2024-07-04 17:58:55 +03:00
Calin-Teodor
491f793530 feat(react-native-sdk): add stompjs to peerDependencies 2024-07-04 11:47:16 +03:00
Calin-Teodor
eb0317fb8d deps(react-native-screens, react-navigation): update to latest 2024-07-04 11:46:57 +03:00
Saúl Ibarra Corretgé
59da1537be chore(deps,rn) update react-native-async-storage
Ref: https://github.com/jitsi/jitsi-meet/issues/14850
2024-07-03 15:19:17 +03:00
Mengyuan Liu
9e1e6237ce fix(raise-hand) clone queue instead of mutate (#14867) 2024-07-02 18:35:09 +03:00
Jaya Allamsetty
5c0b8467d5 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1840.0.0+fc115be5...v1841.0.0+2d90500a
2024-07-02 11:24:52 -04:00
Nathan Beck
b4a5e63d1d feat(silent): hide unmute if participant joined without audio (#14803)
* feat(silent): hide unmute if participant joined without audio

* Add additional listener for SILENT_STATUS_CHANGED

* squash: Rename local variable.

* chore(deps) lib-jitsi-meet@latest

https://github.com/jitsi/lib-jitsi-meet/compare/v1839.0.0+ea523fc6...v1840.0.0+fc115be5

---------

Co-authored-by: damencho <damencho@jitsi.org>
2024-07-02 08:22:10 -05:00
Calinteodor
3ae50b6c4c feat(android): check for microphone permission so ongoing service can start (#14865)
* feat(android/sdk): for API >= 33, launch JitsiOngoingConferenceService
only if POST_NOTIFICATIONS and RECORD_AUDIO permissions have been granted
2024-07-02 15:58:08 +03:00
ilaydadastan
bc9525a908 fix(contributing): contributing file has been updated to be directed to the handbook Fixes #14702 2024-07-02 09:55:51 +02:00
Calin-Teodor
c6dcac47a8 feat(android/sdk): fixed enterpictureinpicture method call 2024-07-01 14:55:57 +03:00
Mihaela Dumitru
f9f5cf87b9 fix(recording) start transcription from notification when configured (#14879) 2024-06-28 16:04:37 +03:00
damencho
b969fba433 feat(visitors): Adds option to disable self-demote button.
Fixes #14539
2024-06-28 15:29:55 +03:00
Дамян Минков
f0fc63f573 feat(visitors): Handles live conference and queue service. (#14869)
* feat(visitors): Handling of live conference and queue service.

* squash: Small refactor mobile code.

* squash: Drop debug log.

* chore(deps) lib-jitsi-meet@latest

https://github.com/jitsi/lib-jitsi-meet/compare/v1836.0.0+d05325f3...v1839.0.0+ea523fc6

* squash: Adds a count function.

* squash: Drop debug print.

* squash: Skip if queueService is not enabled.

* squash: Avoids double subscribing for visitorsWaiting.

* squash: Fixes lint error.

* squash: Fixes showing dialog.
2024-06-28 07:29:41 -05:00
ltorje-8x8
d618175074 feat(doc): add waiting queue documentation (#14775)
* feat-13184 - document waiting queue

* feat-13184 - document waiting queue

* fix doc - 15 min --> 15 sec

* fix doc flow

* fix typo

* fix typo

* cleanup token

* move resources to waiting-queue folder

* apply review
2024-06-28 07:29:19 -05:00
61 changed files with 5470 additions and 4148 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -822,6 +822,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,6 +837,7 @@
"audioModeration": "Unmute themselves",
"blockEveryoneMicCamera": "Block everyone's mic and camera",
"breakoutRooms": "Breakout rooms",
"goLive": "Go live",
"invite": "Invite Someone",
"moreModerationActions": "More moderation options",
"moreModerationControls": "More moderation controls",
@@ -852,6 +855,7 @@
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})",
"visitorInQueue": " (waiting {{count}})",
"visitorRequests": " (requests {{count}})",
"visitors": "Visitors {{count}}",
"waitingLobby": "Waiting in lobby ({{count}})"
@@ -1495,7 +1499,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": {

6876
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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-async-storage/async-storage": "1.23.1",
"@react-native-community/clipboard": "1.5.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/v1838.0.0+1993a030/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1841.0.0+2d90500a/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -81,7 +82,7 @@
"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",
@@ -96,11 +97,11 @@
"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",
@@ -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",

View File

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

View File

@@ -61,6 +61,7 @@
"@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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -80,6 +80,7 @@ export interface IConfigState extends IConfig {
audio?: boolean;
video?: boolean;
};
queueService: string;
};
}

View File

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

View File

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

View File

@@ -25,6 +25,7 @@ export interface IParticipant {
isJigasi?: boolean;
isReplaced?: boolean;
isReplacing?: number;
isSilent?: boolean;
jwtId?: string;
loadableAvatarUrl?: string;
loadableAvatarUrlUseCORS?: boolean;

View File

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

View File

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

View File

@@ -44,6 +44,7 @@ export const screen = {
profile: 'Profile'
},
unsafeRoomWarning: 'Unsafe Room Warning',
visitorsQueue: 'Visitors Queue',
welcome: {
main: 'Welcome',
tabs: {

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ export interface INotificationProps {
description?: string | React.ReactNode;
descriptionArguments?: Object;
descriptionKey?: string;
disableClosing?: boolean;
hideErrorSupportLink?: boolean;
icon?: string;
maxLines?: number;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,6 +78,11 @@ interface IProps {
*/
_isParticipantAvailable?: boolean;
/**
* Whether or not the targeted participant joined without audio.
*/
_isParticipantSilent: boolean;
/**
* Whether the local participant is moderator or not.
*/
@@ -143,6 +148,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
_disableGrantModerator,
_isBreakoutRoom,
_isParticipantAvailable,
_isParticipantSilent,
_moderator,
_rooms,
_showDemote,
@@ -166,10 +172,10 @@ class RemoteVideoMenu extends PureComponent<IProps> {
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { _isParticipantAvailable }>
<AskUnmuteButton { ...buttonProps } />
{!_isParticipantSilent && <AskUnmuteButton { ...buttonProps } />}
{ !_disableRemoteMute && <MuteButton { ...buttonProps } /> }
<MuteEveryoneElseButton { ...buttonProps } />
{ !_disableRemoteMute && <MuteVideoButton { ...buttonProps } /> }
{ !_disableRemoteMute && !_isParticipantSilent && <MuteVideoButton { ...buttonProps } /> }
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />
{ !_disableKick && <KickButton { ...buttonProps } /> }
@@ -242,7 +248,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);
@@ -257,7 +263,8 @@ 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),
_rooms,

View File

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

View File

@@ -214,7 +214,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 +230,7 @@ const ParticipantContextMenu = ({
}
}
if (!disableRemoteMute) {
if (!disableRemoteMute && !participant.isSilent) {
if (!(isClickedFromParticipantPane && quickActionButtonType === QUICK_ACTION_BUTTON.MUTE)) {
buttons.push(<MuteButton { ...getButtonProps(BUTTONS.MUTE) } />);
}

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

@@ -2,38 +2,55 @@ 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 { 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 { 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 +60,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));
}
});
@@ -130,17 +147,173 @@ 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, preferVisitor, visitors: visitorsConfig } = getState()['features/base/config'];
const { locationURL } = 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();
let delay = 0;
// now let's connect to meeting
if ('randomDelayMs' in msg) {
delay = msg.randomDelayMs;
}
setTimeout(() => {
dispatch(joinConference());
dispatch(setInVisitorsQueue(false));
}, 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.
*

View File

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

View File

@@ -0,0 +1,138 @@
/* 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;
/**
* 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}`);
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 {void}
*/
disconnect(): void {
if (!this.stompClient) {
return;
}
const url = this.stompClient.brokerURL;
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;
}
}

View File

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

View File

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

View File

@@ -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,8 @@ local um_is_admin = require 'core.usermanager'.is_admin;
local json = require 'cjson.safe';
local inspect = require 'inspect';
local token_util = module:require 'token/util'.new(module);
local MUC_NS = 'http://jabber.org/protocol/muc';
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
@@ -36,6 +39,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,6 +304,33 @@ local function process_promotion_response(room, id, approved)
allow = approved }):up());
end
local function go_live(room)
-- let's inform the queue service
local function cb(content_, code_, response_, request_)
local room = room;
if code_ == 200 then
-- meeting went live ???
module:log('info', 'live')
else
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)
};
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_prefix..'.'..muc_domain_base, function(host_module, host)
@@ -451,6 +488,22 @@ 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 room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live then
go_live(room);
end
end, -2); -- metadata hook on -1
host_module:hook('jitsi-metadata-updated', function (event)
if event.key == 'visitors' then
local room = event.room;
if room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live then
go_live(room);
end
end
end);
end
if always_visitors_enabled then
local visitorsEnabledField = {

View File

@@ -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
@@ -479,4 +497,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;

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View 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:
![Flow](img/waiting-queue-ds.png)
## Topics
The topics used:
![Topics](img/waiting-queue-topics.png)
## 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).