Compare commits

..

37 Commits

Author SHA1 Message Date
damencho
8ddab7464c feat(visitors): Force promote works for all moderators. 2024-06-27 16:04:32 +03:00
damencho
9076fb3e4a fix(conference-duration): Fixes formatting the creation time.
Fixes #14815.
2024-06-27 15:32:34 +03:00
Saúl Ibarra Corretgé
d8079a4232 feat(external_api) add ability to start transcriptions together with recordings 2024-06-27 12:53:17 +02:00
Jaya Allamsetty
c992a8274c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1836.0.0+d05325f3...v1837.0.0+6bcc577a
2024-06-25 22:44:42 -04:00
Boris Grozev
b5e059a0a9 feat(prosody): Add handling of a room-metadata-changed event. 2024-06-25 09:31:22 +03:00
Saúl Ibarra Corretgé
0be3e2b103 feat(external_api) include transcription state in recordingStatusChanged 2024-06-24 17:34:19 +02:00
Saúl Ibarra Corretgé
ae138c1a15 feat(recording) add isLiveStreamingRunning helper 2024-06-24 17:34:19 +02:00
Jaya Allamsetty
0f8de50d26 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1834.0.0+d7ffbfaf...v1836.0.0+d05325f3
2024-06-21 11:09:47 -04:00
Calin-Teodor
ff7b6010bd feat(toolbox): enable full screen button for ipad browsers 2024-06-21 18:04:54 +03:00
Saúl Ibarra Corretgé
270cdd017d fix(build) compile the frontend before making a source package
Ref: https://github.com/jitsi/jitsi-meet/issues/14842
2024-06-18 16:01:07 +02:00
Calin-Teodor
f4cae6350b feat(ios): updated cocoalumberjack dependency 2024-06-18 15:05:10 +03:00
Calin-Teodor
f0126a43f3 feat(android): updated timber dependecy 2024-06-18 15:05:10 +03:00
kiraware
b5deb65815 lang: add indonesian translation (#14836)
* add indonesian translation

* add id to languages.json
2024-06-14 08:49:14 -05:00
Hristo Terezov
6d9bbe0376 fix(MainToolbar): replace hidden buttons.
Currently if a button in the main toolbar is not visible, the button is
not replaced by another button from the overflow menu.
2024-06-14 10:18:00 +03:00
Mihaela Dumitru
da634c211e fix(config) add missing apostrophe 2024-06-13 12:26:27 -05:00
Jaya Allamsetty
61d96a5fd5 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1833.0.0+8c5c8029...v1834.0.0+d7ffbfaf
2024-06-13 10:29:51 -04:00
Jaya Allamsetty
07e46b9399 fix(tracks): Update the codec for local tracks in redux.
Also add helper functions for torture tests.
2024-06-13 09:07:37 -04:00
Saúl Ibarra Corretgé
3139111d36 chore(deps) update react-native-webrtc
iOS rendering improvements.
2024-06-13 11:59:42 +03:00
damencho
205f88ec5a fix(system_chat_message): Fixes delivering system messages to visitors. 2024-06-12 07:20:26 -05:00
dependabot[bot]
c8d6cdf6c8 chore(deps): bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-12 13:06:52 +02:00
Calin-Teodor
7022243a95 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1830.0.0+5a14bd43...v1833.0.0+8c5c8029
2024-06-12 13:38:31 +03:00
Calinteodor
b2ae72249d feat(base/native): Fixes around UI (#14820)
feat(base/native): Fixes around 
notifications, polls, local participant video menu, breakout room and participants UI
2024-06-12 13:02:04 +03:00
Horatiu Muresan
8ee6d179d5 fix(remote-control) Fix caps lock key name (#14821)
- this was causing a crash on the controlled meeting instance
2024-06-11 15:33:54 +03:00
Horatiu Muresan
bebcfa3fd7 fix(iOS-responsive-ui) Attempt fix iOS responsive ui issue (#14819)
- on iOS safari and chrome, in case we show eg a spinner until we get the videoConferenceJoined event, all `clientResize` are done with size 0 for width/height
- on iOS we never get a `clientResize` call with correct values, except if we force a call by eg opening/closing the chat window
2024-06-11 13:59:11 +03:00
damencho
d0130f9975 fix(breakout-rooms): Closes menu after clicking on it.
The menu with Rename and Close for the breakout rooms.
2024-06-07 16:23:22 -05:00
damencho
efd2db70ca feat(jwt): Adds more logs when jwt is expired. 2024-06-07 09:05:01 -05:00
Horatiu Muresan
befffa7e85 fix(subject) Fix setting and broadcasting subject (#14807) 2024-06-07 16:19:36 +03:00
Saúl Ibarra Corretgé
0368b4d671 feat(rn,config) use WebSockets for XMPP by default on mobile
This aligns mobile and web. WS has been the default on meet.jit.si and
beta.meet.jit.si for quite a while now.
2024-06-07 14:34:44 +02:00
Saúl Ibarra Corretgé
acccd760d1 feat(rn) remove 360p resolution limitation
Layer suspension now works on mobile, let's give this a try once again.
2024-06-07 14:34:13 +02:00
Calin-Teodor
7696dbc347 feat(notifications/native): minor ui refurbishment 2024-06-07 15:06:05 +03:00
Saúl Ibarra Corretgé
e25ceebeec fix(ios) don't pretty-print build output with Fastlane 2024-06-06 15:31:27 +02:00
Saúl Ibarra Corretgé
c7ee6280d2 fix(android) fix build with Java 17 2024-06-06 15:05:20 +02:00
Calin-Teodor
796a2217aa feat(rnsdk): exclude adding metro dep if already available 2024-06-06 00:08:54 +03:00
Saúl Ibarra Corretgé
e9bf1ada51 chore(deps) npm audit fix 2024-06-04 18:59:31 +02:00
Saúl Ibarra Corretgé
667b9fba67 chore(deps) update react-native-webrtc@124.0.0 2024-06-04 18:49:50 +02:00
Calin-Teodor
bbf55e8476 feat(ios/sdk): fix room taken from wrong param 2024-06-04 14:36:37 +03:00
Calinteodor
11b48a9690 feat(sdk/android): custom overflow menu buttons fix (#14795)
* feat(android/sdk): create setConfigOverride for array list of objects
2024-06-04 14:05:18 +03:00
88 changed files with 3067 additions and 1110 deletions

View File

@@ -122,7 +122,7 @@ deploy-local:
dev: deploy-init deploy-css deploy-rnnoise-binary deploy-tflite deploy-meet-models deploy-lib-jitsi-meet deploy-olm deploy-tf-wasm deploy-excalidraw-dev deploy-face-landmarks
$(WEBPACK_DEV_SERVER)
source-package:
source-package: compile deploy
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html resources/*.txt fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
cp css/all.css source_package/jitsi-meet/css && \

View File

@@ -37,7 +37,6 @@ import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
/**
* The one and only Activity that the Jitsi Meet app needs. The
@@ -74,7 +73,6 @@ public class MainActivity extends JitsiMeetActivity {
*/
private String defaultURL;
// JitsiMeetActivity overrides
//
@@ -146,12 +144,12 @@ public class MainActivity extends JitsiMeetActivity {
}
private void setJitsiMeetConferenceDefaultOptions() {
// Set default options
JitsiMeetConferenceOptions defaultOptions
= new JitsiMeetConferenceOptions.Builder()
.setServerURL(buildURL(defaultURL))
.setFeatureFlag("welcomepage.enabled", true)
.setFeatureFlag("resolution", 360)
.setFeatureFlag("server-url-change.enabled", !configurationByRestrictions)
.build();
JitsiMeet.setDefaultConferenceOptions(defaultOptions);

View File

@@ -11,7 +11,7 @@
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx1024m -XX:MaxPermSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit

View File

@@ -47,7 +47,7 @@ dependencies {
implementation 'com.facebook.fresco:animated-gif:2.5.0'
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.jakewharton.timber:timber:5.0.1'
implementation 'com.squareup.duktape:duktape-android:1.3.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'androidx.startup:startup-runtime:1.1.0'

View File

@@ -21,6 +21,7 @@ import android.os.Parcel;
import android.os.Parcelable;
import java.net.URL;
import java.util.ArrayList;
/**
@@ -229,6 +230,12 @@ public class JitsiMeetConferenceOptions implements Parcelable {
return this;
}
public Builder setConfigOverride(String config, ArrayList<Bundle> arrayList) {
this.config.putParcelableArrayList(config, arrayList);
return this;
}
/**
* Builds the immutable {@link JitsiMeetConferenceOptions} object with the configuration
* that this {@link Builder} instance specified.

View File

@@ -91,9 +91,6 @@ var config = {
// Enables supports for AV1 codec.
// enableAv1Support: false,
// Enables XMPP WebSocket (as opposed to BOSH) for the given amount of users.
// mobileXmppWsThreshold: 10, // enable XMPP WebSockets on mobile for 10% of the users
// P2P test mode disables automatic switching to P2P when there are 2
// participants in the conference.
// p2pTestMode: false,
@@ -1770,7 +1767,7 @@ var config = {
// // to control the performance.
// userLimit: 25,
// // The url for more info about the whiteboard and its usage limitations.
// limitUrl: 'https://example.com/blog/whiteboard-limits,
// limitUrl: 'https://example.com/blog/whiteboard-limits',
// },
// The watchRTC initialize config params as described :

View File

@@ -45,7 +45,7 @@ target 'JitsiMeetSDK' do
# Native pod dependencies
#
pod 'CocoaLumberjack', '3.7.2'
pod 'CocoaLumberjack', '3.7.4'
pod 'ObjectiveDropboxOfficial', '6.2.3'
end
@@ -70,7 +70,7 @@ target 'JitsiMeetSDKLite' do
# Native pod dependencies
#
pod 'CocoaLumberjack', '3.7.2'
pod 'CocoaLumberjack', '3.7.4'
end
post_install do |installer|

View File

@@ -10,9 +10,9 @@ PODS:
- AppAuth/ExternalUserAgent (1.6.2):
- AppAuth/Core
- boost (1.76.0)
- CocoaLumberjack (3.7.2):
- CocoaLumberjack/Core (= 3.7.2)
- CocoaLumberjack/Core (3.7.2)
- CocoaLumberjack (3.7.4):
- CocoaLumberjack/Core (= 3.7.4)
- CocoaLumberjack/Core (3.7.4)
- DoubleConversion (1.1.6)
- FBLazyVector (0.72.14)
- FBReactNativeSpec (0.72.14):
@@ -134,7 +134,7 @@ PODS:
- AppAuth/Core (~> 1.6)
- GTMSessionFetcher/Core (< 4.0, >= 1.5)
- GTMSessionFetcher/Core (3.2.0)
- JitsiWebRTC (118.0.0)
- JitsiWebRTC (124.0.0)
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
- libwebp/mux (= 1.3.2)
@@ -453,8 +453,8 @@ PODS:
- react-native-video/Video (6.0.0-alpha.11):
- PromisesSwift
- React-Core
- react-native-webrtc (118.0.7):
- JitsiWebRTC (~> 118.0.0)
- react-native-webrtc (124.0.1):
- JitsiWebRTC (~> 124.0.0)
- React-Core
- react-native-webview (13.8.7):
- RCT-Folly (= 2021.07.22.00)
@@ -597,7 +597,7 @@ PODS:
DEPENDENCIES:
- "amplitude-react-native (from `../node_modules/@amplitude/react-native`)"
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- CocoaLumberjack (= 3.7.2)
- CocoaLumberjack (= 3.7.4)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
@@ -829,7 +829,7 @@ SPEC CHECKSUMS:
amplitude-react-native: 0ed8cab759aafaa94961b82122bf56297da607ad
AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570
boost: 7dcd2de282d72e344012f7d6564d024930a6a440
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: d98eefb42c5a64cb28ef966bd9096c76770d8f24
FBReactNativeSpec: 53d4eb00e8e1b6e987a3dd5906d2afe131cc54c8
@@ -850,7 +850,7 @@ SPEC CHECKSUMS:
GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34
GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae
GTMSessionFetcher: 41b9ef0b4c08a6db4b7eb51a21ae5183ec99a2c8
JitsiWebRTC: 3a41671ef65a51d7204323814b055a2690b921c7
JitsiWebRTC: 37fb2fb70d42cac58c06948527a5f9e1b3f50812
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
@@ -882,7 +882,7 @@ SPEC CHECKSUMS:
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: 472b7c366eaaaa0207e546d9a50410df89790bcf
react-native-webrtc: 8b024c7bb9a005d2b9efeba4c691172dbd00268d
react-native-webrtc: 8b9b90ab7ad5b77811a22a3c3ac1fe2a64737e29
react-native-webview: 95464c1249cae28186d3b7c6f494c6a124b16160
React-NativeModulesApple: 9b58b8f5c1d8ea68d0a63f569a9e786d01281ea6
React-perflogger: daabc494c6328efc1784a4b49b8b74fca305d11c
@@ -915,6 +915,6 @@ SPEC CHECKSUMS:
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: c32e0be1a17f8f1f0e633a3122f7666441f52c82
PODFILE CHECKSUM: ec00682c7062a323dff24a3c3643ca0bbb420d01
PODFILE CHECKSUM: 64167bdb1a26f5768c6bfc81ef60633b31abf8e5
COCOAPODS: 1.15.2

View File

@@ -35,7 +35,6 @@
jitsiMeet.defaultConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
[builder setFeatureFlag:@"welcomepage.enabled" withBoolean:YES];
[builder setFeatureFlag:@"resolution" withValue:@(360)];
[builder setFeatureFlag:@"ios.screensharing.enabled" withBoolean:YES];
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
}];

View File

@@ -79,7 +79,8 @@ platform :ios do
build_app(
scheme: "JitsiMeet",
include_symbols: true,
export_xcargs: "-allowProvisioningUpdates"
export_xcargs: "-allowProvisioningUpdates",
xcodebuild_formatter: ""
)
# Upload the build to TestFlight

View File

@@ -62,7 +62,7 @@
// Initialize the one and only bridge for interfacing with React Native.
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
// Initialize the listener for handling start/stop screensharing notifications.
_screenshareEventEmiter = [[ScheenshareEventEmiter alloc] init];
@@ -141,7 +141,7 @@
if (_bridgeWrapper != nil) {
return;
};
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
}
@@ -231,7 +231,7 @@
}
- (void)setDefaultConferenceOptions:(JitsiMeetConferenceOptions *)defaultConferenceOptions {
if (defaultConferenceOptions != nil && _defaultConferenceOptions.room != nil) {
if (defaultConferenceOptions != nil && defaultConferenceOptions.room != nil) {
@throw [NSException exceptionWithName:@"RuntimeError"
reason:@"'room' must be null in the default conference options"
userInfo:nil];

View File

@@ -26,6 +26,7 @@
"hsb": "Hornjoserbšćina",
"hu": "Magyar",
"hy": "Հայերեն",
"id": "Bahasa",
"is": "Íslenska",
"it": "Italiano",
"ja": "日本語",

1559
lang/main-id.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -100,7 +100,7 @@ import {
} from '../../react/features/participants-pane/actions';
import { getParticipantsPaneOpen, isForceMuted } from '../../react/features/participants-pane/functions';
import { startLocalVideoRecording, stopLocalVideoRecording } from '../../react/features/recording/actions.any';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
@@ -629,6 +629,7 @@ function initCommands() {
* @param { string } arg.youtubeStreamKey - The youtube stream key.
* @param { string } arg.youtubeBroadcastID - The youtube broadcast ID.
* @param { Object } arg.extraMetadata - Any extra metadata params for file recording.
* @param { boolean } arg.transcription - Whether a transcription should be started or not.
* @returns {void}
*/
'start-recording': ({
@@ -640,7 +641,8 @@ function initCommands() {
rtmpBroadcastID,
youtubeStreamKey,
youtubeBroadcastID,
extraMetadata = {}
extraMetadata = {},
transcription
}) => {
const state = APP.store.getState();
const conference = getCurrentConference(state);
@@ -715,25 +717,33 @@ function initCommands() {
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey || rtmpStreamKey
};
} else {
logger.error('Invalid recording mode provided');
return;
}
if (isScreenshotCaptureEnabled(state, true, false)) {
APP.store.dispatch(toggleScreenshotCaptureSummary(true));
}
conference.startRecording(recordingConfig);
// Start audio / video recording, if requested.
if (typeof recordingConfig !== 'undefined') {
conference.startRecording(recordingConfig);
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(true, false, null));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: true
});
}
},
/**
* Stops a recording or streaming in progress.
*
* @param {string} mode - `local`, `file` or `stream`.
* @param {boolean} transcription - Whether the transcription needs to be stopped.
* @returns {void}
*/
'stop-recording': mode => {
'stop-recording': (mode, transcription) => {
const state = APP.store.getState();
const conference = getCurrentConference(state);
@@ -743,6 +753,13 @@ function initCommands() {
return;
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(false, false, null));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false
});
}
if (mode === 'local') {
APP.store.dispatch(stopLocalVideoRecording());
@@ -1917,14 +1934,16 @@ class API {
* @param {boolean} on - True if recording is on, false otherwise.
* @param {string} mode - Stream or file or local.
* @param {string} error - Error type or null if success.
* @param {boolean} transcription - True if a transcription is being recorded, false otherwise.
* @returns {void}
*/
notifyRecordingStatusChanged(on, mode, error) {
notifyRecordingStatusChanged(on, mode, error, transcription) {
this._sendEvent({
name: 'recording-status-changed',
on,
mode,
error
error,
transcription
});
}

View File

@@ -1446,6 +1446,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* @param { string } options.youtubeStreamKey - The youtube stream key.
* @param { string } options.youtubeBroadcastID - The youtube broadcast ID.
* @param {Object } options.extraMetadata - Any extra metadata params for file recording.
* @param { boolean } arg.transcription - Whether a transcription should be started or not.
* @returns {void}
*/
startRecording(options) {
@@ -1456,10 +1457,11 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* Stops a recording or streaming session that is in progress.
*
* @param {string} mode - `file` or `stream`.
* @param {boolean} transcription - Whether the transcription needs to be stopped.
* @returns {void}
*/
stopRecording(mode) {
this.executeCommand('stopRecording', mode);
stopRecording(mode, transcription) {
this.executeCommand('stopRecording', mode, transcription);
}
/**

685
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,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/v1830.0.0+5a14bd43/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1837.0.0+6bcc577a/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -105,7 +105,7 @@
"react-native-url-polyfill": "2.0.0",
"react-native-video": "6.0.0-alpha.11",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "118.0.7",
"react-native-webrtc": "124.0.1",
"react-native-webview": "13.8.7",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
@@ -181,7 +181,7 @@
"webpack": "5.76.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "4.9.0",
"webpack-dev-server": "4.7.3"
"webpack-dev-server": "4.15.2"
},
"overrides": {
"@xmldom/xmldom": "0.8.7"

View File

@@ -21,6 +21,11 @@ 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;
}

View File

@@ -151,7 +151,7 @@ export function appNavigate(uri?: string, options: IReloadNowOptions = {}) {
}
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config, locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
if (!room) {

View File

@@ -74,7 +74,7 @@ export function appNavigate(uri?: string) {
const config = await loadConfig();
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config, locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
};
}

View File

@@ -984,12 +984,12 @@ export function setStartMutedPolicy(
* @param {string} subject - The new subject.
* @returns {void}
*/
export function setSubject(subject: string | undefined) {
export function setSubject(subject: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { conference } = getState()['features/base/conference'];
if (conference) {
conference.setSubject(subject || '');
conference.setSubject(subject);
} else {
dispatch({
type: SET_PENDING_SUBJECT_CHANGE,
@@ -1008,7 +1008,7 @@ export function setSubject(subject: string | undefined) {
* localSubject: string
* }}
*/
export function setLocalSubject(localSubject: string | undefined) {
export function setLocalSubject(localSubject: string) {
return {
type: CONFERENCE_LOCAL_SUBJECT_CHANGED,
localSubject

View File

@@ -185,18 +185,12 @@ export function forEachConference(
export function getConferenceName(stateful: IStateful): string {
const state = toState(stateful);
const { callee } = state['features/base/jwt'];
const {
callDisplayName,
localSubject: configLocalSubject,
subject: configSubject
} = state['features/base/config'];
const { callDisplayName } = state['features/base/config'];
const { localSubject, pendingSubjectChange, room, subject } = getConferenceState(state);
return (pendingSubjectChange
|| configSubject
return (localSubject
|| pendingSubjectChange
|| subject
|| configLocalSubject
|| localSubject
|| callDisplayName
|| callee?.name
|| (room && safeStartCase(safeDecodeURIComponent(room)))) ?? '';

View File

@@ -3,6 +3,8 @@ import { AnyAction } from 'redux';
import { FaceLandmarks } from '../../face-landmarks/types';
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock/constants';
import { ISpeakerStats } from '../../speaker-stats/reducer';
import { SET_CONFIG } from '../config/actionTypes';
import { IConfig } from '../config/configType';
import { CONNECTION_WILL_CONNECT, SET_LOCATION_URL } from '../connection/actionTypes';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import ReducerRegistry from '../redux/ReducerRegistry';
@@ -278,11 +280,33 @@ ReducerRegistry.register<IConferenceState>('features/base/conference',
...state,
metadata: action.metadata
};
case SET_CONFIG:
return _setConfig(state, action);
}
return state;
});
/**
* Processes subject and local subject of the conference based on the new config.
*
* @param {Object} state - The Redux state of feature base/conference.
* @param {Action} action - The Redux action SET_CONFIG to reduce.
* @private
* @returns {Object} The new state after the reduction of the specified action.
*/
function _setConfig(state: IConferenceState, { config }: { config: IConfig; }) {
const { localSubject, subject } = config;
return {
...state,
localSubject,
pendingSubjectChange: subject,
subject: undefined
};
}
/**
* Reduces a specific Redux action AUTH_STATUS_CHANGED of the feature
* base/conference.
@@ -606,10 +630,7 @@ function _setRoom(state: IConferenceState, action: AnyAction) {
*/
return assign(state, {
error: undefined,
localSubject: undefined,
pendingSubjectChange: undefined,
room,
subject: undefined
room
});
}

View File

@@ -96,51 +96,53 @@ export function overwriteConfig(config: Object) {
*
* @param {Object} config - The configuration to be represented by the feature
* base/config.
* @param {URL} locationURL - The URL of the location which necessitated the
* loading of a configuration.
* @returns {Function}
*/
export function setConfig(config: IConfig = {}, locationURL: URL | undefined) {
// Now that the loading of the config was successful override the values
// with the parameters passed in the hash part of the location URI.
// TODO We're still in the middle ground between old Web with config,
// and interfaceConfig used via global variables and new
// Web and mobile reading the respective values from the redux store.
// Only the config will be overridden on React Native, as the other
// globals will be undefined here. It's intentional - we do not care to
// override those configs yet.
locationURL
&& setConfigFromURLParams(
export function setConfig(config: IConfig = {}) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { locationURL } = getState()['features/base/connection'];
// On Web the config also comes from the window.config global,
// but it is resolved in the loadConfig procedure.
config,
window.interfaceConfig,
locationURL);
// Now that the loading of the config was successful override the values
// with the parameters passed in the hash part of the location URI.
// TODO We're still in the middle ground between old Web with config,
// and interfaceConfig used via global variables and new
// Web and mobile reading the respective values from the redux store.
// Only the config will be overridden on React Native, as the other
// globals will be undefined here. It's intentional - we do not care to
// override those configs yet.
locationURL
&& setConfigFromURLParams(
let { bosh } = config;
// On Web the config also comes from the window.config global,
// but it is resolved in the loadConfig procedure.
config,
window.interfaceConfig,
locationURL);
if (bosh) {
// Normalize the BOSH URL.
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL?.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL?.href);
let { bosh } = config;
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
if (bosh) {
// Normalize the BOSH URL.
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL?.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL?.href);
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
config.bosh = bosh;
}
config.bosh = bosh;
}
return {
type: SET_CONFIG,
config
dispatch({
type: SET_CONFIG,
config
});
};
}

View File

@@ -544,7 +544,6 @@ export interface IConfig {
assumeBandwidth?: boolean;
disableE2EE?: boolean;
dumpTranscript?: boolean;
mobileXmppWsThreshold?: number;
noAutoPlayVideo?: boolean;
p2pTestMode?: boolean;
skipInterimTranscriptions?: boolean;

View File

@@ -126,13 +126,6 @@ export function constructOptions(state: IReduxState) {
const { bosh, preferBosh, flags } = options;
let { websocket } = options;
// TESTING: Only enable WebSocket for some percentage of users.
if (websocket && navigator.product === 'ReactNative') {
if ((Math.random() * 100) >= (options?.testing?.mobileXmppWsThreshold ?? 0)) {
websocket = undefined;
}
}
if (preferBosh) {
websocket = undefined;
}

View File

@@ -19,3 +19,14 @@ export function isIosMobileBrowser() {
return Platform.OS === 'ios';
}
/**
* Returns whether or not the current environment is an ipad device.
*
* @returns {boolean}
*/
export function isIpadMobileBrowser() {
// @ts-ignore
return isIosMobileBrowser() && Platform.isPad;
}

View File

@@ -12,8 +12,7 @@ export const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
export const JitsiConferenceEvents = JitsiMeetJS.events.conference;
export const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
export const JitsiConnectionEvents = JitsiMeetJS.events.connection;
export const JitsiConnectionQualityEvents
= JitsiMeetJS.events.connectionQuality;
export const JitsiConnectionQualityEvents = JitsiMeetJS.events.connectionQuality;
export const JitsiDetectionEvents = JitsiMeetJS.events.detection;
export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;

View File

@@ -1,11 +1,16 @@
// @ts-ignore
const { userAgent, maxTouchPoints, platform } = navigator;
let OS = '';
const { maxTouchPoints, platform, userAgent } = navigator;
let OS = '',
isPad = false;
if (userAgent.match(/Android/i)) {
OS = 'android';
} else if (userAgent.match(/iP(ad|hone|od)/i) || (maxTouchPoints && maxTouchPoints > 2 && /MacIntel/.test(platform))) {
OS = 'ios';
} else if (userAgent.match(/iP(ad)/i)) {
OS = 'ios';
isPad = true;
} else if (userAgent.match(/Mac(intosh| OS X)/i)) {
OS = 'macos';
} else if (userAgent.match(/Windows/i)) {
@@ -18,6 +23,13 @@ if (userAgent.match(/Android/i)) {
* Provides a minimal equivalent of react-native's Platform abstraction.
*/
export default {
/**
* Returns a boolean which defines if device is an iPad.
*
* @type {boolean}
*/
isPad,
/**
* The operating system on which the application is executing.
*

View File

@@ -35,6 +35,10 @@ const REDUCED_UI_THRESHOLD = 300;
*/
export function clientResized(clientWidth: number, clientHeight: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!clientWidth && !clientHeight) {
return;
}
let availableWidth = clientWidth;
if (navigator.product !== 'ReactNative') {

View File

@@ -1,5 +1,6 @@
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
import { CONFERENCE_JOINED } from '../conference/actionTypes';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { clientResized } from './actions';
@@ -27,6 +28,19 @@ MiddlewareRegistry.register(store => next => action => {
_appWillMount(store);
break;
case CONFERENCE_JOINED: {
const { clientHeight = 0, clientWidth = 0 } = store.getState()['features/base/responsive-ui'];
if (!clientHeight && !clientWidth) {
const {
innerHeight,
innerWidth
} = window;
store.dispatch(clientResized(innerWidth, innerHeight));
}
break;
}
}
return result;

View File

@@ -1,8 +1,13 @@
import { IReduxState, IStore } from '../../app/types';
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
import { VIDEO_CODEC } from '../../video-quality/constants';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
import { getTrackByMediaTypeAndParticipant, getVideoTrackByParticipant } from '../tracks/functions';
import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
getVideoTrackByParticipant
} from '../tracks/functions';
/**
* Indicates whether the test mode is enabled. When it's enabled
@@ -51,6 +56,78 @@ export function isLargeVideoReceived({ getState }: IStore): boolean {
return Boolean(videoTrack && !videoTrack.muted && isTrackStreamingStatusActive(videoTrack));
}
/**
* Returns whether the local video track is encoded in AV1.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingAv1({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.AV1) {
return true;
}
return false;
}
/**
* Returns whether the local video track is encoded in H.264.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingH264({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.H264) {
return true;
}
return false;
}
/**
* Returns whether the local video track is encoded in VP8.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingVp8({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.VP8) {
return true;
}
return false;
}
/**
* Returns whether the local video track is encoded in VP9.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingVp9({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.VP9) {
return true;
}
return false;
}
/**
* Returns whether the last media event received for a remote video indicates that the video is playing, if not muted.
*

View File

@@ -10,6 +10,10 @@ import { setConnectionState } from './actions';
import {
getRemoteVideoType,
isLargeVideoReceived,
isLocalCameraEncodingAv1,
isLocalCameraEncodingH264,
isLocalCameraEncodingVp8,
isLocalCameraEncodingVp9,
isRemoteVideoReceived,
isTestModeEnabled
} from './functions';
@@ -86,6 +90,10 @@ function _bindTortureHelpers(store: IStore) {
getJitsiMeetGlobalNS().testing = {
getRemoteVideoType: getRemoteVideoType.bind(null, store),
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
isLocalCameraEncodingAv1: isLocalCameraEncodingAv1.bind(null, store),
isLocalCameraEncodingH264: isLocalCameraEncodingH264.bind(null, store),
isLocalCameraEncodingVp8: isLocalCameraEncodingVp8.bind(null, store),
isLocalCameraEncodingVp9: isLocalCameraEncodingVp9.bind(null, store),
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
};
}

View File

@@ -40,7 +40,7 @@ import {
getTrackByJitsiTrack
} from './functions';
import logger from './logger';
import { ITrackOptions } from './types';
import { ITrack, ITrackOptions } from './types';
/**
* Add a given local track to the conference.
@@ -450,6 +450,32 @@ export function trackAdded(track: any) {
};
}
/**
* Create an action for when a track's codec has been signaled to have been changed.
*
* @param {JitsiLocalTrack} track - JitsiLocalTrack instance.
* @param {string} codec - The video codec.
* @returns {{
* type: TRACK_UPDATED,
* track: Track
* }}
*/
export function trackCodecChanged(track: ITrack, codec: string): {
track: {
codec: string;
jitsiTrack: any;
};
type: 'TRACK_UPDATED';
} {
return {
type: TRACK_UPDATED,
track: {
codec,
jitsiTrack: track
}
};
}
/**
* Create an action for when a track's muted state has been signaled to be
* changed.

View File

@@ -44,6 +44,7 @@ export interface ITrackOptions {
* any.
*/
export interface ITrack {
codec: string;
getOriginalStream: Function;
isReceivingData: boolean;
jitsiTrack: any;

View File

@@ -39,7 +39,7 @@ const useContextMenu = <T>(): [(force?: boolean | Object) => void,
return;
}
if (raiseContext !== initialState) {
if (raiseContext !== initialState || force) {
setRaiseContext(initialState);
}
});

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { FlatList } from 'react-native';
import { useSelector } from 'react-redux';
@@ -33,6 +33,19 @@ const BreakoutRooms = () => {
.sort((p1, p2) => (p1?.name || '').localeCompare(p2?.name || ''));
const showAddBreakoutRoom = useSelector(isAddBreakoutRoomButtonVisible);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
const renderListHeaderComponent = useMemo(() => (
<>
{ showAutoAssign && <AutoAssignButton /> }
{ inBreakoutRoom && <LeaveBreakoutRoomButton /> }
{
isBreakoutRoomsSupported
&& rooms.map(room => (<CollapsibleRoom
key = { room.id }
room = { room }
roomId = { room.id } />))
}
</>
), [ showAutoAssign, inBreakoutRoom, isBreakoutRoomsSupported, rooms ]);
return (
<JitsiScreen
@@ -42,21 +55,7 @@ const BreakoutRooms = () => {
{ /* Fixes warning regarding nested lists */ }
<FlatList
/* eslint-disable react/jsx-no-bind */
ListHeaderComponent = { () => (
<>
{ showAutoAssign && <AutoAssignButton /> }
{ inBreakoutRoom && <LeaveBreakoutRoomButton /> }
{
isBreakoutRoomsSupported
&& rooms.map(room => (<CollapsibleRoom
key = { room.id }
room = { room }
roomId = { room.id } />))
}
</>
) }
ListHeaderComponent = { renderListHeaderComponent }
data = { [] as ReadonlyArray<undefined> }
keyExtractor = { keyExtractor }
renderItem = { null }

View File

@@ -2,12 +2,11 @@ import React, { useCallback } from 'react';
import { TouchableOpacity } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { openHighlightDialog } from '../../../recording/actions.native';
import HighlightButton from '../../../recording/components/Recording/native/HighlightButton';
import RecordingLabel from '../../../recording/components/native/RecordingLabel';
import { getActiveSession } from '../../../recording/functions';
import { isLiveStreamingRunning } from '../../../recording/functions';
import VisitorsCountLabel from '../../../visitors/components/native/VisitorsCountLabel';
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
@@ -30,8 +29,7 @@ interface IProps {
const AlwaysOnLabels = ({ createOnPress }: IProps) => {
const dispatch = useDispatch();
const isStreaming = useSelector((state: IReduxState) =>
Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM)));
const isStreaming = useSelector(isLiveStreamingRunning);
const openHighlightDialogCallback = useCallback(() =>
dispatch(openHighlightDialog()), [ dispatch ]);

View File

@@ -4,6 +4,8 @@ import { IJitsiConference } from '../base/conference/reducer';
import {
JitsiConnectionQualityEvents
} from '../base/lib-jitsi-meet';
import { trackCodecChanged } from '../base/tracks/actions.any';
import { getLocalTracks } from '../base/tracks/functions.any';
/**
* Contains all the callbacks to be notified when stats are updated.
@@ -129,6 +131,10 @@ const statsEmitter = {
codec: allUserCodecs[localUserId as keyof typeof allUserCodecs]
});
modifiedLocalStats.codec
&& Object.keys(modifiedLocalStats.codec).length
&& this._updateLocalCodecs(modifiedLocalStats.codec);
this._emitStatsUpdate(localUserId, modifiedLocalStats);
// Get all the unique user ids from the framerate and resolution stats
@@ -162,6 +168,33 @@ const statsEmitter = {
this._emitStatsUpdate(id, remoteUserStats);
});
},
/**
* Updates the codec associated with the local tracks.
* This is currently used for torture tests.
*
* @param {any} codecs - Codec information per local SSRC.
* @returns {void}
*/
_updateLocalCodecs(codecs: any) {
if (typeof APP !== 'undefined') {
const tracks = APP.store.getState()['features/base/tracks'];
const localTracks = getLocalTracks(tracks);
for (const track of localTracks) {
const ssrc = track.jitsiTrack?.getSsrc();
if (ssrc && Object.keys(codecs).find(key => Number(key) === ssrc)) {
const codecsPerSsrc = codecs[ssrc];
const codec = codecsPerSsrc.audio ?? codecsPerSsrc.video;
if (track.codec !== codec) {
APP.store.dispatch(trackCodecChanged(track.jitsiTrack, codec));
}
}
}
}
}
};

View File

@@ -0,0 +1,25 @@
import { useSelector } from 'react-redux';
import { isMobileBrowser } from '../base/environment/utils';
import { isVpaasMeeting } from '../jaas/functions';
import EmbedMeetingButton from './components/EmbedMeetingButton';
const embed = {
key: 'embedmeeting',
Content: EmbedMeetingButton,
group: 4
};
/**
* A hook that returns the embed button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useEmbedButton() {
const _isVpaasMeeting = useSelector(isVpaasMeeting);
if (!isMobileBrowser() && !_isVpaasMeeting) {
return embed;
}
}

View File

@@ -0,0 +1,24 @@
import { useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
import SharedDocumentButtonWeb from './components/SharedDocumentButton';
const etherpad = {
key: 'etherpad',
Content: SharedDocumentButtonWeb,
group: 3
};
/**
* A hook that returns the etherpad button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useEtherpadButton() {
const visible = useSelector((state: IReduxState) => Boolean(state['features/etherpad'].documentUrl));
if (visible) {
return etherpad;
}
}

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import FeedbackButtonWeb from './components/FeedbackButton.web';
import { shouldSendJaaSFeedbackMetadata } from './functions.web';
const feedback = {
key: 'feedback',
Content: FeedbackButtonWeb,
group: 4
};
/**
* A hook that returns the feedback button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useFeedbackButton() {
const visible = useSelector(shouldSendJaaSFeedbackMetadata);
if (visible) {
return feedback;
}
}

View File

@@ -0,0 +1,25 @@
import { useSelector } from 'react-redux';
import { isMobileBrowser } from '../base/environment/utils';
import KeyboardShortcutsButton from './components/web/KeyboardShortcutsButton';
import { areKeyboardShortcutsEnabled } from './functions';
const shortcuts = {
key: 'shortcuts',
Content: KeyboardShortcutsButton,
group: 4
};
/**
* A hook that returns the keyboard shortcuts button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useKeyboardShortcutsButton() {
const _areKeyboardShortcutsEnabled = useSelector(areKeyboardShortcutsEnabled);
if (!isMobileBrowser() && _areKeyboardShortcutsEnabled) {
return shortcuts;
}
}

View File

@@ -161,7 +161,7 @@ const Notification = ({
<Text
key = { index }
style = { styles.contentText }>
{replaceNonUnicodeEmojis(line)}
{ replaceNonUnicodeEmojis(line) }
</Text>
))
}
@@ -200,7 +200,7 @@ const Notification = ({
<View
pointerEvents = 'box-none'
style = { styles.contentContainer }>
{_renderContent()}
{ _renderContent() }
</View>
<View style = { styles.btnContainer as ViewStyle }>
{ mapAppearanceToButtons() }

View File

@@ -3,7 +3,7 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const contentColumn = {
flex: 1,
flexDirection: 'column',
marginLeft: BaseTheme.spacing[2]
paddingLeft: BaseTheme.spacing[2]
};
const notification = {
@@ -14,9 +14,9 @@ const notification = {
display: 'flex',
flexDirection: 'row',
height: 'auto',
marginBottom: BaseTheme.spacing[2],
marginHorizontal: BaseTheme.spacing[2],
maxWidth: 400,
paddingBottom: BaseTheme.spacing[2],
paddingHorizontal: BaseTheme.spacing[2],
maxWidth: 432,
width: 'auto'
};
@@ -42,20 +42,20 @@ export default {
*/
contentContainer: {
marginHorizontal: BaseTheme.spacing[2]
paddingHorizontal: BaseTheme.spacing[2]
},
contentText: {
color: BaseTheme.palette.text04,
marginLeft: BaseTheme.spacing[5],
marginTop: BaseTheme.spacing[1]
paddingLeft: BaseTheme.spacing[5],
paddingTop: BaseTheme.spacing[1]
},
contentTextTitle: {
color: BaseTheme.palette.text04,
fontWeight: 'bold',
marginLeft: BaseTheme.spacing[5],
marginTop: BaseTheme.spacing[2]
paddingLeft: BaseTheme.spacing[5],
paddingTop: BaseTheme.spacing[2]
},
/**
@@ -89,19 +89,20 @@ export default {
},
iconContainer: {
left: BaseTheme.spacing[1],
position: 'absolute',
top: BaseTheme.spacing[2]
left: BaseTheme.spacing[2],
top: 12
},
btn: {
marginLeft: BaseTheme.spacing[4]
paddingLeft: BaseTheme.spacing[3]
},
btnContainer: {
display: 'flex',
flexDirection: 'row',
marginLeft: BaseTheme.spacing[3]
paddingLeft: BaseTheme.spacing[4],
paddingTop: BaseTheme.spacing[1]
},
withToolbox: {

View File

@@ -21,6 +21,13 @@ const ParticipantsPane = () => {
const isLocalModerator = useSelector(isLocalParticipantModerator);
const keyExtractor
= useCallback((e: undefined, i: number) => i.toString(), []);
const renderListHeaderComponent = () => (
<>
<VisitorsList />
<LobbyParticipantList />
<MeetingParticipantList />
</>
);
return (
<JitsiScreen
@@ -31,13 +38,7 @@ const ParticipantsPane = () => {
<FlatList
// eslint-disable-next-line react/jsx-no-bind
ListHeaderComponent = { () => (
<>
<VisitorsList />
<LobbyParticipantList />
<MeetingParticipantList />
</>
) }
ListHeaderComponent = { renderListHeaderComponent }
data = { [] as ReadonlyArray<undefined> }
keyExtractor = { keyExtractor }
renderItem = { null }

View File

@@ -1,14 +1,23 @@
import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { handleLobbyChatInitialized } from '../chat/actions.web';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions.web';
import ParticipantsPaneButton from './components/web/ParticipantsPaneButton';
import { isParticipantsPaneEnabled } from './functions';
interface IDrawerParticipant {
displayName?: string;
participantID: string;
}
const participants = {
key: 'participants-pane',
Content: ParticipantsPaneButton,
group: 2
};
/**
* Hook used to create admit/reject lobby actions.
*
@@ -57,3 +66,16 @@ export function useParticipantDrawer(): [
openDrawerForParticipant
];
}
/**
* A hook that returns the participants pane button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useParticipantPaneButton() {
const participantsPaneEnabled = useSelector(isParticipantsPaneEnabled);
if (participantsPaneEnabled) {
return participants;
}
}

View File

@@ -12,7 +12,7 @@ import { editPoll } from '../../actions';
import { isSubmitAnswerDisabled } from '../../functions';
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
import { chatStyles, dialogStyles } from './styles';
import { dialogStyles, pollsStyles } from './styles';
const PollAnswer = (props: AbstractProps) => {
const {
@@ -39,17 +39,17 @@ const PollAnswer = (props: AbstractProps) => {
t('polls.by', { name: localParticipant?.name })
}
</Text>
<View style = { chatStyles.answerContent as ViewStyle }>
<View style = { pollsStyles.answerContent as ViewStyle }>
{
poll.answers.map((answer, index: number) => (
<View
key = { index }
style = { chatStyles.switchRow as ViewStyle } >
style = { pollsStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
disabled = { poll.saved }
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel as TextStyle }>
<Text style = { pollsStyles.switchLabel as TextStyle }>
{ answer.name }
</Text>
</View>
@@ -58,7 +58,7 @@ const PollAnswer = (props: AbstractProps) => {
</View>
{
pollSaved
? <View style = { chatStyles.buttonRow as ViewStyle }>
? <View style = { pollsStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.edit'
labelKey = 'polls.answer.edit'
@@ -66,28 +66,28 @@ const PollAnswer = (props: AbstractProps) => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
style = { chatStyles.pollCreateButton }
style = { pollsStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.send'
labelKey = 'polls.answer.send'
onClick = { sendPoll }
style = { chatStyles.pollCreateButton }
style = { pollsStyles.pollCreateButton }
type = { PRIMARY } />
</View>
: <View style = { chatStyles.buttonRow as ViewStyle }>
: <View style = { pollsStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { chatStyles.pollCreateButton }
style = { pollsStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { chatStyles.pollCreateButton }
style = { pollsStyles.pollCreateButton }
type = { PRIMARY } />
</View>
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
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 { Divider } from 'react-native-paper';
@@ -7,13 +7,11 @@ import { useDispatch } from 'react-redux';
import Button from '../../../base/ui/components/native/Button';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import styles
from '../../../settings/components/native/styles';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
import { chatStyles, dialogStyles } from './styles';
import { dialogStyles, pollsStyles } from './styles';
const PollCreate = (props: AbstractProps) => {
const {
@@ -98,7 +96,7 @@ const PollCreate = (props: AbstractProps) => {
);
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
? chatStyles.pollCreateButtonsContainerAndroid : chatStyles.pollCreateButtonsContainerIos;
? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
/* eslint-disable react/jsx-no-bind */
const renderListItem = ({ index }: { index: number; }) => {
@@ -119,7 +117,6 @@ const PollCreate = (props: AbstractProps) => {
id = { `polls-answer-input-${index}` }
label = { t('polls.create.pollOption', { index: index + 1 }) }
maxLength = { CHAR_LIMIT }
multiline = { true }
onChange = { name => setAnswer(index,
{
name,
@@ -140,26 +137,30 @@ const PollCreate = (props: AbstractProps) => {
);
};
return (
<View style = { chatStyles.pollCreateContainer as ViewStyle }>
<View style = { chatStyles.pollCreateSubContainer as ViewStyle }>
<Input
autoFocus = { true }
blurOnSubmit = { false }
customStyles = {{ container: dialogStyles.customContainer }}
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
multiline = { true }
onChange = { setQuestion }
onSubmitEditing = { onQuestionKeyDown }
placeholder = { t('polls.create.questionPlaceholder') }
const renderListHeaderComponent = useMemo(() => (
<>
<Input
autoFocus = { true }
blurOnSubmit = { false }
customStyles = {{ container: dialogStyles.customContainer }}
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
onChange = { setQuestion }
onSubmitEditing = { onQuestionKeyDown }
placeholder = { t('polls.create.questionPlaceholder') }
// This is set to help the touch event not be propagated to any subviews.
pointerEvents = { 'auto' }
value = { question } />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
// This is set to help the touch event not be propagated to any subviews.
pointerEvents = { 'auto' }
value = { question } />
<Divider style = { pollsStyles.fieldSeparator as ViewStyle } />
</>
), [ question ]);
return (
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
<FlatList
ListHeaderComponent = { renderListHeaderComponent }
data = { answers }
extraData = { answers }
keyExtractor = { (item, index) => index.toString() }
@@ -175,10 +176,10 @@ const PollCreate = (props: AbstractProps) => {
addAnswer();
requestFocus(answers.length);
} }
style = { chatStyles.pollCreateAddButton }
style = { pollsStyles.pollCreateAddButton }
type = { SECONDARY } />
<View
style = { chatStyles.buttonRow as ViewStyle }>
style = { pollsStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.create.cancel'
labelKey = 'polls.create.cancel'
@@ -188,14 +189,14 @@ const PollCreate = (props: AbstractProps) => {
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
style = { chatStyles.pollCreateButton }
style = { pollsStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.create.save'
disabled = { isSubmitDisabled }
labelKey = 'polls.create.save'
onClick = { onSubmit }
style = { chatStyles.pollCreateButton }
style = { pollsStyles.pollCreateButton }
type = { PRIMARY } />
</View>
</View>

View File

@@ -6,7 +6,7 @@ import { shouldShowResults } from '../../functions';
import PollAnswer from './PollAnswer';
import PollResults from './PollResults';
import { chatStyles } from './styles';
import { pollsStyles } from './styles';
interface IProps {
@@ -27,7 +27,7 @@ const PollItem = ({ pollId, setCreateMode }: IProps) => {
return (
<View
style = { chatStyles.pollItemContainer as ViewStyle }>
style = { pollsStyles.pollItemContainer as ViewStyle }>
{ showResults
? <PollResults
key = { pollId }

View File

@@ -6,7 +6,7 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import AbstractPollResults from '../AbstractPollResults';
import type { AbstractProps, AnswerInfo } from '../AbstractPollResults';
import { chatStyles, dialogStyles, resultsStyles } from './styles';
import { dialogStyles, pollsStyles, resultsStyles } from './styles';
/**
* Component that renders the poll results.
@@ -100,14 +100,14 @@ const PollResults = (props: AbstractProps) => {
data = { answers }
keyExtractor = { (item, index) => index.toString() }
renderItem = { answer => renderRow(answer.item) } />
<View style = { chatStyles.bottomLinks as ViewStyle }>
<View style = { pollsStyles.bottomLinks as ViewStyle }>
<Button
labelKey = {
showDetails
? 'polls.results.hideDetailedResults'
: 'polls.results.showDetailedResults'
}
labelStyle = { chatStyles.toggleText }
labelStyle = { pollsStyles.toggleText }
onClick = { toggleIsDetailed }
type = { BUTTON_TYPES.TERTIARY } />
<Button
@@ -116,7 +116,7 @@ const PollResults = (props: AbstractProps) => {
? 'polls.results.changeVote'
: 'polls.results.vote'
}
labelStyle = { chatStyles.toggleText }
labelStyle = { pollsStyles.toggleText }
onClick = { changeVote }
type = { BUTTON_TYPES.TERTIARY } />
</View>

View File

@@ -10,7 +10,7 @@ import { IconMessage } from '../../../base/icons/svg';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import PollItem from './PollItem';
import { chatStyles } from './styles';
import { pollsStyles } from './styles';
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
@@ -42,12 +42,12 @@ const PollsList = ({ setCreateMode }: IPollListProps) => {
<>
{
listPolls.length === 0
&& <View style = { chatStyles.noPollContent as ViewStyle }>
&& <View style = { pollsStyles.noPollContent as ViewStyle }>
<Icon
color = { BaseTheme.palette.icon03 }
size = { 160 }
src = { IconMessage } />
<Text style = { chatStyles.noPollText as TextStyle } >
<Text style = { pollsStyles.noPollText as TextStyle } >
{
t('polls.results.empty')
}

View File

@@ -17,7 +17,7 @@ import type { AbstractProps } from '../AbstractPollsPane';
import PollCreate from './PollCreate';
import PollsList from './PollsList';
import { chatStyles } from './styles';
import { pollsStyles } from './styles';
const PollsPane = (props: AbstractProps) => {
const { createMode, onCreate, setCreateMode, t } = props;
@@ -42,14 +42,14 @@ const PollsPane = (props: AbstractProps) => {
}, [ isPollsTabFocused, nbUnreadPolls ]);
const createPollButtonStyles = Platform.OS === 'android'
? chatStyles.createPollButtonAndroid : chatStyles.createPollButtonIos;
? pollsStyles.createPollButtonAndroid : pollsStyles.createPollButtonIos;
return (
<JitsiScreen
contentContainerStyle = { chatStyles.pollPane as StyleType }
contentContainerStyle = { pollsStyles.pollPane as StyleType }
disableForcedKeyboardDismiss = { true }
hasExtraHeaderHeight = { true }
style = { chatStyles.pollPaneContainer as StyleType }>
style = { pollsStyles.pollPaneContainer as StyleType }>
{
createMode
? <PollCreate setCreateMode = { setCreateMode } />

View File

@@ -30,7 +30,6 @@ export const dialogStyles = createStyleSheet({
},
optionRemoveButton: {
marginTop: BaseTheme.spacing[2],
width: 128
},
@@ -110,7 +109,7 @@ export const resultsStyles = createStyleSheet({
}
});
export const chatStyles = createStyleSheet({
export const pollsStyles = createStyleSheet({
noPollContent: {
alignItems: 'center',
@@ -195,7 +194,6 @@ export const chatStyles = createStyleSheet({
pollCreateButton: {
marginHorizontal: BaseTheme.spacing[1],
maxWidth: 160,
flex: 1
},
@@ -229,26 +227,9 @@ export const chatStyles = createStyleSheet({
marginHorizontal: BaseTheme.spacing[1]
},
unreadPollsCounterContainer: {
display: 'flex',
flexDirection: 'row'
},
unreadPollsCounterDescription: {
color: BaseTheme.palette.text01
},
unreadPollsCounterCircle: {
backgroundColor: BaseTheme.palette.warning01,
borderRadius: BaseTheme.spacing[3] / 2,
height: BaseTheme.spacing[3],
justifyContent: 'center',
marginLeft: BaseTheme.spacing[2],
width: BaseTheme.spacing[3]
},
unreadPollsCounter: {
alignSelf: 'center',
color: BaseTheme.palette.text04
fieldSeparator: {
borderBottomWidth: 1,
borderColor: BaseTheme.palette.ui05,
marginTop: BaseTheme.spacing[3]
}
});

View File

@@ -50,9 +50,6 @@ export default class PrejoinApp extends BaseApp<Props> {
? store.getState()['features/base/settings']
: { startWithAudioMuted: undefined,
startWithVideoMuted: undefined };
const { locationURL } = store
? store.getState()['features/base/connection']
: { locationURL: undefined };
dispatch?.(setConfig({
prejoinConfig: {
@@ -60,7 +57,7 @@ export default class PrejoinApp extends BaseApp<Props> {
},
startWithAudioMuted,
startWithVideoMuted
}, locationURL));
}));
await dispatch?.(setupInitialDevices());
const { tryCreateLocalTracks, errors } = createPrejoinTracks();

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import ReactionsMenuButton from './components/web/ReactionsMenuButton';
import { isReactionsButtonEnabled } from './functions';
const reactions = {
key: 'reactions',
Content: ReactionsMenuButton,
group: 2
};
/**
* A hook that returns the reactions button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useReactionsButton() {
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
if (reactionsButtonEnabled) {
return reactions;
}
}

View File

@@ -5,8 +5,8 @@ import { IReduxState } from '../../app/types';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { isRecorderTranscriptionsRunning } from '../../transcribing/functions';
import {
getActiveSession,
getSessionStatusToShow,
isLiveStreamingRunning,
isRecordingRunning,
isRemoteParticipantRecordingLocally
} from '../functions';
@@ -81,9 +81,9 @@ export function _mapStateToProps(state: IReduxState, ownProps: any) {
const { mode } = ownProps;
const isLiveStreamingLabel = mode === JitsiRecordingConstants.mode.STREAM;
const _isTranscribing = isRecorderTranscriptionsRunning(state);
const isLivestreamingRunning = Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM));
const _isLivestreamingRunning = isLiveStreamingRunning(state);
const _isVisible = isLiveStreamingLabel
? isLivestreamingRunning // this is the livestreaming label
? _isLivestreamingRunning // this is the livestreaming label
: isRecordingRunning(state) || isRemoteParticipantRecordingLocally(state)
|| _isTranscribing; // this is the recording label

View File

@@ -2,13 +2,12 @@ import { IReduxState } from '../../../app/types';
import { IconSites } from '../../../base/icons/svg';
import { MEET_FEATURES } from '../../../base/jwt/constants';
import { isJwtFeatureEnabled } from '../../../base/jwt/functions';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { maybeShowPremiumFeatureDialog } from '../../../jaas/actions';
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
import { getActiveSession, isCloudRecordingRunning } from '../../functions';
import { isCloudRecordingRunning, isLiveStreamingButtonVisible, isLiveStreamingRunning } from '../../functions';
import { getLiveStreaming } from './functions';
@@ -133,11 +132,12 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
const isModerator = isLocalParticipantModerator(state);
const liveStreaming = getLiveStreaming(state);
if (isModerator) {
visible = liveStreaming.enabled ? isJwtFeatureEnabled(state, 'livestreaming', true) : false;
} else {
visible = false;
}
visible = isLiveStreamingButtonVisible({
localParticipantIsModerator: isModerator,
liveStreamingEnabled: liveStreaming?.enabled,
liveStreamingEnabledInJwt: isJwtFeatureEnabled(state, 'livestreaming', true),
isInBreakoutRoom: isInBreakoutRoom(state)
});
}
// disable the button if the recording is running.
@@ -149,12 +149,11 @@ export function _mapStateToProps(state: IReduxState, ownProps: IProps) {
// disable the button if we are in a breakout room.
if (isInBreakoutRoom(state)) {
_disabled = true;
visible = false;
}
return {
_disabled,
_isLiveStreamRunning: Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM)),
_isLiveStreamRunning: isLiveStreamingRunning(state),
_tooltip,
visible
};

View File

@@ -11,7 +11,7 @@ import {
isLocalParticipantModerator
} from '../base/participants/functions';
import { registerSound, unregisterSound } from '../base/sounds/actions';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
import { canAddTranscriber, isRecorderTranscriptionsRunning } from '../transcribing/functions';
@@ -168,6 +168,16 @@ export function isCloudRecordingRunning(state: IReduxState) {
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
}
/**
* Returns true if there is a live streaming running.
*
* @param {IReduxState} state - The redux state to search in.
* @returns {boolean}
*/
export function isLiveStreamingRunning(state: IReduxState) {
return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM));
}
/**
* Returns true if there is a recording session running.
*
@@ -262,13 +272,13 @@ export function getRecordButtonProps(state: IReduxState) {
}
// disable the button if the livestreaming is running.
if (visible && getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
if (visible && isLiveStreamingRunning(state)) {
disabled = true;
tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
}
// disable the button if we are in a breakout room.
if (isInBreakoutRoom(state)) {
if (isInBreakoutRoomF(state)) {
disabled = true;
visible = false;
}
@@ -404,3 +414,31 @@ export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], should
RECORDING_ON_SOUND_ID,
getSoundFileSrc(RECORDING_ON_SOUND_FILE, language)));
}
/**
* Returns true if the live streaming button should be visible.
*
* @param {boolean} localParticipantIsModerator - True if the local participant is moderator.
* @param {boolean} liveStreamingEnabled - True if the live streaming is enabled.
* @param {boolean} liveStreamingEnabledInJwt - True if the lives treaming feature is enabled in JWT.
* @returns {boolean}
*/
export function isLiveStreamingButtonVisible({
localParticipantIsModerator,
liveStreamingEnabled,
liveStreamingEnabledInJwt,
isInBreakoutRoom
}: {
isInBreakoutRoom: boolean;
liveStreamingEnabled: boolean;
liveStreamingEnabledInJwt: boolean;
localParticipantIsModerator: boolean;
}) {
let visible = false;
if (localParticipantIsModerator && !isInBreakoutRoom) {
visible = liveStreamingEnabled ? liveStreamingEnabledInJwt : false;
}
return visible;
}

View File

@@ -0,0 +1,63 @@
import { useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { getLiveStreaming } from './components/LiveStream/functions';
import LiveStreamButton from './components/LiveStream/web/LiveStreamButton';
import RecordButton from './components/Recording/web/RecordButton';
import { getRecordButtonProps, isLiveStreamingButtonVisible } from './functions';
const recording = {
key: 'recording',
Content: RecordButton,
group: 2
};
const livestreaming = {
key: 'livestreaming',
Content: LiveStreamButton,
group: 2
};
/**
* A hook that returns the recording button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useRecordingButton() {
const recordingProps = useSelector(getRecordButtonProps);
const toolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
if (toolbarButtons?.includes('recording') && recordingProps.visible) {
return recording;
}
}
/**
* A hook that returns the livestreaming button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useLiveStreamingButton() {
const toolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
const localParticipantIsModerator = useSelector(isLocalParticipantModerator);
const liveStreaming = useSelector(getLiveStreaming);
const liveStreamingEnabledInJwt
= useSelector((state: IReduxState) => isJwtFeatureEnabled(state, 'livestreaming', true));
const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
if (toolbarButtons?.includes('recording')
&& isLiveStreamingButtonVisible({
localParticipantIsModerator,
liveStreamingEnabled: liveStreaming?.enabled,
liveStreamingEnabledInJwt,
isInBreakoutRoom: _isInBreakoutRoom
})) {
return livestreaming;
}
}

View File

@@ -101,7 +101,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
(recorderSession: any) => {
if (recorderSession) {
recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch);
recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
}
return;
@@ -133,7 +133,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
dispatch(updateLocalRecordingStatus(true, onlySelf));
sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(true, 'local');
APP.API.notifyRecordingStatusChanged(
true, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
}
} catch (err: any) {
logger.error('Capture failed', err);
@@ -154,7 +155,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
};
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(false, 'local', err.message);
APP.API.notifyRecordingStatusChanged(
false, 'local', err.message, isRecorderTranscriptionsRunning(getState()));
}
dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
@@ -172,7 +174,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
dispatch(playSound(RECORDING_OFF_SOUND_ID));
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(false, 'local');
APP.API.notifyRecordingStatusChanged(
false, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
}
}
break;
@@ -237,7 +240,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(true, mode);
APP.API.notifyRecordingStatusChanged(
true, mode, undefined, isRecorderTranscriptionsRunning(state));
}
}
} else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
@@ -269,7 +273,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(false, mode);
APP.API.notifyRecordingStatusChanged(
false, mode, undefined, isRecorderTranscriptionsRunning(state));
}
}
}
@@ -312,14 +317,15 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => async action =>
* in recording session.
*
* @private
* @param {Object} recorderSession - The recorder session model from the
* @param {Object} session - The recorder session model from the
* lib.
* @param {Dispatch} dispatch - The Redux Dispatch function.
* @param {Function} getState - The Redux getState function.
* @returns {void}
*/
function _showRecordingErrorNotification(recorderSession: any, dispatch: IStore['dispatch']) {
const mode = recorderSession.getMode();
const error = recorderSession.getError();
function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
const mode = session.getMode();
const error = session.getError();
const isStreamMode = mode === JitsiMeetJS.constants.recording.mode.STREAM;
switch (error) {
@@ -367,6 +373,6 @@ function _showRecordingErrorNotification(recorderSession: any, dispatch: IStore[
}
if (typeof APP !== 'undefined') {
APP.API.notifyRecordingStatusChanged(false, mode, error);
APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
}
}

View File

@@ -40,7 +40,7 @@ export const KEYS = {
ALT: 'alt',
CONTROL: 'control',
SHIFT: 'shift',
CAPS_LOCK: 'caps_lock', // not supported by robotjs
CAPS_LOCK: 'capslock',
SPACE: 'space',
PRINTSCREEN: 'printscreen',
INSERT: 'insert',

View File

@@ -7,6 +7,7 @@ import { getFeatureFlag } from '../../../base/flags/functions';
import { IconSecurityOff, IconSecurityOn } from '../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { isSecurityDialogButtonVisible } from '../../functions';
export interface IProps extends AbstractButtonProps {
@@ -71,17 +72,21 @@ export default class AbstractSecurityDialogButton<P extends IProps, S>
*/
export function _mapStateToProps(state: IReduxState) {
const { conference } = state['features/base/conference'];
const { hideLobbyButton } = getSecurityUiConfig(state);
const { locked } = state['features/base/conference'];
const { lobbyEnabled } = state['features/lobby'];
const lobbySupported = conference?.isLobbySupported();
const lobby = lobbySupported && isLocalParticipantModerator(state) && !hideLobbyButton;
const enabledFlag = getFeatureFlag(state, SECURITY_OPTIONS_ENABLED, true);
const enabledLobbyModeFlag = getFeatureFlag(state, LOBBY_MODE_ENABLED, true) && lobby;
const enabledSecurityOptionsFlag = getFeatureFlag(state, SECURITY_OPTIONS_ENABLED, true);
const enabledLobbyModeFlag = getFeatureFlag(state, LOBBY_MODE_ENABLED, true);
const enabledMeetingPassFlag = getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true);
return {
_locked: Boolean(locked || lobbyEnabled),
visible: enabledFlag && (enabledLobbyModeFlag || enabledMeetingPassFlag)
visible: isSecurityDialogButtonVisible({
conference,
securityUIConfig: getSecurityUiConfig(state),
isModerator: isLocalParticipantModerator(state),
enabledLobbyModeFlag,
enabledMeetingPassFlag,
enabledSecurityOptionsFlag
})
};
}

View File

@@ -0,0 +1,28 @@
/**
* Returns true if the security dialog button should be visible and false otherwise.
*
* @param {Object} options - The parameters needed to determine the security dialog button visibility.
* @returns {boolean}
*/
export function isSecurityDialogButtonVisible({
conference,
securityUIConfig,
isModerator,
enabledLobbyModeFlag,
enabledSecurityOptionsFlag,
enabledMeetingPassFlag
}: {
conference: any;
enabledLobbyModeFlag: boolean;
enabledMeetingPassFlag: boolean;
enabledSecurityOptionsFlag: boolean;
isModerator: boolean;
securityUIConfig: { hideLobbyButton?: boolean; };
}) {
const { hideLobbyButton } = securityUIConfig;
const lobbySupported = conference?.isLobbySupported();
const lobby = lobbySupported && isModerator && !hideLobbyButton;
return enabledSecurityOptionsFlag && ((enabledLobbyModeFlag && lobby) || enabledMeetingPassFlag);
}

View File

@@ -0,0 +1,45 @@
import { useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
import { getSecurityUiConfig } from '../base/config/functions.any';
import { LOBBY_MODE_ENABLED, MEETING_PASSWORD_ENABLED, SECURITY_OPTIONS_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import SecurityDialogButton from './components/security-dialog/web/SecurityDialogButton';
import { isSecurityDialogButtonVisible } from './functions';
const security = {
key: 'security',
alias: 'info',
Content: SecurityDialogButton,
group: 2
};
/**
* A hook that returns the security dialog button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useSecurityDialogButton() {
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const securityUIConfig = useSelector(getSecurityUiConfig);
const isModerator = useSelector(isLocalParticipantModerator);
const enabledLobbyModeFlag
= useSelector((state: IReduxState) => getFeatureFlag(state, LOBBY_MODE_ENABLED, true));
const enabledSecurityOptionsFlag
= useSelector((state: IReduxState) => getFeatureFlag(state, SECURITY_OPTIONS_ENABLED, true));
const enabledMeetingPassFlag
= useSelector((state: IReduxState) => getFeatureFlag(state, MEETING_PASSWORD_ENABLED, true));
if (isSecurityDialogButtonVisible({
conference,
securityUIConfig,
isModerator,
enabledLobbyModeFlag,
enabledSecurityOptionsFlag,
enabledMeetingPassFlag
})) {
return security;
}
}

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import SpeakerStatsButton from './components/web/SpeakerStatsButton';
import { isSpeakerStatsDisabled } from './functions';
const speakerStats = {
key: 'stats',
Content: SpeakerStatsButton,
group: 3
};
/**
* A hook that returns the speaker stats button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useSpeakerStatsButton() {
const disabled = useSelector(isSpeakerStatsDisabled);
if (!disabled) {
return speakerStats;
}
}

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import ClosedCaptionButton from './components/web/ClosedCaptionButton';
import { canStartSubtitles } from './functions.any';
const cc = {
key: 'closedcaptions',
Content: ClosedCaptionButton,
group: 2
};
/**
* A hook that returns the CC button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useClosedCaptionButton() {
const isStartSubtitlesButtonVisible = useSelector(canStartSubtitles);
if (isStartSubtitlesButtonVisible) {
return cc;
}
}

View File

@@ -0,0 +1 @@
export { default as CustomOptionButton } from './native/CustomOptionButton';

View File

@@ -0,0 +1 @@
export { default as CustomOptionButton } from './web/CustomOptionButton';

View File

@@ -20,7 +20,7 @@ import {
isButtonEnabled,
isToolboxVisible
} from '../../functions.web';
import { useKeyboardShortcuts } from '../../hooks.web';
import { useKeyboardShortcuts, useToolboxButtons } from '../../hooks.web';
import { IToolboxButton } from '../../types';
import HangupButton from '../HangupButton';
@@ -105,6 +105,7 @@ export default function Toolbox({
const toolbarVisible = useSelector(isToolboxVisible);
const mainToolbarButtonsThresholds
= useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds);
const allButtons = useToolboxButtons(customToolbarButtons);
useKeyboardShortcuts(toolbarButtonsToUse);
@@ -216,7 +217,7 @@ export default function Toolbox({
const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons({
customToolbarButtons,
allButtons,
buttonsWithNotifyClick,
toolbarButtons: toolbarButtonsToUse,
clientWidth,
@@ -225,10 +226,9 @@ export default function Toolbox({
});
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& (
(!reactionsButtonEnabled && (raiseHandInOverflowMenu || isNarrowLayout || isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions')
);
&& (
(!reactionsButtonEnabled && (raiseHandInOverflowMenu || isNarrowLayout || isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions'));
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
return (

View File

@@ -3,41 +3,9 @@ import { hasAvailableDevices } from '../base/devices/functions';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
import ChatButton from '../chat/components/web/ChatButton';
import EmbedMeetingButton from '../embed-meeting/components/EmbedMeetingButton';
import SharedDocumentButton from '../etherpad/components/SharedDocumentButton.web';
import FeedbackButton from '../feedback/components/FeedbackButton.web';
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
import KeyboardShortcutsButton from '../keyboard-shortcuts/components/web/KeyboardShortcutsButton';
import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
import ParticipantsPaneButton from '../participants-pane/components/web/ParticipantsPaneButton';
import RaiseHandContainerButton from '../reactions/components/web/RaiseHandContainerButtons';
import ReactionsMenuButton from '../reactions/components/web/ReactionsMenuButton';
import LiveStreamButton from '../recording/components/LiveStream/web/LiveStreamButton';
import RecordButton from '../recording/components/Recording/web/RecordButton';
import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
import { isScreenMediaShared } from '../screen-share/functions';
import SecurityDialogButton from '../security/components/security-dialog/web/SecurityDialogButton';
import SettingsButton from '../settings/components/web/SettingsButton';
import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
import SpeakerStatsButton from '../speaker-stats/components/web/SpeakerStatsButton';
import ClosedCaptionButton from '../subtitles/components/web/ClosedCaptionButton';
import TileViewButton from '../video-layout/components/TileViewButton';
import VideoQualityButton from '../video-quality/components/VideoQualityButton.web';
import VideoBackgroundButton from '../virtual-background/components/VideoBackgroundButton';
import WhiteboardButton from '../whiteboard/components/web/WhiteboardButton';
import { isWhiteboardVisible } from '../whiteboard/functions';
import DownloadButton from './components/DownloadButton';
import HelpButton from './components/HelpButton';
import AudioSettingsButton from './components/web/AudioSettingsButton';
import CustomOptionButton from './components/web/CustomOptionButton';
import FullscreenButton from './components/web/FullscreenButton';
import LinkToSalesforceButton from './components/web/LinkToSalesforceButton';
import ProfileButton from './components/web/ProfileButton';
import ShareDesktopButton from './components/web/ShareDesktopButton';
import ToggleCameraButton from './components/web/ToggleCameraButton';
import VideoSettingsButton from './components/web/VideoSettingsButton';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY, TOOLBAR_TIMEOUT } from './constants';
import { IMainToolbarButtonThresholds, IToolboxButton, NOTIFY_CLICK_MODE } from './types';
@@ -189,261 +157,6 @@ export function getToolbarTimeout(state: IReduxState) {
return toolbarConfig?.timeout || TOOLBAR_TIMEOUT;
}
interface ICustomToolbarButton {
backgroundColor?: string;
icon: string;
id: string;
text: string;
}
/**
* Returns all buttons that could be rendered.
*
* @param {Object} _customToolbarButtons - An array containing custom buttons objects.
* @returns {Object} The button maps mainMenuButtons and overflowMenuButtons.
*/
export function getAllToolboxButtons(
_customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxButton; } {
const microphone = {
key: 'microphone',
Content: AudioSettingsButton,
group: 0
};
const camera = {
key: 'camera',
Content: VideoSettingsButton,
group: 0
};
const profile = {
key: 'profile',
Content: ProfileButton,
group: 1
};
const chat = {
key: 'chat',
Content: ChatButton,
group: 2
};
const desktop = {
key: 'desktop',
Content: ShareDesktopButton,
group: 2
};
// In Narrow layout and mobile web we are using drawer for popups and that is why it is better to include
// all forms of reactions in the overflow menu. Otherwise the toolbox will be hidden and the reactions popup
// misaligned.
const raisehand = {
key: 'raisehand',
Content: RaiseHandContainerButton,
group: 2
};
const reactions = {
key: 'reactions',
Content: ReactionsMenuButton,
group: 2
};
const participants = {
key: 'participants-pane',
Content: ParticipantsPaneButton,
group: 2
};
const invite = {
key: 'invite',
Content: InviteButton,
group: 2
};
const tileview = {
key: 'tileview',
Content: TileViewButton,
group: 2
};
const toggleCamera = {
key: 'toggle-camera',
Content: ToggleCameraButton,
group: 2
};
const videoquality = {
key: 'videoquality',
Content: VideoQualityButton,
group: 2
};
const fullscreen = {
key: 'fullscreen',
Content: FullscreenButton,
group: 2
};
const security = {
key: 'security',
Content: SecurityDialogButton,
group: 2
};
const closedcaptions = {
key: 'closedcaptions',
Content: ClosedCaptionButton,
group: 2
};
const recording = {
key: 'recording',
Content: RecordButton,
group: 2
};
const livestreaming = {
key: 'livestreaming',
Content: LiveStreamButton,
group: 2
};
const linktosalesforce = {
key: 'linktosalesforce',
Content: LinkToSalesforceButton,
group: 2
};
const sharedvideo = {
key: 'sharedvideo',
Content: SharedVideoButton,
group: 3
};
const shareaudio = {
key: 'shareaudio',
Content: ShareAudioButton,
group: 3
};
const noisesuppression = {
key: 'noisesuppression',
Content: NoiseSuppressionButton,
group: 3
};
const whiteboard = {
key: 'whiteboard',
Content: WhiteboardButton,
group: 3
};
const etherpad = {
key: 'etherpad',
Content: SharedDocumentButton,
group: 3
};
const virtualBackground = {
key: 'select-background',
Content: VideoBackgroundButton,
group: 3
};
const stats = {
key: 'stats',
Content: SpeakerStatsButton,
group: 3
};
const settings = {
key: 'settings',
Content: SettingsButton,
group: 4
};
const shortcuts = {
key: 'shortcuts',
Content: KeyboardShortcutsButton,
group: 4
};
const embedmeeting = {
key: 'embedmeeting',
Content: EmbedMeetingButton,
group: 4
};
const feedback = {
key: 'feedback',
Content: FeedbackButton,
group: 4
};
const download = {
key: 'download',
Content: DownloadButton,
group: 4
};
const help = {
key: 'help',
Content: HelpButton,
group: 4
};
const customButtons = _customToolbarButtons?.reduce((prev, { backgroundColor, icon, id, text }) => {
return {
...prev,
[id]: {
backgroundColor,
key: id,
Content: CustomOptionButton,
group: 4,
icon,
text
}
};
}, {});
return {
microphone,
camera,
profile,
desktop,
chat,
raisehand,
reactions,
'participants-pane': participants,
invite,
tileview,
'toggle-camera': toggleCamera,
videoquality,
fullscreen,
security,
closedcaptions,
recording,
livestreaming,
linktosalesforce,
sharedvideo,
shareaudio,
noisesuppression,
whiteboard,
etherpad,
'select-background': virtualBackground,
stats,
settings,
shortcuts,
embedmeeting,
feedback,
download,
help,
...customButtons
};
}
/**
* Sets the notify click mode for the buttons.
*
@@ -464,9 +177,9 @@ function setButtonsNotifyClickMode(buttons: Object, buttonsWithNotifyClick: Map<
}
interface IGetVisibleButtonsParams {
allButtons: { [key: string]: IToolboxButton; };
buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
clientWidth: number;
customToolbarButtons?: ICustomToolbarButton[];
jwtDisabledButtons: string[];
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
toolbarButtons: string[];
@@ -479,22 +192,22 @@ interface IGetVisibleButtonsParams {
* @returns {Object} - The visible buttons arrays .
*/
export function getVisibleButtons({
customToolbarButtons,
allButtons,
buttonsWithNotifyClick,
toolbarButtons,
clientWidth,
jwtDisabledButtons,
mainToolbarButtonsThresholds
}: IGetVisibleButtonsParams) {
const buttons = getAllToolboxButtons(customToolbarButtons);
setButtonsNotifyClickMode(allButtons, buttonsWithNotifyClick);
const filteredButtons = Object.keys(buttons).filter(key =>
const filteredButtons = Object.keys(allButtons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be comming from config.mainToolbarButtons
// override
&& !jwtDisabledButtons.includes(key)
&& isButtonEnabled(key, toolbarButtons));
setButtonsNotifyClickMode(buttons, buttonsWithNotifyClick);
const { order } = mainToolbarButtonsThresholds.find(({ width }) => clientWidth > width)
|| mainToolbarButtonsThresholds[mainToolbarButtonsThresholds.length - 1];
@@ -507,7 +220,7 @@ export function getVisibleButtons({
const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length);
const overflowMenuButtons = filteredButtons.reduce((acc, key) => {
if (!mainButtonsKeys.includes(key)) {
acc.push(buttons[key]);
acc.push(allButtons[key]);
}
return acc;
@@ -522,7 +235,7 @@ export function getVisibleButtons({
}
return {
mainMenuButtons: mainButtonsKeys.map(key => buttons[key]),
mainMenuButtons: mainButtonsKeys.map(key => allButtons[key]),
overflowMenuButtons
};
}

View File

@@ -5,13 +5,24 @@ import { ACTION_SHORTCUT_TRIGGERED, createShortcutEvent } from '../analytics/Ana
import { sendAnalytics } from '../analytics/functions';
import { IReduxState } from '../app/types';
import { toggleDialog } from '../base/dialog/actions';
import { isIosMobileBrowser, isIpadMobileBrowser } from '../base/environment/utils';
import { HELP_BUTTON_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { raiseHand } from '../base/participants/actions';
import { getLocalParticipant, hasRaisedHand } from '../base/participants/functions';
import { isToggleCameraEnabled } from '../base/tracks/functions.web';
import { toggleChat } from '../chat/actions.web';
import ChatButton from '../chat/components/web/ChatButton';
import { useEmbedButton } from '../embed-meeting/hooks';
import { useEtherpadButton } from '../etherpad/hooks';
import { useFeedbackButton } from '../feedback/hooks.web';
import { setGifMenuVisibility } from '../gifs/actions';
import { isGifEnabled } from '../gifs/function.any';
import InviteButton from '../invite/components/add-people-dialog/web/InviteButton';
import { registerShortcut, unregisterShortcut } from '../keyboard-shortcuts/actions.any';
import { useKeyboardShortcutsButton } from '../keyboard-shortcuts/hooks.web';
import NoiseSuppressionButton from '../noise-suppression/components/NoiseSuppressionButton';
import {
close as closeParticipantsPane,
open as openParticipantsPane
@@ -20,20 +31,331 @@ import {
getParticipantsPaneOpen,
isParticipantsPaneEnabled
} from '../participants-pane/functions';
import { useParticipantPaneButton } from '../participants-pane/hooks.web';
import { addReactionToBuffer } from '../reactions/actions.any';
import { toggleReactionsMenuVisibility } from '../reactions/actions.web';
import RaiseHandContainerButton from '../reactions/components/web/RaiseHandContainerButtons';
import { REACTIONS } from '../reactions/constants';
import { shouldDisplayReactionsButtons } from '../reactions/functions.any';
import { useReactionsButton } from '../reactions/hooks';
import { useLiveStreamingButton, useRecordingButton } from '../recording/hooks.web';
import { isSalesforceEnabled } from '../salesforce/functions';
import { startScreenShareFlow } from '../screen-share/actions.web';
import { isScreenVideoShared } from '../screen-share/functions';
import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
import { useSecurityDialogButton } from '../security/hooks';
import SettingsButton from '../settings/components/web/SettingsButton';
import SharedVideoButton from '../shared-video/components/web/SharedVideoButton';
import SpeakerStats from '../speaker-stats/components/web/SpeakerStats';
import { isSpeakerStatsDisabled } from '../speaker-stats/functions';
import { useSpeakerStatsButton } from '../speaker-stats/hooks.web';
import { useClosedCaptionButton } from '../subtitles/hooks.web';
import { toggleTileView } from '../video-layout/actions.any';
import { shouldDisplayTileView } from '../video-layout/functions.any';
import { useTileViewButton } from '../video-layout/hooks';
import VideoQualityButton from '../video-quality/components/VideoQualityButton.web';
import VideoQualityDialog from '../video-quality/components/VideoQualityDialog.web';
import { useVirtualBackgroundButton } from '../virtual-background/hooks';
import { useWhiteboardButton } from '../whiteboard/hooks';
import { setFullScreen } from './actions.web';
import DownloadButton from './components/DownloadButton';
import HelpButton from './components/HelpButton';
import AudioSettingsButton from './components/web/AudioSettingsButton';
import CustomOptionButton from './components/web/CustomOptionButton';
import FullscreenButton from './components/web/FullscreenButton';
import LinkToSalesforceButton from './components/web/LinkToSalesforceButton';
import ProfileButton from './components/web/ProfileButton';
import ShareDesktopButton from './components/web/ShareDesktopButton';
import ToggleCameraButton from './components/web/ToggleCameraButton';
import VideoSettingsButton from './components/web/VideoSettingsButton';
import { isButtonEnabled, isDesktopShareButtonDisabled } from './functions.web';
import { ICustomToolbarButton, IToolboxButton, ToolbarButton } from './types';
const microphone = {
key: 'microphone',
Content: AudioSettingsButton,
group: 0
};
const camera = {
key: 'camera',
Content: VideoSettingsButton,
group: 0
};
const profile = {
key: 'profile',
Content: ProfileButton,
group: 1
};
const chat = {
key: 'chat',
Content: ChatButton,
group: 2
};
const desktop = {
key: 'desktop',
Content: ShareDesktopButton,
group: 2
};
// In Narrow layout and mobile web we are using drawer for popups and that is why it is better to include
// all forms of reactions in the overflow menu. Otherwise the toolbox will be hidden and the reactions popup
// misaligned.
const raisehand = {
key: 'raisehand',
Content: RaiseHandContainerButton,
group: 2
};
const invite = {
key: 'invite',
Content: InviteButton,
group: 2
};
const toggleCamera = {
key: 'toggle-camera',
Content: ToggleCameraButton,
group: 2
};
const videoQuality = {
key: 'videoquality',
Content: VideoQualityButton,
group: 2
};
const fullscreen = {
key: 'fullscreen',
Content: FullscreenButton,
group: 2
};
const linkToSalesforce = {
key: 'linktosalesforce',
Content: LinkToSalesforceButton,
group: 2
};
const shareVideo = {
key: 'sharedvideo',
Content: SharedVideoButton,
group: 3
};
const shareAudio = {
key: 'shareaudio',
Content: ShareAudioButton,
group: 3
};
const noiseSuppression = {
key: 'noisesuppression',
Content: NoiseSuppressionButton,
group: 3
};
const settings = {
key: 'settings',
Content: SettingsButton,
group: 4
};
const download = {
key: 'download',
Content: DownloadButton,
group: 4
};
const help = {
key: 'help',
Content: HelpButton,
group: 4
};
/**
* A hook that returns the toggle camera button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useToggleCameraButton() {
const toggleCameraEnabled = useSelector(isToggleCameraEnabled);
if (toggleCameraEnabled) {
return toggleCamera;
}
}
/**
* A hook that returns the desktop sharing button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function getDesktopSharingButton() {
if (JitsiMeetJS.isDesktopSharingEnabled()) {
return desktop;
}
}
/**
* A hook that returns the fullscreen button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function getFullscreenButton() {
if (!isIosMobileBrowser() || isIpadMobileBrowser()) {
return fullscreen;
}
}
/**
* A hook that returns the "link to salesforce" button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useLinkToSalesforceButton() {
const _isSalesforceEnabled = useSelector(isSalesforceEnabled);
if (_isSalesforceEnabled) {
return linkToSalesforce;
}
}
/**
* A hook that returns the share audio button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function getShareAudioButton() {
if (JitsiMeetJS.isDesktopSharingEnabled() && isScreenAudioSupported()) {
return shareAudio;
}
}
/**
* A hook that returns the download button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useDownloadButton() {
const visible = useSelector(
(state: IReduxState) => typeof state['features/base/config'].deploymentUrls?.downloadAppsUrl === 'string');
if (visible) {
return download;
}
}
/**
* A hook that returns the help button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useHelpButton() {
const visible = useSelector(
(state: IReduxState) =>
typeof state['features/base/config'].deploymentUrls?.userDocumentationURL === 'string'
&& getFeatureFlag(state, HELP_BUTTON_ENABLED, true));
if (visible) {
return help;
}
}
/**
* Returns all buttons that could be rendered.
*
* @param {Object} _customToolbarButtons - An array containing custom buttons objects.
* @returns {Object} The button maps mainMenuButtons and overflowMenuButtons.
*/
export function useToolboxButtons(
_customToolbarButtons?: ICustomToolbarButton[]): { [key: string]: IToolboxButton; } {
const dekstopSharing = getDesktopSharingButton();
const toggleCameraButton = useToggleCameraButton();
const _fullscreen = getFullscreenButton();
const security = useSecurityDialogButton();
const reactions = useReactionsButton();
const participants = useParticipantPaneButton();
const tileview = useTileViewButton();
const cc = useClosedCaptionButton();
const recording = useRecordingButton();
const liveStreaming = useLiveStreamingButton();
const linktosalesforce = useLinkToSalesforceButton();
const shareaudio = getShareAudioButton();
const whiteboard = useWhiteboardButton();
const etherpad = useEtherpadButton();
const virtualBackground = useVirtualBackgroundButton();
const speakerStats = useSpeakerStatsButton();
const shortcuts = useKeyboardShortcutsButton();
const embed = useEmbedButton();
const feedback = useFeedbackButton();
const _download = useDownloadButton();
const _help = useHelpButton();
const buttons: { [key in ToolbarButton]?: IToolboxButton; } = {
microphone,
camera,
profile,
desktop: dekstopSharing,
chat,
raisehand,
reactions,
'participants-pane': participants,
invite,
tileview,
'toggle-camera': toggleCameraButton,
videoquality: videoQuality,
fullscreen: _fullscreen,
security,
closedcaptions: cc,
recording,
livestreaming: liveStreaming,
linktosalesforce,
sharedvideo: shareVideo,
shareaudio,
noisesuppression: noiseSuppression,
whiteboard,
etherpad,
'select-background': virtualBackground,
stats: speakerStats,
settings,
shortcuts,
embedmeeting: embed,
feedback,
download: _download,
help: _help
};
const buttonKeys = Object.keys(buttons) as ToolbarButton[];
buttonKeys.forEach(
key => typeof buttons[key] === 'undefined' && delete buttons[key]);
const customButtons = _customToolbarButtons?.reduce((prev, { backgroundColor, icon, id, text }) => {
prev[id] = {
backgroundColor,
key: id,
id,
Content: CustomOptionButton,
group: 4,
icon,
text
};
return prev;
}, {} as { [key: string]: ICustomToolbarButton; });
return {
...buttons,
...customButtons
};
}
export const useKeyboardShortcuts = (toolbarButtons: Array<string>) => {
const dispatch = useDispatch();

View File

@@ -1,5 +1,7 @@
import { ComponentType } from 'react';
import { CustomOptionButton } from './components';
export interface IToolboxButton {
Content: ComponentType<any>;
group: number;
@@ -53,3 +55,13 @@ export type IMainToolbarButtonThresholds = Array<{
order: Array<ToolbarButton | string>;
width: number;
}>;
export interface ICustomToolbarButton {
Content?: typeof CustomOptionButton;
backgroundColor?: string;
group?: number;
icon: string;
id: string;
key?: string;
text: string;
}

View File

@@ -7,6 +7,7 @@ import { playSound } from '../base/sounds/actions';
import { showNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../recording/constants';
import { isLiveStreamingRunning, isRecordingRunning } from '../recording/functions';
import { isRecorderTranscriptionsRunning } from './functions';
@@ -17,10 +18,10 @@ StateListenerRegistry.register(
/* selector */ isRecorderTranscriptionsRunning,
/* listener */ (isRecorderTranscriptionsRunningValue, { getState, dispatch }) => {
if (isRecorderTranscriptionsRunningValue) {
notifyTranscribingStatusChanged(true);
notifyTranscribingStatusChanged(getState, true);
maybeEmitRecordingNotification(dispatch, getState, true);
} else {
notifyTranscribingStatusChanged(false);
notifyTranscribingStatusChanged(getState, false);
maybeEmitRecordingNotification(dispatch, getState, false);
}
}
@@ -58,11 +59,18 @@ function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState:
/**
* Notify external application (if API is enabled) that transcribing has started or stopped.
*
* @param {Function} getState - The Redux state.
* @param {boolean} on - True if transcribing is on, false otherwise.
* @returns {void}
*/
function notifyTranscribingStatusChanged(on: boolean) {
function notifyTranscribingStatusChanged(getState: IStore['getState'], on: boolean) {
if (typeof APP !== 'undefined') {
const state = getState();
const isRecording = isRecordingRunning(state);
const isStreaming = isLiveStreamingRunning(state);
const mode = isRecording ? JitsiRecordingConstants.mode.FILE : JitsiRecordingConstants.mode.STREAM;
APP.API.notifyRecordingStatusChanged(isRecording || isStreaming, mode, undefined, on);
APP.API.notifyTranscribingStatusChanged(on);
}
}

View File

@@ -0,0 +1,26 @@
import { useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
import { TILE_VIEW_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import TileViewButton from './components/TileViewButton';
const tileview = {
key: 'tileview',
Content: TileViewButton,
group: 2
};
/**
* A hook that returns the tile view button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useTileViewButton() {
const tileViewEnabled = useSelector((state: IReduxState) => getFeatureFlag(state, TILE_VIEW_ENABLED, true));
if (tileViewEnabled) {
return tileview;
}
}

View File

@@ -1,11 +1,9 @@
import { connect } from 'react-redux';
import { openSheet } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import { IconInfoCircle } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import ConnectionStatusComponent from './ConnectionStatusComponent';
import { showConnectionStatus } from '../../../participants-pane/actions.native';
export interface IProps extends AbstractButtonProps {
@@ -31,9 +29,7 @@ class ConnectionStatusButton extends AbstractButton<IProps> {
_handleClick() {
const { dispatch, participantID } = this.props;
dispatch(openSheet(ConnectionStatusComponent, {
participantID
}));
dispatch(showConnectionStatus(participantID));
}
}

View File

@@ -83,13 +83,18 @@ class LocalVideoMenu extends PureComponent<IProps> {
styles: bottomSheetStyles.buttons
};
const connectionStatusButtonProps = {
...buttonProps,
afterClick: undefined
};
return (
<BottomSheet
renderHeader = { this._renderMenuHeader }
showSlidingView = { true }>
<ToggleSelfViewButton { ...buttonProps } />
{ _showDemote && <DemoteToVisitorButton { ...buttonProps } /> }
<ConnectionStatusButton { ...buttonProps } />
<ConnectionStatusButton { ...connectionStatusButtonProps } />
</BottomSheet>
);
}

View File

@@ -5,6 +5,18 @@
*/
export const DEFAULT_LAST_N = 20;
/**
* The supported video codecs.
*
* @type {enum}
*/
export enum VIDEO_CODEC {
AV1 = 'av1',
H264 = 'h264',
VP8 = 'vp8',
VP9 = 'vp9'
}
/**
* The supported remote video resolutions. The values are currently based on
* available simulcast layers.

View File

@@ -0,0 +1,27 @@
import { useSelector } from 'react-redux';
import { isScreenVideoShared } from '../screen-share/functions';
import VideoBackgroundButton from './components/VideoBackgroundButton';
import { checkBlurSupport, checkVirtualBackgroundEnabled } from './functions';
const virtualBackground = {
key: 'select-background',
Content: VideoBackgroundButton,
group: 3
};
/**
* A hook that returns the virtual background button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useVirtualBackgroundButton() {
const _checkBlurSupport = checkBlurSupport();
const _isScreenVideoShared = useSelector(isScreenVideoShared);
const _checkVirtualBackgroundEnabled = useSelector(checkVirtualBackgroundEnabled);
if (_checkBlurSupport && !_isScreenVideoShared && _checkVirtualBackgroundEnabled) {
return virtualBackground;
}
}

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import WhiteboardButton from './components/web/WhiteboardButton';
import { isWhiteboardButtonVisible } from './functions';
const whiteboard = {
key: 'whiteboard',
Content: WhiteboardButton,
group: 3
};
/**
* A hook that returns the whiteboard button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
export function useWhiteboardButton() {
const _isWhiteboardButtonVisible = useSelector(isWhiteboardButtonVisible);
if (_isWhiteboardButtonVisible) {
return whiteboard;
}
}

View File

@@ -226,7 +226,7 @@ function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences)
if body.exp and os.time() >= body.exp then
return nil, "Not acceptable by exp"
return nil, "Not acceptable by exp ("..tostring(os.time()-body.exp)..")"
end
if body.nbf and os.time() < body.nbf then

View File

@@ -101,7 +101,8 @@ function provider.get_sasl_handler(session)
local res, error, reason = token_util:process_and_verify_token(session);
if res == false then
module:log("warn",
"Error verifying token err:%s, reason:%s", error, reason);
"Error verifying token err:%s, reason:%s tenant:%s room:%s",
error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room);
session.auth_token = nil;
measure_verify_fail(1);
return res, error, reason;

View File

@@ -25,7 +25,7 @@ function occupant_joined(event)
if participant_count > 1 then
if room.created_timestamp == nil then
room.created_timestamp = os.time() * 1000; -- Lua provides UTC time in seconds, so convert to milliseconds
room.created_timestamp = string.format('%i', os.time() * 1000); -- Lua provides UTC time in seconds, so convert to milliseconds
end
end
end

View File

@@ -16,6 +16,7 @@ local new_id = require 'util.id'.medium;
local filters = require 'util.filters';
local util = module:require 'util';
local ends_with = util.ends_with;
local is_vpaas = util.is_vpaas;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local get_room_from_jid = util.get_room_from_jid;
@@ -246,9 +247,12 @@ module:hook('muc-broadcast-presence', function (event)
if identity and identity.id then
user_id = session.jitsi_meet_context_user.id;
-- non-vpass and having a token in correct tenant is considered a moderator
if session.jitsi_meet_str_tenant
and session.jitsi_web_query_prefix == string.lower(session.jitsi_meet_str_tenant) then
if room._data.moderator_id then
if room._data.moderator_id == user_id then
is_moderator = true;
end
elseif session.auth_token then
-- non-vpass and having a token is considered a moderator
is_moderator = true;
end
end
@@ -359,8 +363,19 @@ function process_host_module(name, callback)
process_host(name);
end
end
-- if the message received ends with the main domain, these are system messages
-- for visitors, let's correct the room name there
local function message_handler(event)
local origin, stanza = event.origin, event.stanza;
if ends_with(stanza.attr.from, main_domain) then
stanza.attr.from = stanza.attr.from:sub(1, -(main_domain:len() + 1))..local_domain;
end
end
process_host_module(local_domain, function(host_module, host)
host_module:hook('iq/host', stanza_handler, 10);
host_module:hook('message/full', message_handler);
end);
-- only live chat is supported for visitors
@@ -528,6 +543,7 @@ local function iq_from_main_handler(event)
-- if this is update it will either set or remove the password
room:set_password(node.attr.password);
room._data.meetingId = node.attr.meetingId;
room._data.moderator_id = node.attr.moderatorId;
local createdTimestamp = node.attr.createdTimestamp;
room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil;

View File

@@ -26,5 +26,5 @@ function init_session(event)
end
end
module:hook_global("bosh-session", init_session);
module:hook_global("websocket-session", init_session);
module:hook_global("bosh-session", init_session, 1);
module:hook_global("websocket-session", init_session, 1);

View File

@@ -86,7 +86,9 @@ function room_created(event)
return ;
end
room.jitsiMetadata = {};
if not room.jitsiMetadata then
room.jitsiMetadata = {};
end
end
function on_message(event)
@@ -172,6 +174,10 @@ function process_main_muc_loaded(main_muc, host_module)
table.insert(event.form, getFormData(room));
end);
-- The room metadata was updated internally (from another module).
host_module:hook("room-metadata-changed", function(event)
broadcastMetadata(event.room);
end);
end
-- process or waits to process the main muc component

View File

@@ -68,6 +68,7 @@ local function send_visitors_iq(conference_service, room, type)
password = type ~= 'disconnect' and room:get_password() or '',
lobby = room._data.lobbyroom and 'true' or 'false',
meetingId = room._data.meetingId,
moderatorId = room._data.moderator_id, -- can be used from external modules to set single moderator for meetings
createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil
}):up();

View File

@@ -78,7 +78,6 @@ local function request_promotion_received(room, from_jid, from_vnode, nick, time
-- only for raise hand, ignore lowering the hand
if time and time > 0 and (
auto_allow_promotion
or (user_id and user_id == room._data.moderator_id)
or force_promote == 'true') then
-- we are in auto-allow mode, let's reply with accept
-- we store where the request is coming from so we can send back the response
@@ -231,17 +230,6 @@ local function stanza_handler(event)
return true; -- stop processing
end
local force_promote = request_promotion.attr.forcePromote;
if force_promote == 'true' and not is_vpaas(room) then
-- allow force promote only in case there are no moderators in the room
for _, occupant in room:each_occupant() do
if occupant.role == 'moderator' and not is_admin(occupant.bare_jid) then
force_promote = false;
break;
end
end
end
local display_name = visitors_iq:get_child_text('nick', 'http://jabber.org/protocol/nick');
processed = request_promotion_received(
room,
@@ -250,7 +238,7 @@ local function stanza_handler(event)
display_name,
tonumber(request_promotion.attr.time),
request_promotion.attr.userId,
force_promote
request_promotion.attr.forcePromote
);
end