Compare commits

...

51 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
Hristo Terezov
3b4554f4cf fix(participant-features): Add missing properties in the type. 2024-06-03 17:07:52 -05:00
Hristo Terezov
d5dd5e4560 fix(Toolbox): Stop rerendering on every action
getJwtDisabledButtons which is used as a selector will create a new array every time it is called. This will lead to unneccessary rerenders on every action where there is no difference in the result of getJwtDisabledButtons because the reference of the array will be different.
2024-06-03 14:07:22 -05:00
Hristo Terezov
46ea1f577c ref(Toolbox): replace mapStateToProps with hooks. 2024-06-03 14:07:22 -05:00
Calinteodor
be06547cc4 feat(notifications/native): ui fixes (#14786)
* feat(notifications/native): ui fixes
2024-05-30 11:38:47 +03:00
Mihaela Dumitru
bd631f5968 fix(subtitles) show subtitles even without reactions enabled (#14784) 2024-05-29 14:55:47 +03:00
Calin-Teodor
bc69a35cd6 feat(react-native-sdk): match any version between react native and metro config 2024-05-28 19:39:40 +03:00
Calinteodor
75e7f64311 feat(polls/web/native): fixed identical answers verification (#14782)
* feat(polls/web/native): fixed identical answers verification
2024-05-28 14:06:11 +03:00
Nitin Kushwaha
cdcb4cdbfb fix(polls): add vertical margin to create poll button (android) 2024-05-27 18:31:40 +03:00
Calinteodor
c31ef2ebc7 feat(polls/web/native): refactoring (#14778)
* feat(polls/web/native): refactoring
2024-05-27 12:14:17 +03:00
zobadaniel
d49a419284 feat(lang) add upper sorbian to transcription languages
* fix showing transcripts for country codes longer than 2 characters
2024-05-24 15:18:38 +02:00
Hristo Terezov
0913554af9 feat(MainToolbar): implement custom order.
As part of the PR, it also fixes:
 - Removes button aliases
 - Unifies the keys in the object returned by getAllToolboxButtons and the button keys
 - Makes sure that the number of buttons displayed are always the same as the number of buttons specified in the thresholds and removes the exception for not filling up the main toolbar with buttons from overflow menu when reactions button is disabled.
 - Introduces a priority for buttons that will be used to fill empty spaces in the main toolbar.
2024-05-23 12:34:15 -05:00
Aaron van Meerten
9af0003c63 feat(invite): move JWT to Authorization header for invite and search endpoints (#14777) 2024-05-23 11:07:33 -05:00
Calinteodor
3ecc16dc87 feat(polls): rework (#14645)
* feat(polls/web/native): rework polls to manage editing and saving before sending to other participants
2024-05-22 15:03:24 +03:00
Jaya Allamsetty
ef387c939a chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1823.0.0+ec98b020...v1830.0.0+5a14bd43
2024-05-21 13:27:01 -04:00
122 changed files with 3950 additions and 1540 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,
@@ -848,6 +845,22 @@ var config = {
// autoHideWhileChatIsOpen: false,
// },
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
// buttons varies from 2 buttons to 8 buttons. Every array in the mainToolbarButtons array will replace the
// corresponding default buttons configuration matched by the number of buttons specified in the array. Arrays with
// more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a cerain
// configuration (for example when 3 buttons are displayed) the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.
// mainToolbarButtons: [
// [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ],
// [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ],
// [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ],
// [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ],
// [ 'microphone', 'camera', 'chat', 'participants-pane' ],
// [ 'microphone', 'camera', 'chat' ],
// [ 'microphone', 'camera' ]
// ],
// Toolbar buttons which have their click/tap event exposed through the API on
// `toolbarButtonClicked`. Passing a string for the button key will
// prevent execution of the click/tap routine; passing an object with `key` and
@@ -1754,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

@@ -865,6 +865,8 @@
"pinnedParticipant": "The participant is pinned",
"polls": {
"answer": {
"edit": "Edit",
"send": "Send",
"skip": "Skip",
"submit": "Submit"
},
@@ -878,6 +880,7 @@
"pollQuestion": "Poll Question",
"questionPlaceholder": "Ask a question",
"removeOption": "Remove option",
"save": "Save",
"send": "Send"
},
"errors": {

View File

@@ -34,6 +34,7 @@
"hi": "Hindi",
"hmn": "Hmong",
"hr": "Croatian",
"hsb": "Upper Sorbian",
"ht": "Haitian Creole",
"hu": "Hungarian",
"hy": "Armenian",

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/v1823.0.0+ec98b020/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

@@ -55,7 +55,7 @@
"@amplitude/react-native": "0.0.0",
"@braintree/sanitize-url": "0.0.0",
"@giphy/react-native-sdk": "0.0.0",
"@react-native/metro-config": "0.0.0",
"@react-native/metro-config": "*",
"@react-native-async-storage/async-storage": "0.0.0",
"@react-native-community/clipboard": "0.0.0",
"@react-native-community/netinfo": "0.0.0",

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

@@ -441,6 +441,7 @@ export interface IConfig {
};
localSubject?: string;
locationURL?: URL;
mainToolbarButtons?: Array<Array<string>>;
maxFullResolutionParticipants?: number;
microsoftApiApplicationClientID?: string;
moderatedRoomServiceUrl?: string;
@@ -543,7 +544,6 @@ export interface IConfig {
assumeBandwidth?: boolean;
disableE2EE?: boolean;
dumpTranscript?: boolean;
mobileXmppWsThreshold?: number;
noAutoPlayVideo?: boolean;
p2pTestMode?: boolean;
skipInterimTranscriptions?: boolean;

View File

@@ -185,6 +185,7 @@ export default [
'localRecording',
'localSubject',
'logging',
'mainToolbarButtons',
'maxFullResolutionParticipants',
'mouseMoveCallbackInterval',
'notifications',

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

@@ -3,6 +3,7 @@ import jwtDecode from 'jwt-decode';
import { IReduxState } from '../../app/types';
import { getLocalParticipant } from '../participants/functions';
import { IParticipantFeatures } from '../participants/types';
import { parseURLParams } from '../util/parseURLParams';
import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
@@ -49,13 +50,46 @@ export function getJwtName(state: IReduxState) {
*/
export function isJwtFeatureEnabled(state: IReduxState, feature: string, ifNoToken = false, ifNotInFeatures = false) {
const { jwt } = state['features/base/jwt'];
const { features } = getLocalParticipant(state) || {};
return isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures: features,
feature,
ifNoToken,
ifNotInFeatures
});
}
interface IIsJwtFeatureEnabledStatelessParams {
feature: string;
ifNoToken?: boolean;
ifNotInFeatures?: boolean;
jwt?: string;
localParticipantFeatures?: IParticipantFeatures;
}
/**
* Check if the given JWT feature is enabled.
*
* @param {string | undefined} jwt - The jwt token.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @param {string} feature - The feature we want to check.
* @param {boolean} ifNoToken - Default value if there is no token.
* @param {boolean} ifNotInFeatures - Default value if features prop exists but does not have the {@code feature}.
* @returns {bolean}
*/
export function isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures: features,
feature,
ifNoToken = false,
ifNotInFeatures = false
}: IIsJwtFeatureEnabledStatelessParams) {
if (!jwt) {
return ifNoToken;
}
const { features } = getLocalParticipant(state) || {};
// If `features` is undefined, act as if everything is enabled.
if (typeof features === 'undefined') {
return true;

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

@@ -19,9 +19,7 @@ export interface IParticipant {
e2eeVerified?: boolean;
email?: string;
fakeParticipant?: FakeParticipant;
features?: {
'screen-sharing'?: boolean | string;
};
features?: IParticipantFeatures;
getId?: Function;
id: string;
isJigasi?: boolean;
@@ -54,6 +52,23 @@ export interface ILocalParticipant extends IParticipant {
userSelectedMicDeviceLabel?: string;
}
export interface IParticipantFeatures {
'branding'?: boolean | string;
'calendar'?: boolean | string;
'flip'?: boolean | string;
'inbound-call'?: boolean | string;
'livestreaming'?: boolean | string;
'lobby'?: boolean | string;
'moderation'?: boolean | string;
'outbound-call'?: boolean | string;
'recording'?: boolean | string;
'room'?: boolean | string;
'screen-sharing'?: boolean | string;
'sip-inbound-call'?: boolean | string;
'sip-outbound-call'?: boolean | string;
'transcription'?: boolean | string;
}
export interface ISourceInfo {
muted: boolean;
videoType: string;

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

@@ -1,13 +1,16 @@
import React, { forwardRef, useCallback, useState } from 'react';
import {
KeyboardTypeOptions,
NativeSyntheticEvent, ReturnKeyTypeOptions,
NativeSyntheticEvent,
ReturnKeyTypeOptions,
StyleProp,
Text,
TextInput,
TextInputChangeEventData,
TextInputFocusEventData, TextInputKeyPressEventData,
TextInputFocusEventData,
TextInputKeyPressEventData,
TextInputSubmitEditingEventData,
TextStyle,
TouchableOpacity,
View,
ViewStyle
@@ -25,8 +28,16 @@ interface IProps extends IInputProps {
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters' | undefined;
autoFocus?: boolean;
blurOnSubmit?: boolean | undefined;
bottomLabel?: string;
customStyles?: ICustomStyles;
editable?: boolean | undefined;
/**
* The id to set on the input element.
* This is required because we need it internally to tie the input to its
* info (label, error) so that screen reader users don't get lost.
*/
id?: string;
keyboardType?: KeyboardTypeOptions;
maxLength?: number | undefined;
minHeight?: number | string | undefined;
@@ -52,11 +63,13 @@ const Input = forwardRef<TextInput, IProps>(({
autoCapitalize,
autoFocus,
blurOnSubmit,
bottomLabel,
clearable,
customStyles,
disabled,
error,
icon,
id,
keyboardType,
label,
maxLength,
@@ -106,7 +119,7 @@ const Input = forwardRef<TextInput, IProps>(({
onSubmitEditing?.(text);
}, [ onSubmitEditing ]);
return (<View style = { [ styles.inputContainer, customStyles?.container ] }>
return (<View style = { [ styles.inputContainer, customStyles?.container ] as StyleProp<ViewStyle> }>
{label && <Text style = { styles.label }>{ label }</Text>}
<View style = { styles.fieldContainer as StyleProp<ViewStyle> }>
{icon && <Icon
@@ -121,6 +134,7 @@ const Input = forwardRef<TextInput, IProps>(({
autoFocus = { autoFocus }
blurOnSubmit = { blurOnSubmit }
editable = { !disabled }
id = { id }
keyboardType = { keyboardType }
maxLength = { maxLength }
@@ -145,11 +159,11 @@ const Input = forwardRef<TextInput, IProps>(({
clearable && styles.clearableInput,
customStyles?.input,
disabled && styles.inputDisabled,
error && styles.inputError,
focused && styles.inputFocused,
icon && styles.iconInput,
multiline && styles.inputMultiline
] }
multiline && styles.inputMultiline,
focused && styles.inputFocused,
error && styles.inputError
] as StyleProp<TextStyle> }
textContentType = { textContentType }
value = { typeof value === 'number' ? `${value}` : value } />
{ clearable && !disabled && value !== '' && (
@@ -163,6 +177,20 @@ const Input = forwardRef<TextInput, IProps>(({
</TouchableOpacity>
)}
</View>
{
bottomLabel && (
<View>
<Text
id = { `${id}-description` }
style = { [
styles.bottomLabel,
error && styles.bottomLabelError
] }>
{ bottomLabel }
</Text>
</View>
)
}
</View>);
});

View File

@@ -73,5 +73,15 @@ export default {
clearIcon: {
color: BaseTheme.palette.icon01
},
bottomLabel: {
...BaseTheme.typography.labelRegular,
color: BaseTheme.palette.text02,
marginTop: BaseTheme.spacing[2]
},
bottomLabelError: {
color: BaseTheme.palette.textError
}
};

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

@@ -116,7 +116,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
if (!isReactionsEnabled(state)) {
return;
return next(action);
}
const { participant, data } = action;

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

@@ -314,6 +314,7 @@ function _calendarNotification({ dispatch, getState }: IStore, eventToShow: any)
customActionType,
description,
icon,
maxLines: 1,
title,
uid
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));

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

@@ -427,17 +427,20 @@ export function invitePeopleAndChatRooms(
return Promise.resolve();
}
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
'Content-Type': 'application/json'
};
return fetch(
`${inviteServiceUrl}?token=${jwt}`,
`${inviteServiceUrl}`,
{
body: JSON.stringify({
'invited': inviteItems,
'url': inviteUrl
}),
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
headers
}
);
}
@@ -544,8 +547,16 @@ export function searchDirectory( // eslint-disable-line max-params
const query = encodeURIComponent(text);
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {}
};
return fetch(`${serviceUrl}?query=${query}&queryTypes=${
queryTypesString}&jwt=${jwt}`)
queryTypesString}`,
{
method: 'GET',
headers
})
.then(response => {
const jsonify = response.json();

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

@@ -152,15 +152,16 @@ const Notification = ({
if (descriptionArray?.length) {
return (
<>
<Text style = { styles.contentTextTitle as TextStyle }>
{titleText}
<Text
style = { styles.contentTextTitle as TextStyle }>
{ titleText }
</Text>
{
descriptionArray.map((line, index) => (
<Text
key = { index }
style = { styles.contentText }>
{replaceNonUnicodeEmojis(line)}
{ replaceNonUnicodeEmojis(line) }
</Text>
))
}
@@ -170,7 +171,7 @@ const Notification = ({
return (
<Text style = { styles.contentTextTitle as TextStyle }>
{titleText}
{ titleText }
</Text>
);
};
@@ -199,10 +200,10 @@ const Notification = ({
<View
pointerEvents = 'box-none'
style = { styles.contentContainer }>
{_renderContent()}
{ _renderContent() }
</View>
<View style = { styles.btnContainer as ViewStyle }>
{mapAppearanceToButtons()}
{ mapAppearanceToButtons() }
</View>
</View>
<IconButton

View File

@@ -3,21 +3,20 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const contentColumn = {
flex: 1,
flexDirection: 'column',
marginLeft: BaseTheme.spacing[2]
paddingLeft: BaseTheme.spacing[2]
};
const notification = {
display: 'flex',
backgroundColor: BaseTheme.palette.ui10,
borderRadius: BaseTheme.shape.borderRadius,
borderLeftColor: BaseTheme.palette.link01Active,
borderLeftWidth: 4,
borderLeftWidth: BaseTheme.spacing[1],
display: 'flex',
flexDirection: 'row',
maxHeight: 120,
height: 'auto',
marginBottom: BaseTheme.spacing[2],
marginHorizontal: BaseTheme.spacing[2],
maxWidth: 400,
paddingBottom: BaseTheme.spacing[2],
paddingHorizontal: BaseTheme.spacing[2],
maxWidth: 432,
width: 'auto'
};
@@ -43,20 +42,20 @@ export default {
*/
contentContainer: {
marginTop: BaseTheme.spacing[2]
paddingHorizontal: BaseTheme.spacing[2]
},
contentText: {
color: BaseTheme.palette.text04,
marginLeft: BaseTheme.spacing[6],
marginTop: BaseTheme.spacing[1]
paddingLeft: BaseTheme.spacing[5],
paddingTop: BaseTheme.spacing[1]
},
contentTextTitle: {
color: BaseTheme.palette.text04,
marginLeft: BaseTheme.spacing[6],
fontWeight: 'bold',
marginTop: BaseTheme.spacing[1]
paddingLeft: BaseTheme.spacing[5],
paddingTop: BaseTheme.spacing[2]
},
/**
@@ -90,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[1]
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

@@ -18,6 +18,17 @@ export const CHANGE_VOTE = 'CHANGE_VOTE';
*/
export const CLEAR_POLLS = 'CLEAR_POLLS';
/**
* The type of the action triggered when the poll is editing.
*
* {
* type: EDIT_POLL,
* pollId: string,
* editing: boolean
* }
*/
export const EDIT_POLL = 'EDIT_POLL';
/**
* The type of the action which signals that a new Poll was received.
*
@@ -71,3 +82,15 @@ export const RETRACT_VOTE = 'RETRACT_VOTE';
* }
*/
export const RESET_NB_UNREAD_POLLS = 'RESET_NB_UNREAD_POLLS';
/**
* The type of the action triggered when the poll is saved.
*
* {
* type: SAVE_POLL,
* poll: Poll,
* pollId: string,
* saved: boolean
* }
*/
export const SAVE_POLL = 'SAVE_POLL';

View File

@@ -1,11 +1,13 @@
import {
CHANGE_VOTE,
CLEAR_POLLS,
EDIT_POLL,
RECEIVE_ANSWER,
RECEIVE_POLL,
REGISTER_VOTE,
RESET_NB_UNREAD_POLLS,
RETRACT_VOTE
RETRACT_VOTE,
SAVE_POLL
} from './actionTypes';
import { IAnswer, IPoll } from './types';
@@ -128,3 +130,46 @@ export function resetNbUnreadPollsMessages() {
type: RESET_NB_UNREAD_POLLS
};
}
/**
* Action to signal saving a poll.
*
* @param {string} pollId - The id of the poll that gets to be saved.
* @param {IPoll} poll - The Poll object that gets to be saved.
* @param {boolean} saved - Whether the poll is saved or not.
* @returns {{
* type: RECEIVE_POLL,
* poll: IPoll,
* pollId: string,
* saved: boolean
* }}
*/
export function savePoll(pollId: string, poll: IPoll, saved: boolean) {
return {
type: SAVE_POLL,
pollId,
poll: {
...poll,
saved
}
};
}
/**
* Action to signal editing a poll.
*
* @param {string} pollId - The id of the poll that gets to be edited.
* @param {boolean} editing - Whether the poll is in edit mode or not.
* @returns {{
* type: RECEIVE_POLL,
* pollId: string,
* editing: boolean
* }}
*/
export function editPoll(pollId: string, editing: boolean) {
return {
type: EDIT_POLL,
pollId,
editing
};
}

View File

@@ -7,8 +7,8 @@ import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { getParticipantDisplayName } from '../../base/participants/functions';
import { useBoundSelector } from '../../base/util/hooks';
import { registerVote, setVoteChanging } from '../actions';
import { COMMAND_ANSWER_POLL } from '../constants';
import { editPoll, registerVote, setVoteChanging } from '../actions';
import { COMMAND_ANSWER_POLL, COMMAND_NEW_POLL } from '../constants';
import { IPoll } from '../types';
/**
@@ -16,6 +16,7 @@ import { IPoll } from '../types';
*/
type InputProps = {
pollId: string;
setCreateMode: (mode: boolean) => void;
};
/*
@@ -26,7 +27,10 @@ export type AbstractProps = {
checkBoxStates: boolean[];
creatorName: string;
poll: IPoll;
pollId: string;
sendPoll: () => void;
setCheckbox: Function;
setCreateMode: (mode: boolean) => void;
skipAnswer: () => void;
skipChangeVote: () => void;
submitAnswer: () => void;
@@ -42,21 +46,23 @@ export type AbstractProps = {
*/
const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { pollId } = props;
const { pollId, setCreateMode } = props;
const conference: any = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const poll: IPoll = useSelector((state: IReduxState) => state['features/polls'].polls[pollId]);
const { answers, lastVote, question, senderId } = poll;
const [ checkBoxStates, setCheckBoxState ] = useState(() => {
if (poll.lastVote !== null) {
return [ ...poll.lastVote ];
if (lastVote !== null) {
return [ ...lastVote ];
}
return new Array(poll.answers.length).fill(false);
return new Array(answers.length).fill(false);
});
const participantName = useBoundSelector(getParticipantDisplayName, poll.senderId);
const participantName = useBoundSelector(getParticipantDisplayName, senderId);
const setCheckbox = useCallback((index, state) => {
const newCheckBoxStates = [ ...checkBoxStates ];
@@ -81,10 +87,21 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
return false;
}, [ pollId, checkBoxStates, conference ]);
const sendPoll = useCallback(() => {
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId,
question,
answers: answers.map(answer => answer.name)
});
dispatch(editPoll(pollId, false));
}, [ conference, question, answers ]);
const skipAnswer = useCallback(() => {
dispatch(registerVote(pollId, null));
sendAnalytics(createPollEvent('vote.skipped'));
}, [ pollId ]);
const skipChangeVote = useCallback(() => {
@@ -97,7 +114,10 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
checkBoxStates = { checkBoxStates }
creatorName = { participantName }
poll = { poll }
pollId = { pollId }
sendPoll = { sendPoll }
setCheckbox = { setCheckbox }
setCreateMode = { setCreateMode }
skipAnswer = { skipAnswer }
skipChangeVote = { skipChangeVote }
submitAnswer = { submitAnswer }

View File

@@ -1,12 +1,16 @@
import React, { ComponentType, FormEvent, useCallback, useState } from 'react';
/* eslint-disable arrow-body-style */
import React, { ComponentType, FormEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { COMMAND_NEW_POLL } from '../constants';
import { getLocalParticipant } from '../../base/participants/functions';
import { savePoll } from '../actions';
import { hasIdenticalAnswers } from '../functions';
import { IAnswerData, IPoll } from '../types';
/**
* The type of the React {@code Component} props of inheriting component.
@@ -21,12 +25,14 @@ type InputProps = {
**/
export type AbstractProps = InputProps & {
addAnswer: (index?: number) => void;
answers: Array<string>;
answers: Array<IAnswerData>;
editingPoll: IPoll | undefined;
editingPollId: string | undefined;
isSubmitDisabled: boolean;
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
question: string;
removeAnswer: (index: number) => void;
setAnswer: (index: number, value: string) => void;
setAnswer: (index: number, value: IAnswerData) => void;
setQuestion: (question: string) => void;
t: Function;
};
@@ -44,11 +50,45 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
const { setCreateMode } = props;
const [ question, setQuestion ] = useState('');
const pollState = useSelector((state: IReduxState) => state['features/polls'].polls);
const [ answers, setAnswers ] = useState([ '', '' ]);
const editingPoll: [ string, IPoll ] | null = useMemo(() => {
if (!pollState) {
return null;
}
const setAnswer = useCallback((i, answer) => {
for (const key in pollState) {
if (pollState.hasOwnProperty(key) && pollState[key].editing) {
return [ key, pollState[key] ];
}
}
return null;
}, [ pollState ]);
const answerResults = useMemo(() => {
return editingPoll
? editingPoll[1].answers
: [
{
name: '',
voters: []
},
{
name: '',
voters: []
} ];
}, [ editingPoll ]);
const questionResult = useMemo(() => {
return editingPoll ? editingPoll[1].question : '';
}, [ editingPoll ]);
const [ question, setQuestion ] = useState(questionResult);
const [ answers, setAnswers ] = useState(answerResults);
const setAnswer = useCallback((i: number, answer: IAnswerData) => {
setAnswers(currentAnswers => {
const newAnswers = [ ...currentAnswers ];
@@ -59,10 +99,14 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
}, [ answers ]);
const addAnswer = useCallback((i?: number) => {
const newAnswers = [ ...answers ];
const newAnswers: Array<IAnswerData> = [ ...answers ];
sendAnalytics(createPollEvent('option.added'));
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
newAnswers.splice(typeof i === 'number'
? i : answers.length, 0, {
name: '',
voters: []
});
setAnswers(newAnswers);
}, [ answers ]);
@@ -79,23 +123,40 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const dispatch = useDispatch();
const pollId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
const localParticipant = useSelector(getLocalParticipant);
const onSubmit = useCallback(ev => {
if (ev) {
ev.preventDefault();
}
const filteredAnswers = answers.filter(answer => answer.trim().length > 0);
const filteredAnswers = answers.filter(answer => answer.name.trim().length > 0);
if (filteredAnswers.length < 2) {
return;
}
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
const poll = {
changingVote: false,
senderId: localParticipant?.id,
showResults: false,
lastVote: null,
question,
answers: filteredAnswers
});
answers: filteredAnswers,
saved: false,
editing: false
};
if (editingPoll) {
dispatch(savePoll(editingPoll[0], poll, true));
} else {
dispatch(savePoll(pollId, poll, true));
}
sendAnalytics(createPollEvent('created'));
setCreateMode(false);
@@ -105,7 +166,7 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
// Check if the poll create form can be submitted i.e. if the send button should be disabled.
const isSubmitDisabled
= question.trim().length <= 0 // If no question is provided
|| answers.filter(answer => answer.trim().length > 0).length < 2 // If not enough options are provided
|| answers.filter(answer => answer.name.trim().length > 0).length < 2 // If not enough options are provided
|| hasIdenticalAnswers(answers); // If duplicate options are provided
const { t } = useTranslation();
@@ -113,6 +174,8 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
return (<Component
addAnswer = { addAnswer }
answers = { answers }
editingPoll = { editingPoll?.[1] }
editingPollId = { editingPoll?.[0] }
isSubmitDisabled = { isSubmitDisabled }
onSubmit = { onSubmit }
question = { question }

View File

@@ -69,9 +69,9 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
// Getting every voters ID that participates to the poll
for (const answer of pollDetails.answers) {
// checking if the voters is an array for supporting old structure model
const voters = answer.voters?.length ? answer.voters : Object.keys(answer.voters);
const voters: string[] = answer.voters.length ? answer.voters : Object.keys(answer.voters);
voters.forEach(voter => allVoters.add(voter));
voters.forEach((voter: string) => allVoters.add(voter));
}
return pollDetails.answers.map(answer => {

View File

@@ -1,28 +1,34 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import Switch from '../../../base/ui/components/native/Switch';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
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 {
checkBoxStates,
poll,
pollId,
sendPoll,
setCheckbox,
setCreateMode,
skipAnswer,
skipChangeVote,
submitAnswer,
t
} = props;
const { changingVote } = poll;
const { changingVote, saved: pollSaved } = poll;
const dispatch = useDispatch();
const localParticipant = useSelector(getLocalParticipant);
const { PRIMARY, SECONDARY } = BUTTON_TYPES;
@@ -33,34 +39,58 @@ const PollAnswer = (props: AbstractProps) => {
t('polls.by', { name: localParticipant?.name })
}
</Text>
<View style = { chatStyles.answerContent as ViewStyle }>
{poll.answers.map((answer, index) => (
<View
key = { index }
style = { chatStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel as TextStyle }>{answer.name}</Text>
<View style = { pollsStyles.answerContent as ViewStyle }>
{
poll.answers.map((answer, index: number) => (
<View
key = { index }
style = { pollsStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
disabled = { poll.saved }
onChange = { state => setCheckbox(index, state) } />
<Text style = { pollsStyles.switchLabel as TextStyle }>
{ answer.name }
</Text>
</View>
))
}
</View>
{
pollSaved
? <View style = { pollsStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.edit'
labelKey = 'polls.answer.edit'
onClick = { () => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
style = { pollsStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.send'
labelKey = 'polls.answer.send'
onClick = { sendPoll }
style = { pollsStyles.pollCreateButton }
type = { PRIMARY } />
</View>
))}
</View>
<View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
: <View style = { pollsStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { pollsStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { pollsStyles.pollCreateButton }
type = { PRIMARY } />
</View>
}
</>
);
};

View File

@@ -1,22 +1,24 @@
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';
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 {
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
@@ -28,6 +30,7 @@ const PollCreate = (props: AbstractProps) => {
} = props;
const answerListRef = useRef<FlatList>(null);
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -74,7 +77,7 @@ const PollCreate = (props: AbstractProps) => {
// Called on keypress in answer fields
const onAnswerKeyDown = useCallback((index: number, ev) => {
const { key } = ev.nativeEvent;
const currentText = answers[index];
const currentText = answers[index].name;
if (key === 'Backspace' && currentText === '' && answers.length > 1) {
removeAnswer(index);
@@ -92,56 +95,72 @@ const PollCreate = (props: AbstractProps) => {
type = { TERTIARY } />
);
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
/* eslint-disable react/jsx-no-bind */
const renderListItem = ({ index }: { index: number; }) =>
const renderListItem = ({ index }: { index: number; }) => {
// padding to take into account the two default options
(
const isIdenticalAnswer
= answers.slice(0, index).length === 0 ? false : answers.slice(0, index).some(prevAnswer =>
prevAnswer.name === answers[index].name
&& prevAnswer.name !== '' && answers[index].name !== '');
return (
<View
style = { dialogStyles.optionContainer as ViewStyle }>
<Input
blurOnSubmit = { false }
bottomLabel = { (
isIdenticalAnswer ? t('polls.errors.notUniqueOption', { index: index + 1 }) : '') }
error = { isIdenticalAnswer }
id = { `polls-answer-input-${index}` }
label = { t('polls.create.pollOption', { index: index + 1 }) }
maxLength = { CHAR_LIMIT }
multiline = { true }
onChange = { text => setAnswer(index, text) }
onChange = { name => setAnswer(index,
{
name,
voters: []
}) }
onKeyPress = { ev => onAnswerKeyDown(index, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: index + 1 }) }
// This is set to help the touch event not be propagated to any subviews.
pointerEvents = { 'auto' }
ref = { input => registerFieldRef(index, input) }
value = { answers[index] } />
value = { answers[index].name } />
{
answers.length > 2
&& createRemoveOptionButton(() => removeAnswer(index))
}
</View>
);
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
? chatStyles.pollCreateButtonsContainerAndroid : chatStyles.pollCreateButtonsContainerIos;
};
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 } />
<Divider style = { pollsStyles.fieldSeparator as ViewStyle } />
</>
), [ question ]);
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') }
// This is set to help the touch event not be propagated to any subviews.
pointerEvents = { 'auto' }
value = { question } />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
<FlatList
ListHeaderComponent = { renderListHeaderComponent }
data = { answers }
extraData = { answers }
keyExtractor = { (item, index) => index.toString() }
@@ -157,22 +176,27 @@ 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'
onClick = { () => setCreateMode(false) }
style = { chatStyles.pollCreateButton }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
style = { pollsStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.create.send'
accessibilityLabel = 'polls.create.save'
disabled = { isSubmitDisabled }
labelKey = 'polls.create.send'
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 {
@@ -15,20 +15,26 @@ interface IProps {
*/
pollId: string;
/**
* Create mode control.
*/
setCreateMode: (mode: boolean) => void;
}
const PollItem = ({ pollId }: IProps) => {
const PollItem = ({ pollId, setCreateMode }: IProps) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
<View
style = { chatStyles.pollItemContainer as ViewStyle }>
style = { pollsStyles.pollItemContainer as ViewStyle }>
{ showResults
? <PollResults
key = { pollId }
pollId = { pollId } />
: <PollAnswer
pollId = { pollId } />
pollId = { pollId }
setCreateMode = { setCreateMode } />
}
</View>

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,9 +10,13 @@ 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';
const PollsList = () => {
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
}
const PollsList = ({ setCreateMode }: IPollListProps) => {
const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
const { t } = useTranslation();
const listPolls = Object.keys(polls);
@@ -20,7 +24,8 @@ const PollsList = () => {
const renderItem = useCallback(({ item }) => (
<PollItem
key = { item }
pollId = { item } />)
pollId = { item }
setCreateMode = { setCreateMode } />)
, []);
const flatlistRef = useRef<FlatList>(null);
@@ -37,12 +42,12 @@ const PollsList = () => {
<>
{
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,32 +42,31 @@ 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 } />
: <PollsList />
}
{
!createMode && <Button
accessibilityLabel = 'polls.create.create'
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />
: <>
<PollsList setCreateMode = { setCreateMode } />
<Button
accessibilityLabel = 'polls.create.create'
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />
</>
}
</JitsiScreen>
);
};
/*
* We apply AbstractPollsPane to fill in the AbstractProps common
* to both the web and native implementations.

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
},
@@ -209,7 +207,8 @@ export const chatStyles = createStyleSheet({
},
createPollButtonAndroid: {
marginHorizontal: 20
marginHorizontal: 20,
marginVertical: BaseTheme.spacing[5]
},
pollPane: {
@@ -228,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

@@ -1,10 +1,14 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Checkbox from '../../../base/ui/components/web/Checkbox';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { isSubmitAnswerDisabled } from '../../functions';
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
@@ -53,13 +57,18 @@ const PollAnswer = ({
creatorName,
checkBoxStates,
poll,
pollId,
setCheckbox,
setCreateMode,
skipAnswer,
skipChangeVote,
sendPoll,
submitAnswer,
t
}: AbstractProps) => {
const { changingVote } = poll;
const { changingVote, saved: pollSaved } = poll;
const dispatch = useDispatch();
const { classes } = useStyles();
return (
@@ -74,32 +83,50 @@ const PollAnswer = ({
</div>
<ul className = { classes.answerList }>
{
poll.answers.map((answer: any, index: number) => (
poll.answers.map((answer, index: number) => (
<li
className = { classes.answer }
key = { index }>
<Checkbox
checked = { checkBoxStates[index] }
disabled = { poll.saved }
key = { index }
label = { answer.name }
// eslint-disable-next-line react/jsx-no-bind
onChange = { ev => setCheckbox(index, ev.target.checked) } />
</li>
))
}
</ul>
<div className = { classes.footer } >
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
{
pollSaved ? <>
<Button
accessibilityLabel = { t('polls.answer.edit') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.edit' }
onClick = { () => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.send') }
labelKey = { 'polls.answer.send' }
onClick = { sendPoll } />
</> : <>
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
</>
}
</div>
</div>
);

View File

@@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
@@ -64,6 +66,8 @@ const useStyles = makeStyles()(theme => {
const PollCreate = ({
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
@@ -74,6 +78,7 @@ const PollCreate = ({
t
}: AbstractProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -201,11 +206,12 @@ const PollCreate = ({
value = { question } />
</div>
<ol className = { classes.answerList }>
{answers.map((answer: any, i: number) => {
{answers.map((answer, i: number) => {
const isIdenticalAnswer = answers.slice(0, i).length === 0 ? false
: answers.slice(0, i).some((prevAnswer: string) =>
prevAnswer === answer && prevAnswer !== '' && answer !== '');
: answers.slice(0, i).some(prevAnswer =>
prevAnswer.name === answer.name
&& prevAnswer.name !== '' && answer.name !== '');
return (<li
className = { classes.answer }
@@ -217,12 +223,15 @@ const PollCreate = ({
id = { `polls-answer-input-${i}` }
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { val => setAnswer(i, val) }
onChange = { name => setAnswer(i, {
name,
voters: []
}) }
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
textarea = { true }
value = { answer } />
value = { answer.name } />
{ answers.length > 2
&& <button
@@ -252,13 +261,18 @@ const PollCreate = ({
accessibilityLabel = { t('polls.create.cancel') }
className = { classes.buttonMargin }
labelKey = { 'polls.create.cancel' }
onClick = { () => setCreateMode(false) }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.create.send') }
accessibilityLabel = { t('polls.create.save') }
disabled = { isSubmitDisabled }
isSubmit = { true }
labelKey = { 'polls.create.send' } />
labelKey = { 'polls.create.save' } />
</div>
</form>);
};

View File

@@ -14,9 +14,14 @@ interface IProps {
*/
pollId: string;
/**
* Create mode control.
*/
setCreateMode: (mode: boolean) => void;
}
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => {
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId, setCreateMode }: IProps, ref) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
@@ -26,7 +31,8 @@ const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, r
key = { pollId }
pollId = { pollId } />
: <PollAnswer
pollId = { pollId } />
pollId = { pollId }
setCreateMode = { setCreateMode } />
}
</div>

View File

@@ -39,7 +39,11 @@ const useStyles = makeStyles()(theme => {
};
});
const PollsList = () => {
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
}
const PollsList = ({ setCreateMode }: IPollListProps) => {
const { t } = useTranslation();
const { classes, theme } = useStyles();
@@ -80,7 +84,8 @@ const PollsList = () => {
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
ref = { listPolls.length - 1 === index ? pollListEndRef : null }
setCreateMode = { setCreateMode } />
))}
</>
);

View File

@@ -35,7 +35,7 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
? <PollCreate setCreateMode = { setCreateMode } />
: <div className = { classes.container }>
<div className = { classes.listContainer } >
<PollsList />
<PollsList setCreateMode = { setCreateMode } />
</div>
<div className = { classes.footer }>
<Button

View File

@@ -1,5 +1,7 @@
import { IReduxState } from '../app/types';
import { IAnswerData } from './types';
/**
* Selector creator for determining if poll results should be displayed or not.
*
@@ -49,14 +51,14 @@ export function isSubmitAnswerDisabled(checkBoxStates: Array<boolean>) {
/**
* Check if the input array has identical answers.
*
* @param {Array<string>} currentAnswers - The array of current answers to compare.
* @param {Array<IAnswerData>} currentAnswers - The array of current answers to compare.
* @returns {boolean} - Returns true if the answers are identical.
*/
export function hasIdenticalAnswers(currentAnswers: Array<string>): boolean {
export function hasIdenticalAnswers(currentAnswers: Array<IAnswerData>): boolean {
const nonEmptyCurrentAnswers = currentAnswers.filter((answer: string): boolean => answer !== '');
const nonEmptyCurrentAnswers = currentAnswers.filter((answer): boolean => answer.name !== '');
const currentAnswersSet = new Set(nonEmptyCurrentAnswers);
const currentAnswersSet = new Set(nonEmptyCurrentAnswers.map(answer => answer.name));
return currentAnswersSet.size !== nonEmptyCurrentAnswers.length;
}

View File

@@ -50,7 +50,9 @@ const parsePollData = (pollData: IPollData): IPoll | null => {
question,
showResults: true,
lastVote: null,
answers
answers,
saved: false,
editing: false
};
};
@@ -121,7 +123,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
switch (data.type) {
case COMMAND_NEW_POLL: {
const { question, answers, pollId, senderId } = data;
const { pollId, answers, senderId, question } = data;
const poll = {
changingVote: false,
@@ -129,12 +131,14 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
showResults: false,
lastVote: null,
question,
answers: answers.map((answer: IAnswer) => {
answers: answers.map((answer: string) => {
return {
name: answer,
voters: []
};
})
}),
saved: false,
editing: false
};
dispatch(receivePoll(pollId, poll, true));

View File

@@ -3,11 +3,13 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
CHANGE_VOTE,
CLEAR_POLLS,
EDIT_POLL,
RECEIVE_ANSWER,
RECEIVE_POLL,
REGISTER_VOTE,
RESET_NB_UNREAD_POLLS,
RETRACT_VOTE
RETRACT_VOTE,
SAVE_POLL
} from './actionTypes';
import { IAnswer, IPoll } from './types';
@@ -51,20 +53,17 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
};
}
// Reducer triggered when a poll is received
case RECEIVE_POLL: {
const newState = {
// Reducer triggered when a poll is received or saved.
case RECEIVE_POLL:
case SAVE_POLL: {
return {
...state,
polls: {
...state.polls,
// The poll is added to the dictionary of received polls
[action.pollId]: action.poll
},
nbUnreadPolls: state.nbUnreadPolls + 1
};
return newState;
}
// Reducer triggered when an answer is received
@@ -162,6 +161,19 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
};
}
case EDIT_POLL: {
return {
...state,
polls: {
...state.polls,
[action.pollId]: {
...state.polls[action.pollId],
editing: action.editing
}
}
};
}
default:
return state;
}

View File

@@ -27,13 +27,18 @@ export interface IPoll {
* An array of answers:
* the name of the answer name and a map of ids and names of voters voting for this option.
*/
answers: Array<{ name: string; voters: Array<string>; }>;
answers: Array<IAnswerData>;
/**
* Whether the poll vote is being edited/changed.
*/
changingVote: boolean;
/**
* Whether poll is in edit mode or not?.
*/
editing: boolean;
/**
* The last sent votes for this poll, or null if voting was skipped
* Note: This is reset when voting/skipping, not when clicking "Change vote".
@@ -45,10 +50,15 @@ export interface IPoll {
*/
question: string;
/**
* Whether poll is saved or not?.
*/
saved: boolean;
/**
* ID of the sender of this poll.
*/
senderId: string;
senderId: string | undefined;
/**
* Whether the results should be shown instead of the answer form.
@@ -59,3 +69,16 @@ export interface IPoll {
export interface IPollData extends IPoll {
id: string;
}
export interface IAnswerData {
/**
* The answer name chosen for the poll.
*/
name: string;
/**
* An array of voters.
*/
voters: Array<string>;
}

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

@@ -172,8 +172,11 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
}
}
// If the suer is not requesting transcriptions just bail.
if (json.language.slice(0, 2) !== language) {
// If the user is not requesting transcriptions just bail.
// Regex to filter out all possible country codes after language code:
// this should catch all notations like 'en-GB' 'en_GB' and 'enGB'
// and be independent of the country code length
if (json.language.replace(/[-_A-Z].*/, '') !== language) {
return next(action);
}

View File

@@ -59,6 +59,16 @@ export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
*/
export const SET_HANGUP_MENU_VISIBLE = 'SET_HANGUP_MENU_VISIBLE';
/**
* The type of the (redux) action which sets the main toolbar thresholds.
*
* {
* type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
* mainToolbarButtonsThresholds: IMainToolbarButtonThresholds
* }
*/
export const SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS = 'SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS';
/**
* The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
*/

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../../app/types';
import { IReduxState } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { getLocalParticipant, isLocalParticipantModerator } from '../../../base/participants/functions';
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
import { isReactionsButtonEnabled, shouldDisplayReactionsButtons } from '../../../reactions/functions.web';
import {
@@ -15,15 +14,14 @@ import {
setToolbarHovered,
showToolbox
} from '../../actions.web';
import { NOT_APPLICABLE, THRESHOLDS } from '../../constants';
import {
getAllToolboxButtons,
getJwtDisabledButtons,
getVisibleButtons,
isButtonEnabled,
isToolboxVisible
} from '../../functions.web';
import { useKeyboardShortcuts } from '../../hooks.web';
import { IToolboxButton, NOTIFY_CLICK_MODE } from '../../types';
import { useKeyboardShortcuts, useToolboxButtons } from '../../hooks.web';
import { IToolboxButton } from '../../types';
import HangupButton from '../HangupButton';
import { EndConferenceButton } from './EndConferenceButton';
@@ -35,107 +33,12 @@ import Separator from './Separator';
/**
* The type of the React {@code Component} props of {@link Toolbox}.
*/
interface IProps extends WithTranslation {
/**
* Toolbar buttons which have their click exposed through the API.
*/
_buttonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
/**
* Whether or not the chat feature is currently displayed.
*/
_chatOpen: boolean;
/**
* The width of the client.
*/
_clientWidth: number;
/**
* Custom Toolbar buttons.
*/
_customToolbarButtons?: Array<{ backgroundColor?: string; icon: string; id: string; text: string; }>;
/**
* Whether or not a dialog is displayed.
*/
_dialog: boolean;
/**
* Whether or not the toolbox is disabled. It is for recorders.
*/
_disabled: boolean;
/**
* Whether the end conference feature is supported.
*/
_endConferenceSupported: boolean;
/**
* Whether the hangup menu is visible.
*/
_hangupMenuVisible: boolean;
/**
* Whether or not the app is running in mobile browser.
*/
_isMobile: boolean;
/**
* Whether we are in narrow layout mode.
*/
_isNarrowLayout: boolean;
/**
* The array of toolbar buttons disabled through jwt features.
*/
_jwtDisabledButtons: string[];
/**
* Whether or not the overflow menu is displayed in a drawer drawer.
*/
_overflowDrawer: boolean;
/**
* Whether or not the overflow menu is visible.
*/
_overflowMenuVisible: boolean;
/**
* Whether or not to display reactions in separate button.
*/
_reactionsButtonEnabled: boolean;
/**
* Whether the toolbox should be shifted up or not.
*/
_shiftUp: boolean;
/**
* Whether any reactions buttons should be displayed or not.
*/
_shouldDisplayReactionsButtons: boolean;
/**
* The enabled buttons.
*/
_toolbarButtons: Array<string>;
/**
* Flag showing whether toolbar is visible.
*/
_visible: boolean;
/**
* Invoked to active other features of the app.
*/
dispatch: IStore['dispatch'];
interface IProps {
/**
* Explicitly passed array with the buttons which this Toolbox should display.
*/
toolbarButtons: Array<string>;
toolbarButtons?: Array<string>;
}
const useStyles = makeStyles()(() => {
@@ -162,42 +65,58 @@ const useStyles = makeStyles()(() => {
};
});
const Toolbox = ({
_buttonsWithNotifyClick,
_chatOpen,
_clientWidth,
_customToolbarButtons,
_dialog,
_disabled,
_endConferenceSupported,
_hangupMenuVisible,
_isMobile,
_isNarrowLayout,
_jwtDisabledButtons,
_overflowDrawer,
_overflowMenuVisible,
_reactionsButtonEnabled,
_shiftUp,
_shouldDisplayReactionsButtons,
_toolbarButtons,
_visible,
dispatch,
t,
/**
* A component that renders the main toolbar.
*
* @param {IProps} props - The props of the component.
* @returns {ReactElement}
*/
export default function Toolbox({
toolbarButtons
}: IProps) => {
}: IProps) {
const { classes, cx } = useStyles();
const { t } = useTranslation();
const dispatch = useDispatch();
const _toolboxRef = useRef<HTMLDivElement>(null);
useKeyboardShortcuts(toolbarButtons);
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const isNarrowLayout = useSelector((state: IReduxState) => state['features/base/responsive-ui'].isNarrowLayout);
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
const isModerator = useSelector(isLocalParticipantModerator);
const customToolbarButtons = useSelector(
(state: IReduxState) => state['features/base/config'].customToolbarButtons);
const iAmRecorder = useSelector((state: IReduxState) => state['features/base/config'].iAmRecorder);
const iAmSipGateway = useSelector((state: IReduxState) => state['features/base/config'].iAmSipGateway);
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
const shiftUp = useSelector((state: IReduxState) => state['features/toolbox'].shiftUp);
const overflowMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].overflowMenuVisible);
const hangupMenuVisible = useSelector((state: IReduxState) => state['features/toolbox'].hangupMenuVisible);
const buttonsWithNotifyClick
= useSelector((state: IReduxState) => state['features/toolbox'].buttonsWithNotifyClick);
const reduxToolbarButtons = useSelector((state: IReduxState) => state['features/toolbox'].toolbarButtons);
const toolbarButtonsToUse = toolbarButtons || reduxToolbarButtons;
const chatOpen = useSelector((state: IReduxState) => state['features/chat'].isOpen);
const isDialogVisible = useSelector((state: IReduxState) => Boolean(state['features/base/dialog'].component));
const jwt = useSelector((state: IReduxState) => state['features/base/jwt'].jwt);
const localParticipant = useSelector(getLocalParticipant);
const jwtDisabledButtons = getJwtDisabledButtons(jwt, localParticipant?.features);
const reactionsButtonEnabled = useSelector(isReactionsButtonEnabled);
const _shouldDisplayReactionsButtons = useSelector(shouldDisplayReactionsButtons);
const toolbarVisible = useSelector(isToolboxVisible);
const mainToolbarButtonsThresholds
= useSelector((state: IReduxState) => state['features/toolbox'].mainToolbarButtonsThresholds);
const allButtons = useToolboxButtons(customToolbarButtons);
useKeyboardShortcuts(toolbarButtonsToUse);
useEffect(() => {
if (!_visible) {
if (!toolbarVisible) {
if (document.activeElement instanceof HTMLElement
&& _toolboxRef.current?.contains(document.activeElement)) {
document.activeElement.blur();
}
}
}, [ _visible ]);
}, [ toolbarVisible ]);
/**
* Sets the visibility of the hangup menu.
@@ -210,7 +129,7 @@ const Toolbox = ({
const onSetHangupVisible = useCallback((visible: boolean) => {
dispatch(setHangupMenuVisible(visible));
dispatch(setToolbarHovered(visible));
}, []);
}, [ dispatch ]);
/**
* Sets the visibility of the overflow menu.
@@ -223,21 +142,21 @@ const Toolbox = ({
const onSetOverflowVisible = useCallback((visible: boolean) => {
dispatch(setOverflowMenuVisible(visible));
dispatch(setToolbarHovered(visible));
}, []);
}, [ dispatch ]);
useEffect(() => {
if (_hangupMenuVisible && !_visible) {
if (hangupMenuVisible && !toolbarVisible) {
onSetHangupVisible(false);
dispatch(setToolbarHovered(false));
}
}, [ _hangupMenuVisible, _visible ]);
}, [ dispatch, hangupMenuVisible, toolbarVisible, onSetHangupVisible ]);
useEffect(() => {
if (_overflowMenuVisible && _dialog) {
if (overflowMenuVisible && isDialogVisible) {
onSetOverflowVisible(false);
dispatch(setToolbarHovered(false));
}
}, [ _overflowMenuVisible, _dialog ]);
}, [ dispatch, overflowMenuVisible, isDialogVisible, onSetOverflowVisible ]);
/**
* Key handler for overflow/hangup menus.
@@ -248,69 +167,10 @@ const Toolbox = ({
const onEscKey = useCallback((e?: React.KeyboardEvent) => {
if (e?.key === 'Escape') {
e?.stopPropagation();
_hangupMenuVisible && dispatch(setHangupMenuVisible(false));
_overflowMenuVisible && dispatch(setOverflowMenuVisible(false));
hangupMenuVisible && dispatch(setHangupMenuVisible(false));
overflowMenuVisible && dispatch(setOverflowMenuVisible(false));
}
}, [ _hangupMenuVisible, _overflowMenuVisible ]);
/**
* Sets the notify click mode for the buttons.
*
* @param {Object} buttons - The list of toolbar buttons.
* @returns {void}
*/
function setButtonsNotifyClickMode(buttons: Object) {
if (typeof APP === 'undefined' || (_buttonsWithNotifyClick?.size ?? 0) <= 0) {
return;
}
Object.values(buttons).forEach((button: any) => {
if (typeof button === 'object') {
button.notifyMode = _buttonsWithNotifyClick.get(button.key);
}
});
}
/**
* Returns all buttons that need to be rendered.
*
* @param {Object} state - The redux state.
* @returns {Object} The visible buttons arrays .
*/
function getVisibleButtons() {
const buttons = getAllToolboxButtons(_customToolbarButtons);
setButtonsNotifyClickMode(buttons);
const isHangupVisible = isButtonEnabled('hangup', _toolbarButtons);
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|| THRESHOLDS[THRESHOLDS.length - 1];
const keys = Object.keys(buttons);
const filtered = [
...order.map(key => buttons[key as keyof typeof buttons]),
...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
].filter(({ key, alias = NOT_APPLICABLE }) =>
!_jwtDisabledButtons.includes(key)
&& (isButtonEnabled(key, _toolbarButtons) || isButtonEnabled(alias, _toolbarButtons))
);
let sliceIndex = _overflowDrawer || _reactionsButtonEnabled ? order.length + 2 : order.length + 1;
if (isHangupVisible) {
sliceIndex -= 1;
}
// This implies that the overflow button will be displayed, so save some space for it.
if (sliceIndex < filtered.length) {
sliceIndex -= 1;
}
return {
mainMenuButtons: filtered.slice(0, sliceIndex),
overflowMenuButtons: filtered.slice(sliceIndex)
};
}
}, [ dispatch, hangupMenuVisible, overflowMenuVisible ]);
/**
* Dispatches an action signaling the toolbar is not being hovered.
@@ -318,9 +178,9 @@ const Toolbox = ({
* @private
* @returns {void}
*/
function onMouseOut() {
!_overflowMenuVisible && dispatch(setToolbarHovered(false));
}
const onMouseOut = useCallback(() => {
!overflowMenuVisible && dispatch(setToolbarHovered(false));
}, [ dispatch, overflowMenuVisible ]);
/**
* Dispatches an action signaling the toolbar is being hovered.
@@ -328,9 +188,9 @@ const Toolbox = ({
* @private
* @returns {void}
*/
function onMouseOver() {
const onMouseOver = useCallback(() => {
dispatch(setToolbarHovered(true));
}
}, [ dispatch ]);
/**
* Toggle the toolbar visibility when tabbing into it.
@@ -338,35 +198,48 @@ const Toolbox = ({
* @returns {void}
*/
const onTabIn = useCallback(() => {
if (!_visible) {
if (!toolbarVisible) {
dispatch(showToolbox());
}
}, [ _visible ]);
}, [ toolbarVisible, dispatch ]);
/**
* Renders the toolbox content.
*
* @returns {ReactElement}
*/
function renderToolboxContent() {
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${_isMobile || _isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
if (iAmRecorder || iAmSipGateway) {
return null;
}
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons();
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& (
(!_reactionsButtonEnabled && (raiseHandInOverflowMenu || _isNarrowLayout || _isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions')
);
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
const endConferenceSupported = Boolean(conference?.isEndConferenceSupported() && isModerator);
const isMobile = isMobileBrowser();
return (
const rootClassNames = `new-toolbox ${toolbarVisible ? 'visible' : ''} ${
toolbarButtonsToUse.length ? '' : 'no-buttons'} ${chatOpen ? 'shift-right' : ''}`;
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${isMobile || isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = getVisibleButtons({
allButtons,
buttonsWithNotifyClick,
toolbarButtons: toolbarButtonsToUse,
clientWidth,
jwtDisabledButtons,
mainToolbarButtonsThresholds
});
const raiseHandInOverflowMenu = overflowMenuButtons.some(({ key }) => key === 'raisehand');
const showReactionsInOverflowMenu = _shouldDisplayReactionsButtons
&& (
(!reactionsButtonEnabled && (raiseHandInOverflowMenu || isNarrowLayout || isMobile))
|| overflowMenuButtons.some(({ key }) => key === 'reactions'));
const showRaiseHandInReactionsMenu = showReactionsInOverflowMenu && raiseHandInOverflowMenu;
return (
<div
className = { cx(rootClassNames, shiftUp && 'shift-up') }
id = 'new-toolbox'>
<div className = { containerClassName }>
<div
className = 'toolbox-content-wrapper'
onFocus = { onTabIn }
{ ...(_isMobile ? {} : {
{ ...(isMobile ? {} : {
onMouseOut,
onMouseOver
}) }>
@@ -407,7 +280,7 @@ const Toolbox = ({
return acc;
}, []) }
isOpen = { _overflowMenuVisible }
isOpen = { overflowMenuVisible }
key = 'overflow-menu'
onToolboxEscKey = { onEscKey }
onVisibilityChange = { onSetOverflowVisible }
@@ -415,104 +288,38 @@ const Toolbox = ({
showReactionsMenu = { showReactionsInOverflowMenu } />
)}
{isButtonEnabled('hangup', _toolbarButtons) && (
_endConferenceSupported
{isButtonEnabled('hangup', toolbarButtonsToUse) && (
endConferenceSupported
? <HangupMenuButton
ariaControls = 'hangup-menu'
isOpen = { _hangupMenuVisible }
isOpen = { hangupMenuVisible }
key = 'hangup-menu'
notifyMode = { _buttonsWithNotifyClick?.get('hangup-menu') }
notifyMode = { buttonsWithNotifyClick?.get('hangup-menu') }
onVisibilityChange = { onSetHangupVisible }>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
className = { classes.hangupMenu }
hidden = { false }
inDrawer = { _overflowDrawer }
inDrawer = { overflowDrawer }
onKeyDown = { onEscKey }>
<EndConferenceButton
buttonKey = 'end-meeting'
notifyMode = { _buttonsWithNotifyClick?.get('end-meeting') } />
notifyMode = { buttonsWithNotifyClick?.get('end-meeting') } />
<LeaveConferenceButton
buttonKey = 'hangup'
notifyMode = { _buttonsWithNotifyClick?.get('hangup') } />
notifyMode = { buttonsWithNotifyClick?.get('hangup') } />
</ContextMenu>
</HangupMenuButton>
: <HangupButton
buttonKey = 'hangup'
customClass = 'hangup-button'
key = 'hangup-button'
notifyMode = { _buttonsWithNotifyClick.get('hangup') }
visible = { isButtonEnabled('hangup', _toolbarButtons) } />
notifyMode = { buttonsWithNotifyClick.get('hangup') }
visible = { isButtonEnabled('hangup', toolbarButtonsToUse) } />
)}
</div>
</div>
</div>
);
}
if (_disabled) {
return null;
}
const rootClassNames = `new-toolbox ${_visible ? 'visible' : ''} ${
_toolbarButtons.length ? '' : 'no-buttons'} ${_chatOpen ? 'shift-right' : ''}`;
return (
<div
className = { cx(rootClassNames, _shiftUp && 'shift-up') }
id = 'new-toolbox'>
{renderToolboxContent()}
</div>
);
};
/**
* Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @param {Object} ownProps - The props explicitly passed.
* @private
* @returns {{}}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const { conference } = state['features/base/conference'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
const endConferenceSupported = conference?.isEndConferenceSupported() && isLocalParticipantModerator(state);
const {
customToolbarButtons,
iAmRecorder,
iAmSipGateway
} = state['features/base/config'];
const {
hangupMenuVisible,
overflowMenuVisible,
overflowDrawer
} = state['features/toolbox'];
const { clientWidth } = state['features/base/responsive-ui'];
const toolbarButtons = ownProps.toolbarButtons || state['features/toolbox'].toolbarButtons;
return {
_buttonsWithNotifyClick: state['features/toolbox'].buttonsWithNotifyClick,
_chatOpen: state['features/chat'].isOpen,
_clientWidth: clientWidth,
_customToolbarButtons: customToolbarButtons,
_dialog: Boolean(state['features/base/dialog'].component),
_disabled: Boolean(iAmRecorder || iAmSipGateway),
_endConferenceSupported: Boolean(endConferenceSupported),
_isMobile: isMobileBrowser(),
_jwtDisabledButtons: getJwtDisabledButtons(state),
_hangupMenuVisible: hangupMenuVisible,
_isNarrowLayout: isNarrowLayout,
_overflowMenuVisible: overflowMenuVisible,
_overflowDrawer: overflowDrawer,
_reactionsButtonEnabled: isReactionsButtonEnabled(state),
_shiftUp: state['features/toolbox'].shiftUp,
_shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state),
_toolbarButtons: toolbarButtons,
_visible: isToolboxVisible(state)
};
}
export default translate(connect(_mapStateToProps)(Toolbox));

View File

@@ -6,23 +6,23 @@ import { ToolbarButton } from './types';
export const THRESHOLDS = [
{
width: 565,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants', 'tileview' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ]
},
{
width: 520,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants', 'tileview' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ]
},
{
width: 470,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ]
},
{
width: 420,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ]
},
{
width: 370,
order: [ 'microphone', 'camera', 'chat', 'participants' ]
order: [ 'microphone', 'camera', 'chat', 'participants-pane' ]
},
{
width: 225,
@@ -34,7 +34,43 @@ export const THRESHOLDS = [
}
];
export const NOT_APPLICABLE = 'N/A';
/**
* Main toolbar buttons priority used to determine which button should be picked to fill empty spaces for disabled
* buttons.
*/
export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
'microphone',
'camera',
'desktop',
'chat',
'raisehand',
'reactions',
'participants-pane',
'tileview',
'invite',
'toggle-camera',
'videoquality',
'fullscreen',
'security',
'closedcaptions',
'recording',
'livestreaming',
'linktosalesforce',
'sharedvideo',
'shareaudio',
'noisesuppression',
'whiteboard',
'etherpad',
'select-background',
'stats',
'settings',
'shortcuts',
'profile',
'embedmeeting',
'feedback',
'download',
'help'
];
export const TOOLBAR_TIMEOUT = 4000;

View File

@@ -1,7 +1,8 @@
import { IReduxState } from '../app/types';
import { FEATURES_TO_BUTTONS_MAPPING } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { isJwtFeatureEnabledStateless } from '../base/jwt/functions';
import { IGUMPendingState } from '../base/media/types';
import { IParticipantFeatures } from '../base/participants/types';
/**
* Indicates if the audio mute button is disabled or not.
@@ -19,12 +20,18 @@ export function isAudioMuteButtonDisabled(state: IReduxState) {
/**
* Returns the buttons corresponding to features disabled through jwt.
*
* @param {IReduxState} state - The state from the Redux store.
* @param {string | undefined} jwt - The jwt token.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @returns {string[]} - The disabled by jwt buttons array.
*/
export function getJwtDisabledButtons(state: IReduxState) {
export function getJwtDisabledButtons(jwt: string | undefined, localParticipantFeatures?: IParticipantFeatures) {
return Object.keys(FEATURES_TO_BUTTONS_MAPPING).reduce((acc: string[], current: string) => {
if (!isJwtFeatureEnabled(state, current, true)) {
if (!isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures,
feature: current,
ifNoToken: true
})) {
acc.push(FEATURES_TO_BUTTONS_MAPPING[current as keyof typeof FEATURES_TO_BUTTONS_MAPPING]);
}

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