mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-28 04:17:48 +00:00
Compare commits
51 Commits
8002
...
fix-p2p-co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ddab7464c | ||
|
|
9076fb3e4a | ||
|
|
d8079a4232 | ||
|
|
c992a8274c | ||
|
|
b5e059a0a9 | ||
|
|
0be3e2b103 | ||
|
|
ae138c1a15 | ||
|
|
0f8de50d26 | ||
|
|
ff7b6010bd | ||
|
|
270cdd017d | ||
|
|
f4cae6350b | ||
|
|
f0126a43f3 | ||
|
|
b5deb65815 | ||
|
|
6d9bbe0376 | ||
|
|
da634c211e | ||
|
|
61d96a5fd5 | ||
|
|
07e46b9399 | ||
|
|
3139111d36 | ||
|
|
205f88ec5a | ||
|
|
c8d6cdf6c8 | ||
|
|
7022243a95 | ||
|
|
b2ae72249d | ||
|
|
8ee6d179d5 | ||
|
|
bebcfa3fd7 | ||
|
|
d0130f9975 | ||
|
|
efd2db70ca | ||
|
|
befffa7e85 | ||
|
|
0368b4d671 | ||
|
|
acccd760d1 | ||
|
|
7696dbc347 | ||
|
|
e25ceebeec | ||
|
|
c7ee6280d2 | ||
|
|
796a2217aa | ||
|
|
e9bf1ada51 | ||
|
|
667b9fba67 | ||
|
|
bbf55e8476 | ||
|
|
11b48a9690 | ||
|
|
3b4554f4cf | ||
|
|
d5dd5e4560 | ||
|
|
46ea1f577c | ||
|
|
be06547cc4 | ||
|
|
bd631f5968 | ||
|
|
bc69a35cd6 | ||
|
|
75e7f64311 | ||
|
|
cdcb4cdbfb | ||
|
|
c31ef2ebc7 | ||
|
|
d49a419284 | ||
|
|
0913554af9 | ||
|
|
9af0003c63 | ||
|
|
3ecc16dc87 | ||
|
|
ef387c939a |
2
Makefile
2
Makefile
@@ -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 && \
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
21
config.js
21
config.js
@@ -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 :
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"hsb": "Hornjoserbšćina",
|
||||
"hu": "Magyar",
|
||||
"hy": "Հայերեն",
|
||||
"id": "Bahasa",
|
||||
"is": "Íslenska",
|
||||
"it": "Italiano",
|
||||
"ja": "日本語",
|
||||
|
||||
1559
lang/main-id.json
Normal file
1559
lang/main-id.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"hi": "Hindi",
|
||||
"hmn": "Hmong",
|
||||
"hr": "Croatian",
|
||||
"hsb": "Upper Sorbian",
|
||||
"ht": "Haitian Creole",
|
||||
"hu": "Hungarian",
|
||||
"hy": "Armenian",
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
6
modules/API/external/external_api.js
vendored
6
modules/API/external/external_api.js
vendored
@@ -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
685
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
5
react-native-sdk/update_dependencies.js
vendored
5
react-native-sdk/update_dependencies.js
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))) ?? '';
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -185,6 +185,7 @@ export default [
|
||||
'localRecording',
|
||||
'localSubject',
|
||||
'logging',
|
||||
'mainToolbarButtons',
|
||||
'maxFullResolutionParticipants',
|
||||
'mouseMoveCallbackInterval',
|
||||
'notifications',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface ITrackOptions {
|
||||
* any.
|
||||
*/
|
||||
export interface ITrack {
|
||||
codec: string;
|
||||
getOriginalStream: Function;
|
||||
isReceivingData: boolean;
|
||||
jitsiTrack: any;
|
||||
|
||||
@@ -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>);
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ const useContextMenu = <T>(): [(force?: boolean | Object) => void,
|
||||
return;
|
||||
}
|
||||
|
||||
if (raiseContext !== initialState) {
|
||||
if (raiseContext !== initialState || force) {
|
||||
setRaiseContext(initialState);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -116,7 +116,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
|
||||
if (!isReactionsEnabled(state)) {
|
||||
return;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const { participant, data } = action;
|
||||
|
||||
@@ -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 ]);
|
||||
|
||||
|
||||
@@ -314,6 +314,7 @@ function _calendarNotification({ dispatch, getState }: IStore, eventToShow: any)
|
||||
customActionType,
|
||||
description,
|
||||
icon,
|
||||
maxLines: 1,
|
||||
title,
|
||||
uid
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
25
react/features/embed-meeting/hooks.ts
Normal file
25
react/features/embed-meeting/hooks.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
24
react/features/etherpad/hooks.ts
Normal file
24
react/features/etherpad/hooks.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
react/features/feedback/hooks.web.ts
Normal file
23
react/features/feedback/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
25
react/features/keyboard-shortcuts/hooks.web.ts
Normal file
25
react/features/keyboard-shortcuts/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 } />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
23
react/features/reactions/hooks.web.ts
Normal file
23
react/features/reactions/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
63
react/features/recording/hooks.web.ts
Normal file
63
react/features/recording/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
28
react/features/security/functions.ts
Normal file
28
react/features/security/functions.ts
Normal 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);
|
||||
}
|
||||
45
react/features/security/hooks.web.ts
Normal file
45
react/features/security/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
react/features/speaker-stats/hooks.web.ts
Normal file
23
react/features/speaker-stats/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
react/features/subtitles/hooks.web.ts
Normal file
23
react/features/subtitles/hooks.web.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
1
react/features/toolbox/components/index.native.ts
Normal file
1
react/features/toolbox/components/index.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CustomOptionButton } from './native/CustomOptionButton';
|
||||
1
react/features/toolbox/components/index.web.ts
Normal file
1
react/features/toolbox/components/index.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as CustomOptionButton } from './web/CustomOptionButton';
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user